diff --git a/printer/doc.go b/printer/doc.go new file mode 100644 index 00000000..7035b237 --- /dev/null +++ b/printer/doc.go @@ -0,0 +1,97 @@ +/* +Printer provides features for printing out IPLD nodes and their contained data in a human-readable diagnostic format. + +Outputs should look like... + + map{ + string{"foo"}: string{"bar"} + string{"zot"}: struct{ + someFieldName: list{ + 0: string{"this list is untyped"} + 1: string{"and contains a mixture of kinds of values"} + 2: int{400} + 3: bool{true} + } + otherField: list{ + 0: string{"mind you: 'ANamedListType' is the name of the *list type*."} + 1: string{"it is not the name of the types of the value."} + 2: string{"you'd have to look at the schema for that information."} + 3: string{"or, of course, you can see it at the start of each of these entries, since they are also each annotated."} + } + moreField: list<[nullable String]>{ + 0: string{"this is a typed list"} + 1: string{"but anonymous (meaning you see the value type in the 'name' of it)"} + 2: null + } + } + string{"frog"}: map<{String:String}>{ + string{"as you have probably imagined"}: string{"this is a typed (but anonymous type) map"} + } + string{"numbers"}: int{1} + string{"binary"}: bytes{ABCDEF0123456789} + string{"typed numbers"}: int{9000} + string{"typed string"}: string{"okay, this one needed some marker prefixes."} + string{"map with typed keys"}: map<{MyNamedTypeString:MyNamedTypeString}>{ + string{"work just fine"}: string{"there's no ambiguity"} + string{"you could elide key type info"}: string{"as long as its a string kind, anyway"} + string{"but we don't by default"}: string{"explicit is good, especially in a debug tool!"} + } + string{"structs"}: struct{ + foo: string{"do not need to have quoted field names"} + bar: string{"because (unlike map keys) their character range is already restricted"} + } + string{"unit types"}: unit + string{"notice unit types"}: string{"have no braces at all, because they have literally no further details. they're all type info."} + string{"unions"}: union{string{ + "that was wild, wasn't it. Check out these double closing braces, coming up, too! also the string got forced to a new line, even though it usually would've clung closer to its type and kind marker." + }} + string{"enums"}: enum{"inhabitant name"} + string{"typed bools"}: bool{true} + string{"map with struct keys"{: map<{FooBar:String}>{ + struct{foo:"foo", bar:"bar"}: string{"that one probably surprised you, didn't it?"} + struct{foo:"hmmm", bar:"maybe"}: string{"we might be able to get away without the kind+type marker, actually. but we need the one-liner struct content printing, at least, for sure."} + } + string{"map with really wicked keys"}: map<{WickedNestedUnion:String}>{ + union{union{string{"wow"}}}: "yeah, that happens sometimes" + } + } + +The pattern is a preamble saying what kind the value is (and what type, if applicable), followed by the actual value content, in braces. +For untyped nodes, this means `kindname{"value"}` (so: `string{"foo"}` and `int{12}` and `bool{true}` etc), +or for typed nodes, we get `typekindname{"value"}`. +In addition to the example above, you can check out the tests for a few more examples of how it looks. + +Some configuration options are available to elide some information. +For example, some configuration can reduce the amount of annotational weight around strings +(which is possible to do without getting completely vague because the quotation markings for strings already are syntatically distinctive). +Not all things can be configured for elision, however. +For example, for the various number kinds, the kind preambles are always required. (Number parsers are otherwise often an annoying lookahead problem.) +Similarly, for bytes, the kind preamble is always required. (Among other things, the hexidecimal up until the first letter could be confused with an integer, if we didn't label both of them.) +Anything that's typed also gets a preamble with the type and kind information, even if its kind is something we'd otherwise elide, like string. + +Notice that struct fields aren't quoted. (It's not necessary, because field names are already constrained.) +But map keys are. (They need quoting because they can be any string.) + +Note that the output of printer is NOT INTENDED TO BE PARSABLE. +It is NOT an IPLD codec! +It is a diagnostic format only. +Much of the information included (especially about schema type information) +is _more_ information than the IPLD data model holds alone, +so trying to re-parse the printer output would be a strange choice. + +The diagnostic format emitted by printer is not formally specified, +and is not necessarily language-agnostic. +It may not even remain stable across releases of this library. +It is intended to be used for diagnostics only. + +*/ +package printer + +/* +How to print ADLs is not yet clear. + +Perhaps something like `` will do; +this would also stack reasonably clearly with types as ``; +this style would have the downside of making ADLs look *very* different than other mere representation strategies, +which may be totally reasonable or mildly questionable depending on how purist you feel about that. +*/ diff --git a/printer/printer.go b/printer/printer.go new file mode 100644 index 00000000..a03fc51c --- /dev/null +++ b/printer/printer.go @@ -0,0 +1,345 @@ +package printer + +import ( + "io" + "os" + "strconv" + "strings" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/schema" +) + +// Print emits a textual description of the node tree straight to stdout. +// All printer configuration will be the default; +// links will be printed, and will not be traversed. +func Print(n datamodel.Node) { + Config{}.Print(n) +} + +// Sprint returns a textual description of the node tree. +// All printer configuration will be the default; +// links will be printed, and will not be traversed. +func Sprint(n datamodel.Node) string { + return Config{}.Sprint(n) +} + +// Fprint accepts an io.Writer to which a textual description of the node tree will be written. +// All printer configuration will be the default; +// links will be printed, and will not be traversed. +func Fprint(w io.Writer, n datamodel.Node) { + Config{}.Fprint(w, n) +} + +// Print emits a textual description of the node tree straight to stdout. +// The configuration structure this method is attached to can be used to specified details for how the printout will be formatted. +func (cfg Config) Print(n datamodel.Node) { + cfg.Fprint(os.Stdout, n) +} + +// Sprint returns a textual description of the node tree. +// The configuration structure this method is attached to can be used to specified details for how the printout will be formatted. +func (cfg Config) Sprint(n datamodel.Node) string { + var buf strings.Builder + cfg.Fprint(&buf, n) + return buf.String() +} + +// Fprint accepts an io.Writer to which a textual description of the node tree will be written. +// The configuration structure this method is attached to can be used to specified details for how the printout will be formatted. +func (cfg Config) Fprint(w io.Writer, n datamodel.Node) { + pr := printBuf{w, cfg} + pr.Config.init() + pr.doString(0, printState_normal, n) +} + +type Config struct { + // If true, long strings and long byte sequences will truncated, and will include ellipses instead. + // + // Not yet supported. + Abbreviate bool + + // If set, the indentation to use. + // If nil, it will be treated as a default "\t". + Indentation []byte + + // Probably does exactly what you think it does. + StartingIndent []byte + + // Set to true if you like verbosity, I guess. + // If false, strings will only have kind+type markings if they're typed. + // + // Not yet supported. + AlwaysMarkStrings bool + + // Set to true if you want type info to be skipped for any type that's in the Prelude + // (e.g. instead of `string{` seeing only `string{` is preferred, etc). + // + // Not yet supported. + ElidePreludeTypeInfo bool + + // Set to true if you want maps to use "complex"-style printouts: + // meaning they will print their keys on separate lines than their values, + // and keys may spread across mutiple lines if appropriate. + // + // If not set, a heuristic will be used based on if the map is known to + // have keys that are complex enough that rendering them as oneline seems likely to overload. + // See Config.useCmplxKeys for exactly how that's deteremined. + UseMapComplexStyleAlways bool + + // For maps to use "complex"-style printouts (or not) per type. + // See docs on UseMapComplexStyleAlways for the overview of what "complex"-style means. + UseMapComplexStyleOnType map[schema.TypeName]bool +} + +func (cfg *Config) init() { + if cfg.Indentation == nil { + cfg.Indentation = []byte{'\t'} + } +} + +// oneline decides if a value should be flatted into printing on a single, +// or if it's allowed to spread out over multiple lines. +// Note that this will not be asked if something outside of a value has already declared it's +// doing a oneline rendering; that railroads everything within it into that mode too. +func (cfg Config) oneline(typ schema.Type, isInKey bool) bool { + return isInKey // Future: this could become customizable, with some kind of Always|OnlyInKeys|Never option enum per type. +} + +// useRepr decides if a value should be printed using its representation. +// Sometimes configuring this to be true for structs or unions with stringy representations +// will cause map printouts using them as keys to become drastically more readable +// (if with some loss of informativeness, or at least loss of explicitness). +func (cfg Config) useRepr(typ schema.Type, isInKey bool) bool { + return false +} + +// useCmplxKeys decides if a map should print itself using a multi-line and extra-indented style for keys. +func (cfg Config) useCmplxKeys(mapn datamodel.Node) bool { + if cfg.UseMapComplexStyleAlways { + return true + } + tn, ok := mapn.(schema.TypedNode) + if !ok { + return false + } + force, ok := cfg.UseMapComplexStyleOnType[tn.Type().Name()] + if ok { + return force + } + ti, ok := tn.Type().(*schema.TypeMap) + if !ok { // Probably should never even have been asked, then? + panic("how did you get here?") + } + return !cfg.oneline(ti.KeyType(), true) +} + +// FUTURE: one could imagine putting an optional LinkSystem param into the Config, too, and some recursion control. +// It's definitely going to be the default to do zero recursion across links, though, +// as doing that requires creating graph visualizations, and that is both possible, yet to do well becomes rather nontrivial. +// Also, often a single node's tree visualization has been enough to get started debugging whatever I need to debug so far. + +type printBuf struct { + wr io.Writer + + Config +} + +func (z *printBuf) writeString(s string) { + z.wr.Write([]byte(s)) +} + +func (z *printBuf) doIndent(indentLevel int) { + z.wr.Write(z.Config.StartingIndent) + for i := 0; i < indentLevel; i++ { + z.wr.Write(z.Config.Indentation) + } +} + +const ( + printState_normal uint8 = iota + printState_isKey // may sometimes entersen or stringify things harder. + printState_isValue // signals that we're continuing a line that started with a key (so, don't emit indent). + printState_isCmplxKey // used to ask something to use multiline form, and an extra indent -- the opposite of what isKey does. + printState_isCmplxValue // we're continuing a line (so don't emit indent), and we're stuck in complex mode (so keep telling your children to stay in this state too). +) + +func (z *printBuf) doString(indentLevel int, printState uint8, n datamodel.Node) { + // First: indent. + switch printState { + case printState_normal, printState_isKey, printState_isCmplxKey: + z.doIndent(indentLevel) + } + // Second: the typekind and type name; or, just the kind, if there's no type. + // Note: this can be somewhat overbearing -- for example, typed strings are going to get called out as `string{"value"}`. + // This is rather agonizingly verbose, but also accurate; I'm not sure if we'd want to elide information about typed-vs-untyped entirely. + if tn, ok := n.(schema.TypedNode); ok { + z.writeString(tn.Type().TypeKind().String()) + z.writeString("<") + z.writeString(string(tn.Type().Name())) + z.writeString(">") + switch tn.Type().TypeKind() { + case schema.TypeKind_Invalid: + z.writeString("{?!}") + case schema.TypeKind_Map: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_List: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Unit: + return // that's it! there's no content data for a unit type. + case schema.TypeKind_Bool: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Int: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Float: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_String: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Bytes: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Link: + // continue -- the data-model driven behavior is sufficient to handle the content. + case schema.TypeKind_Struct: + // Very similar to a map, but keys aren't quoted. + // Also, because it's possible for structs to be keys in a map themselves, they potentially need oneline emission. + // Or, to customize emission in another direction if being a key in a map that's printing in "complex" mode. + // FUTURE: there should also probably be some way to configure instructions to use their representation form instead. + oneline := + printState == printState_isCmplxValue || + printState != printState_isCmplxKey && z.Config.oneline(tn.Type(), printState == printState_isKey) + deepen := 1 + if printState == printState_isCmplxKey { + deepen = 2 + } + childState := printState_isValue + if oneline { + childState = printState_isCmplxValue + } + z.writeString("{") + if !oneline { + z.writeString("\n") + } + for itr := n.MapIterator(); !itr.Done(); { + k, v, _ := itr.Next() + if !oneline { + z.doIndent(indentLevel + deepen) + } + fn, _ := k.AsString() + z.writeString(fn) + z.writeString(": ") + z.doString(indentLevel+deepen, childState, v) + if oneline { + if !itr.Done() { + z.writeString(", ") + } + } else { + z.writeString("\n") + } + } + if !oneline { + z.doIndent(indentLevel) + } + z.writeString("}") + return + case schema.TypeKind_Union: + // There will only be one thing in it, but we still have to use an iterator + // to figure out what that is if we're doing this generically. + // We can ignore the key and just look at the value type again though (even though those are the same in practice). + _, v, _ := n.MapIterator().Next() + z.writeString("{") + z.doString(indentLevel, printState_isValue, v) + z.writeString("}") + return + case schema.TypeKind_Enum: + panic("TODO") + default: + panic("unreachable") + } + } else { + if n.IsAbsent() { + z.writeString("absent") + return + } + z.writeString(n.Kind().String()) + } + // Third: all the actual content. + // FUTURE: this is probably gonna become... somewhat more conditional, and may end up being a sub-function to be reasonably wieldy. + switch n.Kind() { + case datamodel.Kind_Map: + // Maps have to decide if they have complex keys and want to use an additionally-intended pattern to make that readable. + // "Complex" here means roughly: if you try to cram them into one line, it doesn't look good. + // This choice starts at the map but is mostly executed during the printing of the key: + // the key will start itself at normal indentation, + // but should then doubly indent all its nested values (assuming it has any). + cmplxKeys := z.Config.useCmplxKeys(n) + childKeyState := printState_isKey + if cmplxKeys { + childKeyState = printState_isCmplxKey + } + z.writeString("{\n") + for itr := n.MapIterator(); !itr.Done(); { + k, v, err := itr.Next() + if err != nil { + z.doIndent(indentLevel + 1) + z.writeString("!! map iteration step yielded error: ") + z.writeString(err.Error()) + z.writeString("\n") + break + } + z.doString(indentLevel+1, childKeyState, k) + z.writeString(": ") + z.doString(indentLevel+1, printState_isValue, v) + z.writeString("\n") + } + z.doIndent(indentLevel) + z.writeString("}") + case datamodel.Kind_List: + z.writeString("{\n") + for itr := n.ListIterator(); !itr.Done(); { + idx, v, err := itr.Next() + if err != nil { + z.doIndent(indentLevel + 1) + z.writeString("!! list iteration step yielded error: ") + z.writeString(err.Error()) + z.writeString("\n") + break + } + z.doIndent(indentLevel + 1) + z.writeString(strconv.FormatInt(idx, 10)) + z.writeString(": ") + z.doString(indentLevel+1, printState_isValue, v) + z.writeString("\n") + } + z.doIndent(indentLevel) + z.writeString("}") + case datamodel.Kind_Null: + // nothing: we already wrote the word "null" when we wrote the kind info prefix. + case datamodel.Kind_Bool: + z.writeString("{") + if b, _ := n.AsBool(); b { + z.writeString("true") + } else { + z.writeString("false") + } + z.writeString("}") + case datamodel.Kind_Int: + x, _ := n.AsInt() + z.writeString("{") + z.writeString(strconv.FormatInt(x, 10)) + z.writeString("}") + case datamodel.Kind_Float: + x, _ := n.AsFloat() + z.writeString("{") + strconv.FormatFloat(x, 'f', -1, 64) + z.writeString("}") + case datamodel.Kind_String: + x, _ := n.AsString() + z.writeString("{") + z.writeString(strconv.QuoteToGraphic(x)) + z.writeString("}") + case datamodel.Kind_Bytes: + panic("TODO") + case datamodel.Kind_Link: + panic("TODO") + } +} diff --git a/printer/printer_test.go b/printer/printer_test.go new file mode 100644 index 00000000..8a2653ed --- /dev/null +++ b/printer/printer_test.go @@ -0,0 +1,179 @@ +package printer + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "github.com/warpfork/go-wish" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" +) + +func TestSimpleData(t *testing.T) { + n, _ := qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "some key", qp.String("some value")) + qp.MapEntry(ma, "another key", qp.String("another value")) + qp.MapEntry(ma, "nested map", qp.Map(2, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "deeper entries", qp.String("deeper values")) + qp.MapEntry(ma, "more deeper entries", qp.String("more deeper values")) + })) + qp.MapEntry(ma, "nested list", qp.List(2, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.Int(1)) + qp.ListEntry(la, qp.Int(2)) + })) + }) + qt.Check(t, Sprint(n), qt.CmpEquals(), wish.Dedent(` + map{ + string{"some key"}: string{"some value"} + string{"another key"}: string{"another value"} + string{"nested map"}: map{ + string{"deeper entries"}: string{"deeper values"} + string{"more deeper entries"}: string{"more deeper values"} + } + string{"nested list"}: list{ + 0: int{1} + 1: int{2} + } + }`, + )) +} + +func TestTypedData(t *testing.T) { + t.Run("structs", func(t *testing.T) { + type FooBar struct { + Foo string + Bar string + } + ts := schema.MustTypeSystem( + schema.SpawnString("String"), + schema.SpawnStruct("FooBar", []schema.StructField{ + schema.SpawnStructField("foo", "String", false, false), + schema.SpawnStructField("bar", "String", false, false), + }, nil), + ) + n := bindnode.Wrap(&FooBar{"x", "y"}, ts.TypeByName("FooBar")) + qt.Check(t, Sprint(n), qt.CmpEquals(), wish.Dedent(` + struct{ + foo: string{"x"} + bar: string{"y"} + }`, + )) + }) + t.Run("map-with-struct-keys", func(t *testing.T) { + type FooBar struct { + Foo string + Bar string + } + type WowMap struct { + Keys []FooBar + Values map[FooBar]string + } + ts := schema.MustTypeSystem( + schema.SpawnString("String"), + schema.SpawnStruct("FooBar", []schema.StructField{ + schema.SpawnStructField("foo", "String", false, false), + schema.SpawnStructField("bar", "String", false, false), + }, schema.SpawnStructRepresentationStringjoin(":")), + schema.SpawnMap("WowMap", "FooBar", "String", false), + ) + n := bindnode.Wrap(&WowMap{ + Keys: []FooBar{{"x", "y"}, {"z", "z"}}, + Values: map[FooBar]string{ + {"x", "y"}: "a", + {"z", "z"}: "b", + }, + }, ts.TypeByName("WowMap")) + qt.Check(t, Sprint(n), qt.CmpEquals(), wish.Dedent(` + map{ + struct{foo: string{"x"}, bar: string{"y"}}: string{"a"} + struct{foo: string{"z"}, bar: string{"z"}}: string{"b"} + }`, + )) + }) + t.Run("map-with-nested-struct-keys", func(t *testing.T) { + type Baz struct { + Baz string + } + type FooBar struct { + Foo string + Bar Baz + Baz Baz + } + type WowMap struct { + Keys []FooBar + Values map[FooBar]Baz + } + ts := schema.MustTypeSystem( + schema.SpawnString("String"), + schema.SpawnStruct("FooBar", []schema.StructField{ + schema.SpawnStructField("foo", "String", false, false), + schema.SpawnStructField("bar", "Baz", false, false), + schema.SpawnStructField("baz", "Baz", false, false), + }, schema.SpawnStructRepresentationStringjoin(":")), + schema.SpawnStruct("Baz", []schema.StructField{ + schema.SpawnStructField("baz", "String", false, false), + }, schema.SpawnStructRepresentationStringjoin(":")), + schema.SpawnMap("WowMap", "FooBar", "Baz", false), + ) + n := bindnode.Wrap(&WowMap{ + Keys: []FooBar{{"x", Baz{"y"}, Baz{"y"}}, {"z", Baz{"z"}, Baz{"z"}}}, + Values: map[FooBar]Baz{ + {"x", Baz{"y"}, Baz{"y"}}: Baz{"a"}, + {"z", Baz{"z"}, Baz{"z"}}: Baz{"b"}, + }, + }, ts.TypeByName("WowMap")) + t.Run("complex-keys-in-effect", func(t *testing.T) { + cfg := Config{ + UseMapComplexStyleAlways: true, + } + qt.Check(t, cfg.Sprint(n), qt.CmpEquals(), wish.Dedent(` + map{ + struct{ + foo: string{"x"} + bar: struct{ + baz: string{"y"} + } + baz: struct{ + baz: string{"y"} + } + }: struct{ + baz: string{"a"} + } + struct{ + foo: string{"z"} + bar: struct{ + baz: string{"z"} + } + baz: struct{ + baz: string{"z"} + } + }: struct{ + baz: string{"b"} + } + }`, + )) + }) + t.Run("complex-keys-in-disabled", func(t *testing.T) { + cfg := Config{ + UseMapComplexStyleOnType: map[schema.TypeName]bool{ + "WowMap": false, + }, + } + qt.Check(t, cfg.Sprint(n), qt.CmpEquals(), wish.Dedent(` + map{ + struct{foo: string{"x"}, bar: struct{baz: string{"y"}}, baz: struct{baz: string{"y"}}}: struct{ + baz: string{"a"} + } + struct{foo: string{"z"}, bar: struct{baz: string{"z"}}, baz: struct{baz: string{"z"}}}: struct{ + baz: string{"b"} + } + }`, + )) + }) + }) + +}