From 025464a32940e585c83b76413dc5f18fd4459197 Mon Sep 17 00:00:00 2001 From: Inhere Date: Sat, 10 Dec 2022 15:41:56 +0800 Subject: [PATCH] :sparkles: feat(dump): support dump []byte as string and more new options New options: - SkipNilField TODO - SkipPrivate - BytesAsString close #74 --- dump/dump.go | 33 +++++++++---- dump/dump_test.go | 26 ++++++++++ dump/dumper.go | 97 ++++++++++++------------------------ dump/dumper_test.go | 14 +++--- dump/options.go | 115 +++++++++++++++++++++++++++++++++++++++++++ dump/options_test.go | 73 +++++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 81 deletions(-) create mode 100644 dump/options.go create mode 100644 dump/options_test.go diff --git a/dump/dump.go b/dump/dump.go index 07b3d8bd0..a7f5a8bfb 100644 --- a/dump/dump.go +++ b/dump/dump.go @@ -18,6 +18,9 @@ const ( Fline ) +const defaultSkip = 3 +const defaultSkip2 = 2 + var ( // valid flag for print caller info callerFlags = []int{Ffunc, Ffile, Ffname, Fline} @@ -34,7 +37,7 @@ var ( } // std dumper - std = NewDumper(os.Stdout, 3) + std = NewDumper(os.Stdout, defaultSkip) // no location dumper. std2 = NewWithOptions(func(opts *Options) { opts.Output = os.Stdout @@ -62,19 +65,13 @@ func (ct Theme) wrap(key string, s string) string { } // Std dumper -func Std() *Dumper { - return std -} +func Std() *Dumper { return std } // Reset std dumper -func Reset() { - std = NewDumper(os.Stdout, 3) -} +func Reset() { std = NewDumper(os.Stdout, 3) } // Config std dumper -func Config(fn func(opts *Options)) { - std.WithOptions(fn) -} +func Config(fns ...OptionFunc) { std.WithOptions(fns...) } // V like fmt.Println, but the output is clearer and more beautiful func V(vs ...any) { @@ -101,6 +98,17 @@ func Fprint(w io.Writer, vs ...any) { std.Fprint(w, vs...) } +// Std2 dumper +func Std2() *Dumper { return std2 } + +// Reset2 reset std2 dumper +func Reset2() { + std2 = NewWithOptions(func(opts *Options) { + opts.Output = os.Stdout + opts.ShowFlag = Fnopos + }) +} + // Format like fmt.Println, but the output is clearer and more beautiful func Format(vs ...any) string { w := &bytes.Buffer{} @@ -118,3 +126,8 @@ func NoLoc(vs ...any) { func Clear(vs ...any) { std2.Println(vs...) } + +// is unexported field name on struct +func isUnexported(fieldName string) bool { + return fieldName[0] < 'A' || fieldName[0] > 'Z' +} diff --git a/dump/dump_test.go b/dump/dump_test.go index b2d7be690..5da46227f 100644 --- a/dump/dump_test.go +++ b/dump/dump_test.go @@ -67,6 +67,32 @@ func TestStd(t *testing.T) { assert.Eq(t, Std().IndentLen, 2) } +func TestStd2(t *testing.T) { + assert.Eq(t, Std2().NoColor, false) + assert.Eq(t, Std2().IndentLen, 2) + assert.Eq(t, Fnopos, Std2().ShowFlag) + + buf := newBuffer() + Std2().WithOptions(func(opt *Options) { + opt.Output = buf + opt.NoColor = true + }) + defer Reset2() + + NoLoc(123, "abcd") + + str := buf.String() + fmt.Print(str) + assert.StrContains(t, str, "int(123)") + assert.NotContains(t, str, "PRINT") + + buf.Reset() + Clear(123, "abcd") + str = buf.String() + assert.StrContains(t, str, "int(123)") + assert.NotContains(t, str, "PRINT") +} + func TestConfig(t *testing.T) { is := assert.New(t) buf := new(bytes.Buffer) diff --git a/dump/dumper.go b/dump/dumper.go index 125533f4b..a8e22b601 100644 --- a/dump/dumper.go +++ b/dump/dumper.go @@ -15,30 +15,6 @@ import ( "github.com/gookit/goutil/strutil" ) -// Options for dump vars -type Options struct { - // Output the output writer - Output io.Writer - // NoType dont show data type TODO - NoType bool - // NoColor don't with color - NoColor bool - // IndentLen width. default is 2 - IndentLen int - // IndentChar default is one space - IndentChar byte - // MaxDepth for nested print - MaxDepth int - // ShowFlag for display caller position - ShowFlag int - // MoreLenNL array/slice elements length > MoreLenNL, will wrap new line - // MoreLenNL int - // CallerSkip skip for call runtime.Caller() - CallerSkip int - // ColorTheme for print result. - ColorTheme Theme -} - // printValue must keep track of already-printed pointer values to avoid // infinite recursion. refer the pkg: github.com/kr/pretty type visit struct { @@ -73,30 +49,8 @@ func NewDumper(out io.Writer, skip int) *Dumper { } // NewWithOptions create -func NewWithOptions(fn func(opts *Options)) *Dumper { - d := NewDumper(os.Stdout, 3) - fn(d.Options) - return d -} - -// NewDefaultOptions create. -func NewDefaultOptions(out io.Writer, skip int) *Options { - if out == nil { - out = os.Stdout - } - - return &Options{ - Output: out, - // --- - MaxDepth: 5, - ShowFlag: Ffunc | Ffname | Fline, - // MoreLenNL: 8, - // --- - IndentLen: 2, - IndentChar: ' ', - CallerSkip: skip, - ColorTheme: defaultTheme, - } +func NewWithOptions(fns ...OptionFunc) *Dumper { + return NewDumper(os.Stdout, defaultSkip).WithOptions(fns...) } // WithSkip for dumper @@ -112,8 +66,10 @@ func (d *Dumper) WithoutColor() *Dumper { } // WithOptions for dumper -func (d *Dumper) WithOptions(fn func(opts *Options)) *Dumper { - fn(d.Options) +func (d *Dumper) WithOptions(fns ...OptionFunc) *Dumper { + for _, fn := range fns { + fn(d.Options) + } return d } @@ -121,23 +77,17 @@ func (d *Dumper) WithOptions(fn func(opts *Options)) *Dumper { func (d *Dumper) ResetOptions() { d.curDepth = 0 d.visited = make(map[visit]int) - d.Options = NewDefaultOptions(os.Stdout, 2) + d.Options = NewDefaultOptions(os.Stdout, d.CallerSkip) } // Dump vars -func (d *Dumper) Dump(vs ...any) { - d.dump(vs...) -} +func (d *Dumper) Dump(vs ...any) { d.dump(vs...) } // Print vars. alias of Dump() -func (d *Dumper) Print(vs ...any) { - d.dump(vs...) -} +func (d *Dumper) Print(vs ...any) { d.dump(vs...) } // Println vars. alias of Dump() -func (d *Dumper) Println(vs ...any) { - d.dump(vs...) -} +func (d *Dumper) Println(vs ...any) { d.dump(vs...) } // Fprint print vars to io.Writer func (d *Dumper) Fprint(w io.Writer, vs ...any) { @@ -228,12 +178,20 @@ func (d *Dumper) printOne(v any) { return } + if bts, ok := v.([]byte); ok && d.BytesAsString { + strVal := d.ColorTheme.string(string(bts)) + lenTip := d.ColorTheme.lenTip("#len=" + strconv.Itoa(len(bts)) + ",cap=" + strconv.Itoa(cap(bts))) + d.printf("[]byte(\"%s\"), %s\n", strVal, lenTip) + return + } + + // print reflect value rv := reflect.ValueOf(v) d.printRValue(rv.Type(), rv) } +// print reflect value func (d *Dumper) printRValue(t reflect.Type, v reflect.Value) { - // var isPtr bool // if is a ptr, get real type and value if t.Kind() == reflect.Ptr { if v.IsNil() { @@ -241,10 +199,8 @@ func (d *Dumper) printRValue(t reflect.Type, v reflect.Value) { return } - v = v.Elem() - t = t.Elem() - // add "*" prefix - d.indentPrint("&") + v, t = v.Elem(), t.Elem() + d.indentPrint("&") // add prefix } if !v.IsValid() { @@ -322,6 +278,14 @@ func (d *Dumper) printRValue(t reflect.Type, v reflect.Value) { d.advance(1) fName := t.Field(i).Name + if d.SkipPrivate && isUnexported(fName) { + continue + } + + // if d.SkipNilField { // TODO + // } + + // print field name d.indentPrint(d.ColorTheme.field(fName), ": ") d.msValue = true @@ -341,6 +305,9 @@ func (d *Dumper) printRValue(t reflect.Type, v reflect.Value) { mv := v.MapIndex(key) d.advance(1) + // if d.SkipNilField { // TODO + // } + // print key name if !key.CanInterface() { // d.printf("%s: ", key.String()) diff --git a/dump/dumper_test.go b/dump/dumper_test.go index 25719c102..58f664c30 100644 --- a/dump/dumper_test.go +++ b/dump/dumper_test.go @@ -16,6 +16,8 @@ import ( // return NewDumper(buf, 2) // } +const skipInTest = 2 + var ( ints1 = []int{1, 2, 3, 4} ints2 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} @@ -27,11 +29,11 @@ var ( ) func newStd() *Dumper { - return NewDumper(os.Stdout, 2) + return NewDumper(os.Stdout, skipInTest) } func TestNewDefaultOptions(t *testing.T) { - opts := NewDefaultOptions(nil, 2) + opts := NewDefaultOptions(nil, skipInTest) assert.Eq(t, "text value", opts.ColorTheme.value("text value")) } @@ -53,7 +55,7 @@ func TestDumper_Fprint(t *testing.T) { func TestDump_Basic(t *testing.T) { buffer := new(bytes.Buffer) - dumper := NewDumper(buffer, 2) + dumper := NewDumper(buffer, skipInTest) dumper.Dump( nil, @@ -93,7 +95,7 @@ func TestDump_Basic(t *testing.T) { func TestDump_Ints(t *testing.T) { buffer := new(bytes.Buffer) - dumper := NewDumper(buffer, 2) + dumper := NewDumper(buffer, skipInTest) dumper.WithoutColor() // assert.Equal(t, 8, dumper.MoreLenNL) @@ -119,7 +121,7 @@ func TestDump_Ints(t *testing.T) { func TestDump_Ptr(t *testing.T) { buffer := new(bytes.Buffer) - dumper := NewDumper(buffer, 2) + dumper := NewDumper(buffer, skipInTest) // dumper.WithoutColor() var s string @@ -189,7 +191,7 @@ func TestDumper_AccessCantExportedField(t *testing.T) { // code from https://stackoverflow.com/questions/42664837/how-to-access-unexported-struct-fields-in-golang func TestDumper_AccessCantExportedField1(t *testing.T) { - // init an nested struct + // init a nested struct s1 := st1{st0{2}, 23, "inhere"} myS1 := struct { // cannotExport any // ok diff --git a/dump/options.go b/dump/options.go new file mode 100644 index 000000000..39a172107 --- /dev/null +++ b/dump/options.go @@ -0,0 +1,115 @@ +package dump + +import ( + "io" + "os" +) + +// Options for dumper +type Options struct { + // Output the output writer + Output io.Writer + // NoType don't show data type TODO + NoType bool + // NoColor don't with color + NoColor bool + // IndentLen width. default is 2 + IndentLen int + // IndentChar default is one space + IndentChar byte + // MaxDepth for nested print + MaxDepth int + // ShowFlag for display caller position + ShowFlag int + // CallerSkip skip for call runtime.Caller() + CallerSkip int + // ColorTheme for print result. + ColorTheme Theme + // SkipNilField value dump on map, struct. + SkipNilField bool + // SkipPrivate field dump on struct. + SkipPrivate bool + // BytesAsString dump handle. + BytesAsString bool + // MoreLenNL array/slice elements length > MoreLenNL, will wrap new line + // MoreLenNL int +} + +// OptionFunc type +type OptionFunc func(opts *Options) + +// NewDefaultOptions create. +func NewDefaultOptions(out io.Writer, skip int) *Options { + if out == nil { + out = os.Stdout + } + + return &Options{ + Output: out, + // --- + MaxDepth: 5, + ShowFlag: Ffunc | Ffname | Fline, + // MoreLenNL: 8, + // --- + IndentLen: 2, + IndentChar: ' ', + CallerSkip: skip, + ColorTheme: defaultTheme, + } +} + +// SkipNilField setting. +func SkipNilField() OptionFunc { + return func(opt *Options) { + opt.SkipNilField = true + } +} + +// SkipPrivate field dump on struct. +func SkipPrivate() OptionFunc { + return func(opt *Options) { + opt.SkipPrivate = true + } +} + +// BytesAsString setting. +func BytesAsString() OptionFunc { + return func(opt *Options) { + opt.BytesAsString = true + } +} + +// WithCallerSkip on print caller position information. +func WithCallerSkip(skip int) OptionFunc { + return func(opt *Options) { + opt.CallerSkip = skip + } +} + +// WithoutPosition dont print call dump position information. +func WithoutPosition() OptionFunc { + return func(opt *Options) { + opt.ShowFlag = Fnopos + } +} + +// WithoutOutput setting. +func WithoutOutput(out io.Writer) OptionFunc { + return func(opt *Options) { + opt.Output = out + } +} + +// WithoutColor setting. +func WithoutColor() OptionFunc { + return func(opt *Options) { + opt.NoColor = true + } +} + +// WithoutType setting. +func WithoutType() OptionFunc { + return func(opt *Options) { + opt.NoType = true + } +} diff --git a/dump/options_test.go b/dump/options_test.go new file mode 100644 index 000000000..4601d9ea4 --- /dev/null +++ b/dump/options_test.go @@ -0,0 +1,73 @@ +package dump + +import ( + "fmt" + "testing" + + "github.com/gookit/goutil/testutil/assert" +) + +func TestSkipPrivate(t *testing.T) { + buf := newBuffer() + dumper := newStd().WithOptions(WithoutOutput(buf), WithoutPosition(), WithoutColor()) + + dumper.Println(user) + str := buf.String() + fmt.Print("Default: \n", str) + assert.StrContains(t, str, "id: string(\"ab12345\")") + + buf.Reset() + dumper.WithOptions(SkipNilField(), SkipPrivate()) + dumper.Println(user) + + str = buf.String() + fmt.Print("SkipPrivate: \n", str) + assert.NotContains(t, str, "id: string(\"ab12345\")") +} + +func TestWithoutColor(t *testing.T) { + buf := newBuffer() + dumper := newStd().WithOptions(WithoutOutput(buf), WithoutPosition(), WithCallerSkip(2)) + + dumper.Println("a string") + assert.Equal(t, "string(\"\x1b[0;32ma string\x1b[0m\"), \x1b[0;90m#len=8\x1b[0m\n", buf.String()) + + buf.Reset() + dumper.WithOptions(SkipNilField(), WithoutColor()) + dumper.Println("a string") + assert.Equal(t, "string(\"a string\"), #len=8\n", buf.String()) +} + +// see https://github.com/gookit/goutil/issues/74 +func TestBytesAsString(t *testing.T) { + buf := newBuffer() + dumper := newStd().WithOptions(WithoutOutput(buf), WithoutColor()) + + bts := []byte("hello") + dumper.Println(bts) + str := buf.String() + fmt.Print(str) + assert.StrContains(t, str, "[]uint8 [ #len=5,cap=5") + assert.StrContains(t, str, "uint8(104),") + /* Output: + PRINT AT github.com/gookit/goutil/dump.TestBytesAsString(options_test.go:28) + []uint8 [ #len=5,cap=5 + uint8(104), + uint8(101), + uint8(108), + uint8(108), + uint8(111), + ], + */ + + buf.Reset() + dumper.WithOptions(WithoutType(), BytesAsString()) + dumper.Print(bts) + str = buf.String() + fmt.Print("BytesAsString: \n", str) + assert.StrContains(t, str, "[]byte(\"hello\"), #len=5,cap=5") + /*Output: + PRINT AT github.com/gookit/goutil/dump.TestBytesAsString(options_test.go:49) + []byte("hello"), #len=5,cap=5 + */ +}