diff --git a/dump.go b/dump.go index 8316c61..f97d4e3 100644 --- a/dump.go +++ b/dump.go @@ -36,6 +36,10 @@ type Options struct { StrictGo bool DumpFunc func(reflect.Value, io.Writer) bool + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + // DisablePointerReplacement, if true, disables the replacing of pointer data with variable names // when it's safe. This is useful for diffing two structures, where pointer variables would cause // false changes. However, circular graphs are still detected and elided to avoid infinite output. @@ -315,14 +319,14 @@ func (s *dumpState) descendIntoPossiblePointer(value reflect.Value, f func()) { } func (s *dumpState) dumpVal(value reflect.Value) { - if value.Kind() == reflect.Ptr && value.IsNil() { + v := deInterface(value) + kind := v.Kind() + + if kind == reflect.Ptr && v.IsNil() { s.write([]byte("nil")) return } - v := deInterface(value) - kind := v.Kind() - // Try to handle with dump func if s.config.DumpFunc != nil { buf := new(bytes.Buffer) @@ -332,15 +336,12 @@ func (s *dumpState) dumpVal(value reflect.Value) { } } - // Handle custom dumpers - dumperType := reflect.TypeOf((*Dumper)(nil)).Elem() - if v.Type().Implements(dumperType) { + if ok, fn := s.implMethods(v); ok { s.descendIntoPossiblePointer(v, func() { - // Run the custom dumper buffering the output - buf := new(bytes.Buffer) - dumpFunc := v.MethodByName("LitterDump") - dumpFunc.Call([]reflect.Value{reflect.ValueOf(buf)}) - s.dumpCustom(v, buf) + defer catchPanic(s.w, v) + b := new(bytes.Buffer) + fn(b) + s.dumpCustom(v, b) }) return } @@ -441,6 +442,40 @@ func (s *dumpState) pointerNameFor(v reflect.Value) (string, bool) { return "", false } +// implMethods checks whether the v implements the Dumper/error/fmt.Stringer interfaces or not. +// If implemented, the interface method can be called via the fn. +func (s *dumpState) implMethods(v reflect.Value) (implemented bool, fn func(w io.Writer)) { + if !v.IsValid() || !v.CanInterface() { + return + } + + switch iface := v.Interface().(type) { + case Dumper: + implemented = true + fn = func(w io.Writer) { iface.LitterDump(w) } + + case error: + if !s.config.DisableMethods { + implemented = true + fn = func(w io.Writer) { + w.Write([]byte{' '}) + w.Write([]byte(iface.Error())) + } + } + + case fmt.Stringer: + if !s.config.DisableMethods { + implemented = true + fn = func(w io.Writer) { + w.Write([]byte{' '}) + w.Write([]byte(iface.String())) + } + } + } + + return +} + // prepares a new state object for dumping the provided value func newDumpState(value interface{}, options *Options, writer io.Writer) *dumpState { result := &dumpState{ diff --git a/dump_test.go b/dump_test.go index 8700712..ebe3fad 100644 --- a/dump_test.go +++ b/dump_test.go @@ -50,6 +50,22 @@ func (csld CustomSingleLineDumper) LitterDump(w io.Writer) { _, _ = w.Write([]byte("")) } +type StructImplStringer struct{} + +func (s StructImplStringer) String() string { + return "String() called" +} + +type StructImplError struct{} + +func (s StructImplError) String() string { + return "String() called" +} + +func (s StructImplError) Error() string { + return "Error() called" +} + func TestSdump_primitives(t *testing.T) { runTests(t, "primitives", []interface{}{ false, @@ -230,6 +246,19 @@ func TestSdump_maps(t *testing.T) { }) } +func TestSdump_methods(t *testing.T) { + runTestWithCfg(t, "config_EnableMethods", &litter.Options{ + DisableMethods: false, + }, []interface{}{ + StructImplStringer{}, + &StructImplStringer{}, + (*StructImplStringer)(nil), + StructImplError{}, + &StructImplError{}, + (*StructImplError)(nil), + }) +} + var standardCfg = litter.Options{} func runTestWithCfg(t *testing.T, name string, cfg *litter.Options, cases ...interface{}) { diff --git a/testdata/config_EnableMethods.dump b/testdata/config_EnableMethods.dump new file mode 100644 index 0000000..a66cea2 --- /dev/null +++ b/testdata/config_EnableMethods.dump @@ -0,0 +1,8 @@ +[]interface {}{ + litter_test.StructImplStringer String() called, + *litter_test.StructImplStringer String() called, + nil, + litter_test.StructImplError Error() called, + *litter_test.StructImplError Error() called, + nil, +} \ No newline at end of file diff --git a/util.go b/util.go index 58be475..0230de1 100644 --- a/util.go +++ b/util.go @@ -1,9 +1,16 @@ package litter import ( + "fmt" + "io" "reflect" ) +var ( + percentBangString = []byte("%!(PANIC=") + closeParent = []byte{')'} +) + // deInterface returns values inside of non-nil interfaces when possible. // This is useful for data types like structs, arrays, slices, and maps which // can contain varying types packed inside an interface. @@ -26,3 +33,16 @@ func isZeroValue(v reflect.Value) bool { return (isPointerValue(v) && v.IsNil()) || (v.IsValid() && v.CanInterface() && reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())) } + +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + if v.Kind() == reflect.Ptr && v.IsNil() { + printNil(w) + return + } + + w.Write(percentBangString) + fmt.Fprintf(w, "%v", err) + w.Write(closeParent) + } +}