From 47a089b3e8982668b18f43ba2d274451cdd94756 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 08:37:13 +1000 Subject: [PATCH 01/12] Rename `value` to `Value` so it's available in filters. --- array.go | 4 ++-- interface.go | 2 +- map.go | 6 +++--- ptr.go | 2 +- shallow.go | 16 ++++++++-------- slice.go | 2 +- struct.go | 8 ++++---- value.go | 11 ++++++----- visitor.go | 6 +++--- 9 files changed, 29 insertions(+), 28 deletions(-) diff --git a/array.go b/array.go index 8f9596f..409d3bb 100644 --- a/array.go +++ b/array.go @@ -8,7 +8,7 @@ import ( ) // visitArray formats values with a kind of reflect.Array or Slice. -func (vis *visitor) visitArray(w io.Writer, v value) { +func (vis *visitor) visitArray(w io.Writer, v Value) { if v.IsAmbiguousType { vis.write(w, v.TypeName()) } @@ -23,7 +23,7 @@ func (vis *visitor) visitArray(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitArrayValues(w io.Writer, v value) { +func (vis *visitor) visitArrayValues(w io.Writer, v Value) { ambiguous := v.Type.Elem().Kind() == reflect.Interface for i := 0; i < v.Value.Len(); i++ { diff --git a/interface.go b/interface.go index edfdab2..f4b1cfe 100644 --- a/interface.go +++ b/interface.go @@ -5,7 +5,7 @@ import ( ) // visitInterface formats values with a kind of reflect.Interface. -func (vis *visitor) visitInterface(w io.Writer, v value) { +func (vis *visitor) visitInterface(w io.Writer, v Value) { if v.Value.IsNil() { if v.IsAmbiguousType { vis.write(w, v.TypeName()) diff --git a/map.go b/map.go index 2f7fdce..0b2f15e 100644 --- a/map.go +++ b/map.go @@ -12,7 +12,7 @@ import ( // visitMap formats values with a kind of reflect.Map. // // TODO(jmalloc): sort numerically-keyed maps numerically -func (vis *visitor) visitMap(w io.Writer, v value) { +func (vis *visitor) visitMap(w io.Writer, v Value) { if vis.enter(w, v) { return } @@ -32,7 +32,7 @@ func (vis *visitor) visitMap(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitMapElements(w io.Writer, v value) { +func (vis *visitor) visitMapElements(w io.Writer, v Value) { ambiguous := v.Type.Elem().Kind() == reflect.Interface keys, alignment := vis.formatMapKeys(v) @@ -56,7 +56,7 @@ type mapKey struct { // sorted by their string representation. // // padding is the number of padding characters to add to the shortest key. -func (vis *visitor) formatMapKeys(v value) (keys []mapKey, alignment int) { +func (vis *visitor) formatMapKeys(v Value) (keys []mapKey, alignment int) { var w strings.Builder isInterface := v.Type.Key().Kind() == reflect.Interface keys = make([]mapKey, v.Value.Len()) diff --git a/ptr.go b/ptr.go index 66c2bc3..c94eabd 100644 --- a/ptr.go +++ b/ptr.go @@ -5,7 +5,7 @@ import ( ) // visitPtr formats values with a kind of reflect.Ptr. -func (vis *visitor) visitPtr(w io.Writer, v value) { +func (vis *visitor) visitPtr(w io.Writer, v Value) { if vis.enter(w, v) { return } diff --git a/shallow.go b/shallow.go index 57393c4..d5c3f92 100644 --- a/shallow.go +++ b/shallow.go @@ -7,7 +7,7 @@ import ( // visitInt formats values with a kind of reflect.Int, and the related // fixed-sized types. -func (vis *visitor) visitInt(w io.Writer, v value) { +func (vis *visitor) visitInt(w io.Writer, v Value) { if v.IsAmbiguousType { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Int()) @@ -18,7 +18,7 @@ func (vis *visitor) visitInt(w io.Writer, v value) { // visitUint formats values with a kind of reflect.Uint, and the related // fixed-sized types. -func (vis *visitor) visitUint(w io.Writer, v value) { +func (vis *visitor) visitUint(w io.Writer, v Value) { if v.IsAmbiguousType { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Uint()) @@ -28,7 +28,7 @@ func (vis *visitor) visitUint(w io.Writer, v value) { } // visitFloat formats values with a kind of reflect.Float32 and Float64. -func (vis *visitor) visitFloat(w io.Writer, v value) { +func (vis *visitor) visitFloat(w io.Writer, v Value) { if v.IsAmbiguousType { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Float()) @@ -38,7 +38,7 @@ func (vis *visitor) visitFloat(w io.Writer, v value) { } // visitComplex formats values with a kind of reflect.Complex64 and Complex128. -func (vis *visitor) visitComplex(w io.Writer, v value) { +func (vis *visitor) visitComplex(w io.Writer, v Value) { // note that %v formats a complex number already surrounded in parenthesis s := fmt.Sprintf("%v", v.Value.Complex()) @@ -51,7 +51,7 @@ func (vis *visitor) visitComplex(w io.Writer, v value) { } // visitUintptr formats values with a kind of reflect.Uintptr. -func (vis *visitor) visitUintptr(w io.Writer, v value) { +func (vis *visitor) visitUintptr(w io.Writer, v Value) { s := formatPointerHex(v.Value.Uint(), false) if v.IsAmbiguousType { @@ -63,7 +63,7 @@ func (vis *visitor) visitUintptr(w io.Writer, v value) { } // visitUnsafePointer formats values with a kind of reflect.UnsafePointer. -func (vis *visitor) visitUnsafePointer(w io.Writer, v value) { +func (vis *visitor) visitUnsafePointer(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) if v.IsAmbiguousType { @@ -75,7 +75,7 @@ func (vis *visitor) visitUnsafePointer(w io.Writer, v value) { } // visitChan formats values with a kind of reflect.Chan. -func (vis *visitor) visitChan(w io.Writer, v value) { +func (vis *visitor) visitChan(w io.Writer, v Value) { if v.IsAmbiguousType { vis.write(w, v.TypeName()) vis.write(w, "(") @@ -101,7 +101,7 @@ func (vis *visitor) visitChan(w io.Writer, v value) { } // visitFunc formats values with a kind of reflect.Func. -func (vis *visitor) visitFunc(w io.Writer, v value) { +func (vis *visitor) visitFunc(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) if v.IsAmbiguousType { diff --git a/slice.go b/slice.go index f0e9801..5ff2613 100644 --- a/slice.go +++ b/slice.go @@ -5,7 +5,7 @@ import ( ) // visitSlice formats values with a kind of reflect.Slice. -func (vis *visitor) visitSlice(w io.Writer, v value) { +func (vis *visitor) visitSlice(w io.Writer, v Value) { if vis.enter(w, v) { return } diff --git a/struct.go b/struct.go index 44eab3d..8892129 100644 --- a/struct.go +++ b/struct.go @@ -9,8 +9,8 @@ import ( ) // visitStruct formats values with a kind of reflect.Struct. -func (vis *visitor) visitStruct(w io.Writer, v value) { - if v.IsAmbiguousType && !v.IsAnonymous() { +func (vis *visitor) visitStruct(w io.Writer, v Value) { + if v.IsAmbiguousType && !v.IsAnonymousType() { vis.write(w, v.TypeName()) } @@ -24,9 +24,9 @@ func (vis *visitor) visitStruct(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitStructFields(w io.Writer, v value) { +func (vis *visitor) visitStructFields(w io.Writer, v Value) { alignment := longestFieldName(v.Type) - anon := v.IsAnonymous() + anon := v.IsAnonymousType() var ambiguous bool for i := 0; i < v.Type.NumField(); i++ { diff --git a/value.go b/value.go index 7338f4c..87cbe4c 100644 --- a/value.go +++ b/value.go @@ -5,8 +5,8 @@ import ( "strings" ) -// value is a container for a value that can be formatted. -type value struct { +// Value contains information about a Go value that is to be formatted. +type Value struct { // Value is the value to be formatted. Value reflect.Value @@ -21,7 +21,8 @@ type value struct { IsAmbiguousType bool } -func (v value) TypeName() string { +// TypeName returns the name of the value's type formatted for display. +func (v *Value) TypeName() string { n := v.Type.String() n = strings.Replace(n, "interface {", "interface{", -1) @@ -32,7 +33,7 @@ func (v value) TypeName() string { return n } -// IsAnonymous returns true if the value has an anonymous type. -func (v value) IsAnonymous() bool { +// IsAnonymousType returns true if the value has an anonymous type. +func (v *Value) IsAnonymousType() bool { return v.Type.Name() == "" } diff --git a/visitor.go b/visitor.go index 4b79448..09f7a8c 100644 --- a/visitor.go +++ b/visitor.go @@ -31,7 +31,7 @@ func (vis *visitor) visit(w io.Writer, rv reflect.Value, ambiguous bool) (err er return } - v := value{ + v := Value{ Value: rv, Type: rv.Type(), Kind: rv.Kind(), @@ -83,7 +83,7 @@ func (vis *visitor) visit(w io.Writer, rv reflect.Value, ambiguous bool) (err er // // It returns true if the value is nil, or recursion has occurred, indicating // that the value should not be rendered. -func (vis *visitor) enter(w io.Writer, v value) bool { +func (vis *visitor) enter(w io.Writer, v Value) bool { marker := "nil" if !v.Value.IsNil() { @@ -117,7 +117,7 @@ func (vis *visitor) enter(w io.Writer, v value) bool { // leave indicates that a potentially recursive value has finished rendering. // // It must be called after enter(v) returns true. -func (vis *visitor) leave(v value) { +func (vis *visitor) leave(v Value) { if !v.Value.IsNil() { delete(vis.recursionSet, v.Value.Pointer()) } From baafba2a9f97f4c41e51f81c896eeb2e020ce586 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 10:49:23 +1000 Subject: [PATCH 02/12] Improve the `Value` interface in preparation for filters. --- array.go | 25 ++++++++++++++++++++++--- filter.go | 18 ++++++++++++++++++ interface.go | 19 +++++++++++++++++-- map.go | 36 +++++++++++++++++++++++++++++------- printer.go | 41 +++++++++++++++++++++++++++++++++++------ ptr.go | 16 ++++++++++++++-- shallow.go | 18 +++++++++--------- struct.go | 40 ++++++++++++++++++++++++---------------- value.go | 42 +++++++++++++++++++++++++++++++----------- visitor.go | 24 +++++++++++++----------- 10 files changed, 212 insertions(+), 67 deletions(-) create mode 100644 filter.go diff --git a/array.go b/array.go index 409d3bb..986dbdc 100644 --- a/array.go +++ b/array.go @@ -1,6 +1,7 @@ package dapper import ( + "fmt" "io" "reflect" @@ -9,7 +10,7 @@ import ( // visitArray formats values with a kind of reflect.Array or Slice. func (vis *visitor) visitArray(w io.Writer, v Value) { - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) } @@ -24,10 +25,28 @@ func (vis *visitor) visitArray(w io.Writer, v Value) { } func (vis *visitor) visitArrayValues(w io.Writer, v Value) { - ambiguous := v.Type.Elem().Kind() == reflect.Interface + staticType := v.DynamicType.Elem() + isInterface := staticType.Kind() == reflect.Interface for i := 0; i < v.Value.Len(); i++ { - vis.visit(w, v.Value.Index(i), ambiguous) + elem := v.Value.Index(i) + + if isInterface { + fmt.Print("") + } + + vis.visit( + w, + Value{ + Value: elem, + DynamicType: elem.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) + vis.write(w, "\n") } } diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..51cac22 --- /dev/null +++ b/filter.go @@ -0,0 +1,18 @@ +package dapper + +import ( + "io" +) + +// Filter is a function that provides custom formatting logic for specific +// values. +// +// It writes a formatted representation of v to w, and returns the number of +// bytes written. +// +// If the number of bytes written is non-zero, the default formatting logic is +// skipped. +// +// Particular attention should be paid to the v.IsUnexported field. If this flag +// is true, many operations on v.Value are unavailable. +type Filter func(w io.Writer, v Value) (int, error) diff --git a/interface.go b/interface.go index f4b1cfe..29db164 100644 --- a/interface.go +++ b/interface.go @@ -7,7 +7,10 @@ import ( // visitInterface formats values with a kind of reflect.Interface. func (vis *visitor) visitInterface(w io.Writer, v Value) { if v.Value.IsNil() { - if v.IsAmbiguousType { + // for a nil interface, we only want to render the type if the STATIC type is + // ambigious, since the only information we have available is the interface + // type itself, not the actual implementation's type. + if v.IsAmbiguousStaticType { vis.write(w, v.TypeName()) vis.write(w, "(nil)") } else { @@ -17,5 +20,17 @@ func (vis *visitor) visitInterface(w io.Writer, v Value) { return } - vis.visit(w, v.Value.Elem(), v.IsAmbiguousType) + elem := v.Value.Elem() + + vis.visit( + w, + Value{ + Value: elem, + DynamicType: elem.Type(), + StaticType: v.DynamicType, + IsAmbiguousDynamicType: true, + IsAmbiguousStaticType: v.IsAmbiguousStaticType, + IsUnexported: v.IsUnexported, + }, + ) } diff --git a/map.go b/map.go index 0b2f15e..5b3800f 100644 --- a/map.go +++ b/map.go @@ -18,7 +18,7 @@ func (vis *visitor) visitMap(w io.Writer, v Value) { } defer vis.leave(v) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) } @@ -33,7 +33,8 @@ func (vis *visitor) visitMap(w io.Writer, v Value) { } func (vis *visitor) visitMapElements(w io.Writer, v Value) { - ambiguous := v.Type.Elem().Kind() == reflect.Interface + staticType := v.DynamicType.Elem() + isInterface := staticType.Kind() == reflect.Interface keys, alignment := vis.formatMapKeys(v) for _, mk := range keys { @@ -41,7 +42,17 @@ func (vis *visitor) visitMapElements(w io.Writer, v Value) { vis.write(w, mk.String) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-mk.Width)) - vis.visit(w, mv, ambiguous) + vis.visit( + w, + Value{ + Value: mv, + DynamicType: mv.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) vis.write(w, "\n") } } @@ -58,12 +69,23 @@ type mapKey struct { // padding is the number of padding characters to add to the shortest key. func (vis *visitor) formatMapKeys(v Value) (keys []mapKey, alignment int) { var w strings.Builder - isInterface := v.Type.Key().Kind() == reflect.Interface + staticType := v.DynamicType.Key() + isInterface := staticType.Kind() == reflect.Interface keys = make([]mapKey, v.Value.Len()) alignToLastLine := false - for i, k := range v.Value.MapKeys() { - vis.visit(&w, k, isInterface) + for i, mk := range v.Value.MapKeys() { + vis.visit( + &w, + Value{ + Value: mk, + DynamicType: mk.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) s := w.String() w.Reset() @@ -74,7 +96,7 @@ func (vis *visitor) formatMapKeys(v Value) (keys []mapKey, alignment int) { alignToLastLine = max == last } - keys[i] = mapKey{k, s, last} + keys[i] = mapKey{mk, s, last} } sort.Slice( diff --git a/printer.go b/printer.go index 12efb9e..53058c9 100644 --- a/printer.go +++ b/printer.go @@ -5,6 +5,8 @@ import ( "os" "reflect" "strings" + + "github.com/dogmatiq/iago" ) const ( @@ -16,15 +18,15 @@ const ( DefaultRecursionMarker = "" ) -// defaultPrinter is a Printer instance with default settings. -var defaultPrinter Printer - // Printer generates human-readable representations of Go values. // // The output format is intended to be as minimal as possible, without being // ambigious. To that end, type information is only included where it can not be // reliably inferred from the structure of the value. type Printer struct { + // Filters is the set of filters to apply when formatting values. + Filters []Filter + // Indent is the string used to indent nested values. // If it is empty, DefaultIndent is used. Indent string @@ -35,11 +37,17 @@ type Printer struct { RecursionMarker string } +// emptyInterfaceType is the reflect.Type for interface{}. +var emptyInterfaceType = reflect.TypeOf((*interface{})(nil)).Elem() + // Write writes a pretty-printed representation of v to w. // // It returns the number of bytes written. -func (p *Printer) Write(w io.Writer, v interface{}) (int, error) { +func (p *Printer) Write(w io.Writer, v interface{}) (n int, err error) { + defer iago.Recover(&err) + vis := visitor{ + filters: p.Filters, indent: []byte(p.Indent), recursionMarker: p.RecursionMarker, } @@ -52,9 +60,27 @@ func (p *Printer) Write(w io.Writer, v interface{}) (int, error) { vis.recursionMarker = DefaultRecursionMarker } - err := vis.visit(w, reflect.ValueOf(v), true) + rv := reflect.ValueOf(v) + var rt reflect.Type + + if rv.Kind() != reflect.Invalid { + rt = rv.Type() + } - return vis.bytes, err + vis.visit( + w, + Value{ + Value: rv, + DynamicType: rt, + StaticType: emptyInterfaceType, + IsAmbiguousDynamicType: true, + IsAmbiguousStaticType: true, + IsUnexported: false, + }, + ) + + n = vis.bytes + return } // Format returns a pretty-printed representation of v. @@ -68,6 +94,9 @@ func (p *Printer) Format(v interface{}) string { return b.String() } +// defaultPrinter is a Printer instance with default settings. +var defaultPrinter Printer + // Write writes a pretty-printed representation of v to w using the default // printer settings. // diff --git a/ptr.go b/ptr.go index c94eabd..d74bc2c 100644 --- a/ptr.go +++ b/ptr.go @@ -11,9 +11,21 @@ func (vis *visitor) visitPtr(w io.Writer, v Value) { } defer vis.leave(v) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, "*") } - vis.visit(w, v.Value.Elem(), v.IsAmbiguousType) + elem := v.Value.Elem() + + vis.visit( + w, + Value{ + Value: elem, + DynamicType: elem.Type(), + StaticType: elem.Type(), + IsAmbiguousDynamicType: false, + IsAmbiguousStaticType: v.IsAmbiguousStaticType, + IsUnexported: v.IsUnexported, + }, + ) } diff --git a/shallow.go b/shallow.go index d5c3f92..d0449df 100644 --- a/shallow.go +++ b/shallow.go @@ -8,7 +8,7 @@ import ( // visitInt formats values with a kind of reflect.Int, and the related // fixed-sized types. func (vis *visitor) visitInt(w io.Writer, v Value) { - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Int()) } else { @@ -19,7 +19,7 @@ func (vis *visitor) visitInt(w io.Writer, v Value) { // visitUint formats values with a kind of reflect.Uint, and the related // fixed-sized types. func (vis *visitor) visitUint(w io.Writer, v Value) { - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Uint()) } else { @@ -29,7 +29,7 @@ func (vis *visitor) visitUint(w io.Writer, v Value) { // visitFloat formats values with a kind of reflect.Float32 and Float64. func (vis *visitor) visitFloat(w io.Writer, v Value) { - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Float()) } else { @@ -42,7 +42,7 @@ func (vis *visitor) visitComplex(w io.Writer, v Value) { // note that %v formats a complex number already surrounded in parenthesis s := fmt.Sprintf("%v", v.Value.Complex()) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, s) } else { @@ -54,7 +54,7 @@ func (vis *visitor) visitComplex(w io.Writer, v Value) { func (vis *visitor) visitUintptr(w io.Writer, v Value) { s := formatPointerHex(v.Value.Uint(), false) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { @@ -66,7 +66,7 @@ func (vis *visitor) visitUintptr(w io.Writer, v Value) { func (vis *visitor) visitUnsafePointer(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { @@ -76,7 +76,7 @@ func (vis *visitor) visitUnsafePointer(w io.Writer, v Value) { // visitChan formats values with a kind of reflect.Chan. func (vis *visitor) visitChan(w io.Writer, v Value) { - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, "(") } @@ -95,7 +95,7 @@ func (vis *visitor) visitChan(w io.Writer, v Value) { ) } - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, ")") } } @@ -104,7 +104,7 @@ func (vis *visitor) visitChan(w io.Writer, v Value) { func (vis *visitor) visitFunc(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { diff --git a/struct.go b/struct.go index 8892129..cf22b31 100644 --- a/struct.go +++ b/struct.go @@ -10,11 +10,14 @@ import ( // visitStruct formats values with a kind of reflect.Struct. func (vis *visitor) visitStruct(w io.Writer, v Value) { - if v.IsAmbiguousType && !v.IsAnonymousType() { + // even if the type is ambiguous, we only render it if it's not anonymous this + // is to avoid rendering the full type with field definitions. instead we mark + // each field's value as ambiguous and render their types inline. + if v.IsAmbiguousType() && !v.IsAnonymousType() { vis.write(w, v.TypeName()) } - if v.Type.NumField() == 0 { + if v.DynamicType.NumField() == 0 { vis.write(w, "{}") return } @@ -25,30 +28,35 @@ func (vis *visitor) visitStruct(w io.Writer, v Value) { } func (vis *visitor) visitStructFields(w io.Writer, v Value) { - alignment := longestFieldName(v.Type) - anon := v.IsAnonymousType() - var ambiguous bool + alignment := longestFieldName(v.DynamicType) - for i := 0; i < v.Type.NumField(); i++ { - f := v.Type.Field(i) + for i := 0; i < v.DynamicType.NumField(); i++ { + f := v.DynamicType.Field(i) fv := v.Value.Field(i) - if anon { - ambiguous = v.IsAmbiguousType - } else if f.Type.Kind() == reflect.Interface { - ambiguous = !fv.IsNil() - } else { - ambiguous = false - } - vis.write(w, f.Name) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-len(f.Name))) - vis.visit(w, fv, ambiguous) + vis.visit( + w, + Value{ + Value: fv, + DynamicType: fv.Type(), + StaticType: f.Type, + IsAmbiguousDynamicType: f.Type.Kind() == reflect.Interface, + IsAmbiguousStaticType: v.IsAmbiguousStaticType && v.IsAnonymousType(), + IsUnexported: v.IsUnexported || isUnexportedField(f), + }, + ) vis.write(w, "\n") } } +// isUnxportedField returns true if f is an unexported field. +func isUnexportedField(f reflect.StructField) bool { + return f.PkgPath != "" +} + // longestFieldName returns the length of the longest field name in a struct. func longestFieldName(rt reflect.Type) int { width := 0 diff --git a/value.go b/value.go index 87cbe4c..5a6d527 100644 --- a/value.go +++ b/value.go @@ -10,21 +10,35 @@ type Value struct { // Value is the value to be formatted. Value reflect.Value - // Type is the value's type. - Type reflect.Type - - // Kind is the value's kind. - Kind reflect.Kind - - // IsAmbiguousType is true if the type of v.Value is not clear from what - // has already been rendered. - IsAmbiguousType bool + // DynamicType is the value's type. + DynamicType reflect.Type + + // StaticType is the type of the "variable" that the value is stored in, which + // may not be the same as its dynamic type. + // + // For example, when formatting the values within a slice of interface{} + // containing integers, such as []interface{}{1, 2, 3}, the DynamicType will be + // "int", but the static type will be "interface{}". + StaticType reflect.Type + + // IsAmbiguousDynamicType is true if the value's dynamic type is not clear from + // the context of what has already been rendered. + IsAmbiguousDynamicType bool + + // IsAmbiguousStaticType is true if the value's static type is not clear from + // the context of what has already been rendered. + IsAmbiguousStaticType bool + + // IsUnexported is true if this value was obtained from an unexported struct + // field. If so, it is not possible to extract the underlying value. + IsUnexported bool } // TypeName returns the name of the value's type formatted for display. func (v *Value) TypeName() string { - n := v.Type.String() + n := v.DynamicType.String() n = strings.Replace(n, "interface {", "interface{", -1) + n = strings.Replace(n, "struct {", "struct{", -1) if strings.ContainsAny(n, "() \t\n") { return "(" + n + ")" @@ -35,5 +49,11 @@ func (v *Value) TypeName() string { // IsAnonymousType returns true if the value has an anonymous type. func (v *Value) IsAnonymousType() bool { - return v.Type.Name() == "" + return v.DynamicType.Name() == "" +} + +// IsAmbiguousType returns true if either the dynamic type or the static type is +// ambiguous. +func (v *Value) IsAmbiguousType() bool { + return v.IsAmbiguousDynamicType || v.IsAmbiguousStaticType } diff --git a/visitor.go b/visitor.go index 09f7a8c..8e096f7 100644 --- a/visitor.go +++ b/visitor.go @@ -9,6 +9,9 @@ import ( // visitor walks a Go value in order to render it. type visitor struct { + // filters is the set of filters to apply. + filters []Filter + // indent is the string used to indent nested values. indent []byte @@ -23,22 +26,21 @@ type visitor struct { bytes int } -func (vis *visitor) visit(w io.Writer, rv reflect.Value, ambiguous bool) (err error) { - defer iago.Recover(&err) - - if rv.Kind() == reflect.Invalid { +// TODO: don't return err or, let propagate and use iago.Recover() in Printer instead. +func (vis *visitor) visit(w io.Writer, v Value) { + if v.Value.Kind() == reflect.Invalid { vis.write(w, "interface{}(nil)") return } - v := Value{ - Value: rv, - Type: rv.Type(), - Kind: rv.Kind(), - IsAmbiguousType: ambiguous, + for _, f := range vis.filters { + if n := iago.Must(f(w, v)); n > 0 { + vis.bytes += n + return + } } - switch v.Kind { + switch v.DynamicType.Kind() { // type name is not rendered for these types, as the literals are unambiguous. case reflect.String: vis.writef(w, "%#v", v.Value.String()) @@ -102,7 +104,7 @@ func (vis *visitor) enter(w io.Writer, v Value) bool { marker = vis.recursionMarker } - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, "(") vis.write(w, marker) From 2092afa978599ac54827f7345e392d72bd958906 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 11:17:03 +1000 Subject: [PATCH 03/12] Unwrap interface values inside containers so that `visitor.visit()` sees their correct dynamic type. --- array.go | 7 ++++--- interface.go | 38 +++++++++++++------------------------- interface_test.go | 3 +++ map.go | 14 ++++++++++++++ printer.go | 6 +++++- struct.go | 10 +++++++++- 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/array.go b/array.go index 986dbdc..9b62db6 100644 --- a/array.go +++ b/array.go @@ -1,7 +1,6 @@ package dapper import ( - "fmt" "io" "reflect" @@ -31,8 +30,10 @@ func (vis *visitor) visitArrayValues(w io.Writer, v Value) { for i := 0; i < v.Value.Len(); i++ { elem := v.Value.Index(i) - if isInterface { - fmt.Print("") + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !elem.IsNil() { + elem = elem.Elem() } vis.visit( diff --git a/interface.go b/interface.go index 29db164..6f63943 100644 --- a/interface.go +++ b/interface.go @@ -6,31 +6,19 @@ import ( // visitInterface formats values with a kind of reflect.Interface. func (vis *visitor) visitInterface(w io.Writer, v Value) { - if v.Value.IsNil() { - // for a nil interface, we only want to render the type if the STATIC type is - // ambigious, since the only information we have available is the interface - // type itself, not the actual implementation's type. - if v.IsAmbiguousStaticType { - vis.write(w, v.TypeName()) - vis.write(w, "(nil)") - } else { - vis.write(w, "nil") - } - - return + if !v.Value.IsNil() { + // this should never happen, a more appropraite visit method should have been + // chosen based on the value's dynamic type. + panic("unexpectedly called visitInterface() with non-nil interface") } - elem := v.Value.Elem() - - vis.visit( - w, - Value{ - Value: elem, - DynamicType: elem.Type(), - StaticType: v.DynamicType, - IsAmbiguousDynamicType: true, - IsAmbiguousStaticType: v.IsAmbiguousStaticType, - IsUnexported: v.IsUnexported, - }, - ) + // for a nil interface, we only want to render the type if the STATIC type is + // ambigious, since the only information we have available is the interface + // type itself, not the actual implementation's type. + if v.IsAmbiguousStaticType { + vis.write(w, v.TypeName()) + vis.write(w, "(nil)") + } else { + vis.write(w, "nil") + } } diff --git a/interface_test.go b/interface_test.go index b813493..f5064a6 100644 --- a/interface_test.go +++ b/interface_test.go @@ -6,10 +6,13 @@ type interfaces struct { Iface interface{} } +type iface interface{} + func TestPrinter_Interface(t *testing.T) { // note that capturing a reflect.Value of a nil interface does NOT produces a // value with a "kind" of reflect.Invalid, NOT reflect.Interface. test(t, "nil interface", interface{}(nil), "interface{}(nil)") + test(t, "nil named interface", iface(nil), "interface{}(nil)") // interface information is shed when passed to Printer.Write(). test( t, diff --git a/map.go b/map.go index 5b3800f..25193cd 100644 --- a/map.go +++ b/map.go @@ -39,6 +39,13 @@ func (vis *visitor) visitMapElements(w io.Writer, v Value) { for _, mk := range keys { mv := v.Value.MapIndex(mk.Value) + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !mv.IsNil() { + mv = mv.Elem() + } + vis.write(w, mk.String) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-mk.Width)) @@ -75,6 +82,13 @@ func (vis *visitor) formatMapKeys(v Value) (keys []mapKey, alignment int) { alignToLastLine := false for i, mk := range v.Value.MapKeys() { + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !mk.IsNil() { + mk = mk.Elem() + } + vis.visit( &w, Value{ diff --git a/printer.go b/printer.go index 53058c9..ac0d43a 100644 --- a/printer.go +++ b/printer.go @@ -95,7 +95,11 @@ func (p *Printer) Format(v interface{}) string { } // defaultPrinter is a Printer instance with default settings. -var defaultPrinter Printer +var defaultPrinter = Printer{ + Filters: []Filter{ + ReflectTypeFilter, + }, +} // Write writes a pretty-printed representation of v to w using the default // printer settings. diff --git a/struct.go b/struct.go index cf22b31..4d53fbc 100644 --- a/struct.go +++ b/struct.go @@ -34,6 +34,14 @@ func (vis *visitor) visitStructFields(w io.Writer, v Value) { f := v.DynamicType.Field(i) fv := v.Value.Field(i) + isInterface := f.Type.Kind() == reflect.Interface + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !fv.IsNil() { + fv = fv.Elem() + } + vis.write(w, f.Name) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-len(f.Name))) @@ -43,7 +51,7 @@ func (vis *visitor) visitStructFields(w io.Writer, v Value) { Value: fv, DynamicType: fv.Type(), StaticType: f.Type, - IsAmbiguousDynamicType: f.Type.Kind() == reflect.Interface, + IsAmbiguousDynamicType: isInterface, IsAmbiguousStaticType: v.IsAmbiguousStaticType && v.IsAnonymousType(), IsUnexported: v.IsUnexported || isUnexportedField(f), }, From de451cec3eaffe39ac348ea6b2a5c8a8fbd5fee9 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 11:23:56 +1000 Subject: [PATCH 04/12] Use `Write()` instead of `Format()` in `test()` so that panics do not abort the test. --- visitor_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/visitor_test.go b/visitor_test.go index 3f1a018..ffd39dc 100644 --- a/visitor_test.go +++ b/visitor_test.go @@ -19,12 +19,18 @@ func test( t.Run( n, func(t *testing.T) { - p := Format(v) + var w strings.Builder + _, err := Write(&w, v) + + if err != nil { + t.Fatal(err) + } t.Log("expected:\n\n" + x + "\n") - if p != x { - t.Fatal("actual:\n\n" + p + "\n") + s := w.String() + if s != x { + t.Fatal("actual:\n\n" + s + "\n") } }, ) From 6f55dbb900e9512771f78b88d7970d13345ac996 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 11:34:04 +1000 Subject: [PATCH 05/12] Fix issue whereby pointer element types were not being rendered. --- ptr.go | 2 +- ptr_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ptr.go b/ptr.go index d74bc2c..ebd9de7 100644 --- a/ptr.go +++ b/ptr.go @@ -23,7 +23,7 @@ func (vis *visitor) visitPtr(w io.Writer, v Value) { Value: elem, DynamicType: elem.Type(), StaticType: elem.Type(), - IsAmbiguousDynamicType: false, + IsAmbiguousDynamicType: v.IsAmbiguousDynamicType, IsAmbiguousStaticType: v.IsAmbiguousStaticType, IsUnexported: v.IsUnexported, }, diff --git a/ptr_test.go b/ptr_test.go index d604c29..ff7171a 100644 --- a/ptr_test.go +++ b/ptr_test.go @@ -2,10 +2,36 @@ package dapper_test import "testing" +type ptr struct { + Value interface{} +} + func TestPrinter_Ptr(t *testing.T) { value := 100 test(t, "nil pointer", (*int)(nil), "*int(nil)") test(t, "non-nil pointer", &value, "*int(100)") + + test( + t, + "nil pointer inside interface includes element type", + ptr{ + (*int)(nil), + }, + "dapper_test.ptr{", + " Value: *int(nil)", + "}", + ) + + test( + t, + "non-nil pointer inside interface includes element type", + ptr{ + &value, + }, + "dapper_test.ptr{", + " Value: *int(100)", + "}", + ) } type recursive struct { From 3fb7df3064c076359727ec225adf7e69ad738c77 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 11:40:59 +1000 Subject: [PATCH 06/12] Forward the parent's static type when dealing with pointers. The static type that the value is contained within hasn't changed at this point. --- ptr.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptr.go b/ptr.go index ebd9de7..661d140 100644 --- a/ptr.go +++ b/ptr.go @@ -22,7 +22,7 @@ func (vis *visitor) visitPtr(w io.Writer, v Value) { Value{ Value: elem, DynamicType: elem.Type(), - StaticType: elem.Type(), + StaticType: v.StaticType, IsAmbiguousDynamicType: v.IsAmbiguousDynamicType, IsAmbiguousStaticType: v.IsAmbiguousStaticType, IsUnexported: v.IsUnexported, From fe74c7214e84a50aecd72f99be3e0646874c0236 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:18:12 +1000 Subject: [PATCH 07/12] Change `formatPointerHex()` to accept a concrete type. --- shallow.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/shallow.go b/shallow.go index d0449df..1317095 100644 --- a/shallow.go +++ b/shallow.go @@ -3,6 +3,7 @@ package dapper import ( "fmt" "io" + "strconv" ) // visitInt formats values with a kind of reflect.Int, and the related @@ -52,7 +53,7 @@ func (vis *visitor) visitComplex(w io.Writer, v Value) { // visitUintptr formats values with a kind of reflect.Uintptr. func (vis *visitor) visitUintptr(w io.Writer, v Value) { - s := formatPointerHex(v.Value.Uint(), false) + s := formatPointerHex(uintptr(v.Value.Uint()), false) if v.IsAmbiguousType() { vis.write(w, v.TypeName()) @@ -113,16 +114,14 @@ func (vis *visitor) visitFunc(w io.Writer, v Value) { } // formatPointerHex returns a minimal hexadecimal represenation of v. -func formatPointerHex(v interface{}, zeroIsNil bool) string { - s := fmt.Sprintf("%x", v) - - if s == "0" { +func formatPointerHex(v uintptr, zeroIsNil bool) string { + if v == 0 { if zeroIsNil { return "nil" } - return s + return "0" } - return "0x" + s + return "0x" + strconv.FormatUint(uint64(v), 16) } From 8594ecdc2999ff503a69d32ebc2b02d573ac2ad2 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:26:27 +1000 Subject: [PATCH 08/12] Add a filter that improves rendering of `reflect.Type` values. --- filter.go | 66 ++++++++++++++++++++++++++++++++ filter_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 filter_test.go diff --git a/filter.go b/filter.go index 51cac22..87307b0 100644 --- a/filter.go +++ b/filter.go @@ -2,6 +2,9 @@ package dapper import ( "io" + "reflect" + + "github.com/dogmatiq/iago" ) // Filter is a function that provides custom formatting logic for specific @@ -16,3 +19,66 @@ import ( // Particular attention should be paid to the v.IsUnexported field. If this flag // is true, many operations on v.Value are unavailable. type Filter func(w io.Writer, v Value) (int, error) + +// reflectTypeType is the reflect.Type for reflect.Type itself. +var reflectTypeType = reflect.TypeOf((*reflect.Type)(nil)).Elem() + +// ReflectTypeFilter is a filter that formats reflect.Type values. +func ReflectTypeFilter(w io.Writer, v Value) (n int, err error) { + defer iago.Recover(&err) + + if !v.DynamicType.Implements(reflectTypeType) { + return 0, nil + } + + if v.DynamicType.Kind() == reflect.Interface && v.Value.IsNil() { + return 0, nil + } + + var ambiguous bool + + if v.IsAmbiguousStaticType { + // always render the type if the static type is ambiguous + ambiguous = true + } else if v.IsAmbiguousDynamicType { + // only consider the dynamic type to be ambiguous if the static type isn't reflect.Type + // we're not really concerned with rendering the underlying implementation's type. + ambiguous = v.StaticType != reflectTypeType + } else { + ambiguous = false + } + + if ambiguous { + n += iago.MustWriteString(w, "reflect.Type(") + } + + if v.IsUnexported { + n += iago.MustWriteString(w, "") + } else { + t := v.Value.Interface().(reflect.Type) + + if s := t.PkgPath(); s != "" { + n += iago.MustWriteString(w, s) + n += iago.MustWriteString(w, ".") + } + + if s := t.Name(); s != "" { + n += iago.MustWriteString(w, s) + } else { + n += iago.MustWriteString(w, t.String()) + } + + } + + // always render the pointer value for the type, this way when the field is + // unexported we still get something we can compare to known types instead of a + // rendering of the reflect.rtype struct. + n += iago.MustWriteString(w, " ") + n += iago.MustWriteString(w, formatPointerHex(v.Value.Pointer(), false)) + + if ambiguous { + n += iago.MustWriteString(w, ")") + } + + return +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..a94b8d4 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,101 @@ +package dapper_test + +import ( + "fmt" + "reflect" + "testing" +) + +type reflectType struct { + Exported reflect.Type + unexported reflect.Type +} + +var ( + intType = reflect.TypeOf(0) + intTypePointer = formatReflectTypePointer(intType) + mapType = reflect.TypeOf(map[string]string{}) + mapTypePointer = formatReflectTypePointer(mapType) + namedType = reflect.TypeOf(named{}) + namedTypePointer = formatReflectTypePointer(namedType) +) + +func formatReflectTypePointer(t reflect.Type) string { + return fmt.Sprintf("0x%x", reflect.ValueOf(t).Pointer()) +} + +func TestPrinter_ReflectTypeFilter(t *testing.T) { + test( + t, + "built-in type", + intType, + "reflect.Type(int "+intTypePointer+")", + ) + + test( + t, + "built-in parameterized type", + mapType, + "reflect.Type(map[string]string "+mapTypePointer+")", + ) + + test( + t, + "named type", + reflect.TypeOf(named{}), + "reflect.Type(github.com/dogmatiq/dapper_test.named "+namedTypePointer+")", + ) + + typ := reflect.TypeOf(struct{ Int int }{}) + test( + t, + "anonymous struct", + typ, + "reflect.Type(struct { Int int } "+formatReflectTypePointer(typ)+")", + ) + + typ = reflect.TypeOf((*interface{ Int() int })(nil)).Elem() + test( + t, + "anonymous interface", + typ, + "reflect.Type(interface { Int() int } "+formatReflectTypePointer(typ)+")", + ) + + test( + t, + "includes type when in an anonymous struct", + struct { + Type reflect.Type + }{ + reflect.TypeOf(0), + }, + "{", + " Type: reflect.Type(int "+intTypePointer+")", + "}", + ) + + test( + t, + "does not include type if static type is also reflect.Type", + reflectType{ + Exported: reflect.TypeOf(0), + }, + "dapper_test.reflectType{", + " Exported: int "+intTypePointer, + " unexported: nil", + "}", + ) + + test( + t, + "still renders the pointer address when the value is unexported", + reflectType{ + unexported: reflect.TypeOf(0), + }, + "dapper_test.reflectType{", + " Exported: nil", + " unexported: "+intTypePointer, + "}", + ) +} From 3a8de4e4bdb0710ac50594d5878cc409b822f08d Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:30:07 +1000 Subject: [PATCH 09/12] Update the changelog. --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab34ac3..2cf2982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ The format is based on [Keep a Changelog], and this project adheres to [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html +## [Unreleased] + +### Added + +- Added support for filters, allowing custom rendering on a per-value basis +- Added a built-in filter for `reflect.Type` + +### Fixed + +- Fixed a bug whereby IO errors were not propagated to the caller + ## [0.2.0] - 2019-01-19 ### Added From 7afc68915298b4c4c8d8d895983a856ef1b84d69 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:32:43 +1000 Subject: [PATCH 10/12] Remove unreachable branch. --- filter.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/filter.go b/filter.go index 87307b0..1a83f72 100644 --- a/filter.go +++ b/filter.go @@ -35,7 +35,7 @@ func ReflectTypeFilter(w io.Writer, v Value) (n int, err error) { return 0, nil } - var ambiguous bool + ambiguous := false if v.IsAmbiguousStaticType { // always render the type if the static type is ambiguous @@ -44,8 +44,6 @@ func ReflectTypeFilter(w io.Writer, v Value) (n int, err error) { // only consider the dynamic type to be ambiguous if the static type isn't reflect.Type // we're not really concerned with rendering the underlying implementation's type. ambiguous = v.StaticType != reflectTypeType - } else { - ambiguous = false } if ambiguous { From ef11fa4c2bc8c84954472d3817471b226a34b75b Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:37:59 +1000 Subject: [PATCH 11/12] Add a few more very basic examples. --- printer_test.go | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/printer_test.go b/printer_test.go index be7529c..7d4f76b 100644 --- a/printer_test.go +++ b/printer_test.go @@ -1,10 +1,14 @@ package dapper_test import ( + "bytes" + "fmt" + "os" + . "github.com/dogmatiq/dapper" ) -func ExamplePrint() { +func ExamplePrinter() { type TreeNode struct { Name string Value interface{} @@ -27,7 +31,9 @@ func ExamplePrint() { }, } - Print(v) + p := Printer{} + s := p.Format(v) + fmt.Println(s) // output: dapper_test.TreeNode{ // Name: "root" @@ -46,3 +52,28 @@ func ExamplePrint() { // } // } } + +func ExamplePrint() { + Print(123) + + // output: int(123) +} + +func ExampleFormat() { + s := Format(123) + fmt.Println(s) + + // output: int(123) +} + +func ExampleWrite() { + w := &bytes.Buffer{} + + if _, err := Write(w, 123); err != nil { + panic(err) + } + + w.WriteTo(os.Stdout) + + // output: int(123) +} From 8c61ce226b3a97f11d48d417c9b9768e9d85b8b0 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Jan 2019 12:48:32 +1000 Subject: [PATCH 12/12] Improve the project description. --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9036b28..f5bd52d 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,17 @@ [![GoDoc](https://godoc.org/github.com/dogmatiq/dapper?status.svg)](https://godoc.org/github.com/dogmatiq/dapper) [![Go Report Card](https://goreportcard.com/badge/github.com/dogmatiq/dapper)](https://goreportcard.com/report/github.com/dogmatiq/dapper) -Dapper is a pretty-printer for Go values that aims to produce the shortest -possible output without ambiguity. Additionally, the output is deterministic, -which allows for the generation of human-readable diffs using standard tools. +Dapper is a pretty-printer for Go values. + +It is not intended to be used directly as a debugging tool, but as a library +for applications that need to describe Go values to humans, such as testing +frameworks. + +Some features include: + +- Concise formatting, without type ambiguity +- Deterministic output, useful for generating diffs using standard tools +- A filtering system for producing customized output on a per-value basis ## Example