pax_global_header00006660000000000000000000000064152067536640014530gustar00rootroot0000000000000052 comment=60aeed6ac0becb64e7c8e5c09049dd36ef35e387 golang-github-ysmood-gop-0.4.1/000077500000000000000000000000001520675366400163545ustar00rootroot00000000000000golang-github-ysmood-gop-0.4.1/.cspell.json000066400000000000000000000001341520675366400206050ustar00rootroot00000000000000{ "words": [ "tmpl", "tput", "Unsets", "ysmood" ] } golang-github-ysmood-gop-0.4.1/.github/000077500000000000000000000000001520675366400177145ustar00rootroot00000000000000golang-github-ysmood-gop-0.4.1/.github/workflows/000077500000000000000000000000001520675366400217515ustar00rootroot00000000000000golang-github-ysmood-gop-0.4.1/.github/workflows/test.yml000066400000000000000000000007071520675366400234570ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v2 with: go-version: 1.18 - uses: actions/checkout@v2 - name: lint run: go run github.com/ysmood/golangci-lint@latest - name: test env: TERM: xterm-256color run: | go test -race -coverprofile=coverage.out ./... go run github.com/ysmood/got/cmd/check-cov@latest golang-github-ysmood-gop-0.4.1/.gitignore000066400000000000000000000000311520675366400203360ustar00rootroot00000000000000*.out *.test tmp/ .claudegolang-github-ysmood-gop-0.4.1/.golangci.yml000066400000000000000000000003151520675366400207370ustar00rootroot00000000000000run: skip-dirs-use-default: false linters: enable: - gofmt - gocyclo - misspell - bodyclose disable: - govet gocyclo: min-complexity: 15 issues: exclude-use-default: false golang-github-ysmood-gop-0.4.1/LICENSE000066400000000000000000000020521520675366400173600ustar00rootroot00000000000000MIT License Copyright (c) 2023 Yad Smood Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-ysmood-gop-0.4.1/README.md000066400000000000000000000034731520675366400176420ustar00rootroot00000000000000# Go Pretty Print Value Make a random Go value human readable. The output format uses valid golang syntax, so you don't have to learn any new knowledge to understand the output. ## Features - Uses valid golang syntax to print the data - Make rune, []byte, time, etc. data human readable - Color output with customizable theme - Stable map output with sorted by keys - Auto split multiline large string block - Prints the path of circular reference - Auto format inline json string - Low-level API to extend the lib ## Usage Usually, you only need to use `gop.P` function: ```go package main import ( "time" "github.com/ysmood/got/lib/gop" ) func main() { val := map[string]interface{}{ "bool": true, "number": 1 + 1i, "bytes": []byte{97, 98, 99}, "lines": "multiline string\nline two", "slice": []interface{}{1, 2}, "time": time.Now(), "chan": make(chan int, 1), "struct": struct{ test int32 }{ test: 13, }, "json": `{"a" : 1}`, "func": func(int) int { return 0 }, } val["slice"].([]interface{})[1] = val["slice"] _ = gop.P(val) } ``` The output will be something like: ```go // 2023-10-07T18:19:57.517309+08:00 example/main.go:27 (main.main) map[string]interface {}{ "bool": true, "bytes": []byte("abc"), "chan": make(chan int, 1)/* 0x1400008c070 */, "func": (func(int) int)(nil)/* 0x1025a5460 */, "json": gop.JSONStr(map[string]interface {}{ "a": 1.0, }, `{"a" : 1}`), "lines": `multiline string line two`, "number": 1+1i, "slice": []interface {}{ 1, gop.Circular("slice").([]interface {}), }, "struct": struct { test int32 }{ test: int32(13), }, "time": gop.Time("2023-10-07T18:19:57.516984+08:00", 3081584), } ``` golang-github-ysmood-gop-0.4.1/bench_test.go000066400000000000000000000036651520675366400210330ustar00rootroot00000000000000package gop_test import ( "testing" "time" "unsafe" "github.com/ysmood/gop" ) func benchValue() interface{} { ref := "test" timeStamp, _ := time.Parse(time.RFC3339Nano, "2021-08-28T08:36:36.807908+08:00") fn := func(string) int { return 10 } ch1 := make(chan int) ch2 := make(chan string, 3) ch3 := make(chan struct{}) return []interface{}{ nil, []int{}, []interface{}{true, false, uintptr(0x17), float32(100.121111133)}, true, 10, int8(2), int32(100), float64(100.121111133), complex64(1 + 2i), complex128(1 + 2i), [3]int{1, 2}, ch1, ch2, ch3, fn, map[interface{}]interface{}{ `"test"`: 10, "a": 1, &ref: 1, }, unsafe.Pointer(&ref), struct { Int int str string M map[int]int }{10, "ok", map[int]int{1: 0x20}}, []byte("aa\xe2"), []byte("bytes\n\tbytes"), []byte("long long long long string"), byte('a'), byte(1), '天', "long long long long string", "\ntest", "\t\n`", &ref, (*struct{ Int int })(nil), &struct{ Int int }{}, &map[int]int{1: 2, 3: 4}, &[]int{1, 2}, &[2]int{1, 2}, &[]byte{1, 2}, timeStamp, time.Hour, `{"a": 1}`, []byte(`{"a": 1}`), } } func BenchmarkTokenize(b *testing.B) { v := benchValue() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = gop.Tokenize(v) } } func BenchmarkFormatDefault(b *testing.B) { v := benchValue() ts := gop.Tokenize(v) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = gop.Format(ts, gop.ThemeDefault) } } func BenchmarkFormatNone(b *testing.B) { v := benchValue() ts := gop.Tokenize(v) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = gop.Format(ts, gop.ThemeNone) } } func BenchmarkF(b *testing.B) { v := benchValue() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = gop.F(v) } } func BenchmarkPlain(b *testing.B) { v := benchValue() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = gop.Plain(v) } } golang-github-ysmood-gop-0.4.1/convertors.go000066400000000000000000000032471520675366400211150ustar00rootroot00000000000000package gop import ( "encoding/base64" "fmt" "reflect" "time" ) // SymbolPtr for Ptr const SymbolPtr = "gop.Ptr" // Ptr returns a pointer to v func Ptr(v interface{}) interface{} { val := reflect.ValueOf(v) ptr := reflect.New(val.Type()) ptr.Elem().Set(val) return ptr.Interface() } // SymbolCircular for Circular const SymbolCircular = "gop.Circular" // Circular reference of the path from the root func Circular(path ...interface{}) interface{} { return nil } // SymbolBase64 for Base64 const SymbolBase64 = "gop.Base64" // Base64 returns the []byte that s represents func Base64(s string) []byte { b, _ := base64.StdEncoding.DecodeString(s) return b } // SymbolRune for Rune const SymbolRune = "gop.Rune" func Rune(i int32, r rune) rune { return r } // SymbolTime for Time const SymbolTime = "gop.Time" // Time from parsing s func Time(s string, monotonic int) time.Time { t, _ := time.Parse(time.RFC3339Nano, s) return t } // SymbolDuration for Duration const SymbolDuration = "gop.Duration" // Duration from parsing s func Duration(s string) time.Duration { d, _ := time.ParseDuration(s) return d } // SymbolJSONStr for JSONStr const SymbolJSONStr = "gop.JSONStr" // JSONStr returns the raw func JSONStr(v interface{}, raw string) string { return raw } // SymbolJSONBytes for JSONBytes const SymbolJSONBytes = "gop.JSONBytes" // JSONBytes returns the raw as []byte func JSONBytes(v interface{}, raw string) []byte { return []byte(raw) } const SymbolGopError = "gop.GopError" // GopError returns an error with the given message, it represents an error occurred during tokenization or formatting. func GopError(msg string) error { return fmt.Errorf("%s", msg) } golang-github-ysmood-gop-0.4.1/example/000077500000000000000000000000001520675366400200075ustar00rootroot00000000000000golang-github-ysmood-gop-0.4.1/example/main.go000066400000000000000000000007771520675366400212750ustar00rootroot00000000000000// Package main ... package main import ( "time" "github.com/ysmood/gop" ) func main() { val := map[string]interface{}{ "bool": true, "number": 1 + 1i, "bytes": []byte{97, 98, 99}, "lines": "multiline string\nline two", "slice": []interface{}{1, 2}, "time": time.Now(), "chan": make(chan int, 1), "struct": struct{ test int32 }{ test: 13, }, "json": `{"a" : 1}`, "func": func(int) int { return 0 }, } val["slice"].([]interface{})[1] = val["slice"] _ = gop.P(val) } golang-github-ysmood-gop-0.4.1/fixtures/000077500000000000000000000000001520675366400202255ustar00rootroot00000000000000golang-github-ysmood-gop-0.4.1/fixtures/compile_check.go.tmpl000066400000000000000000000001251520675366400243120ustar00rootroot00000000000000package main import ( "unsafe" "github.com/ysmood/gop" ) func main() { _ = %s }golang-github-ysmood-gop-0.4.1/fixtures/expected.tmpl000066400000000000000000000030151520675366400227230ustar00rootroot00000000000000[]interface {}{ nil, []int{}, []interface {}{ true, false, uintptr(23), float32(100.12111), }, true, 10, int8(2), gop.Rune(100, 'd'), 100.121111133, complex64(1+2i), 1+2i, [3]int{ 1, 2, 0, }, make(chan int)/* {{.ch1}} */, make(chan string, 3)/* {{.ch2}} */, make(chan struct {})/* {{.ch3}} */, (func(string) int)(nil)/* {{.fn}} */, map[interface {}]interface {}{ `"test"`: 10, "a": 1, (interface {})(nil)/* {{.ref}} */: 1, }, unsafe.Pointer(uintptr({{.ptr}})), struct { Int int; str string; M map[int]int }{ Int: 10, str: "ok", M: map[int]int{ 1: 32, }, }, gop.Base64("YWHi"), []byte(`bytes bytes`), []byte("long long long long string"), byte('a'), byte(0x1), gop.Rune(22825, '天'), "long long long long string", ` test`, "" + " \n" + "`", gop.Ptr("test").(*string), (*struct { Int int })(nil), &struct { Int int }{ Int: 0, }, &map[int]int{ 1: 2, 3: 4, }, &[]int{ 1, 2, }, &[2]int{ 1, 2, }, gop.Ptr([]byte("\x01\x02")).(*[]uint8), gop.Time("2021-08-28T08:36:36.807908+08:00", 63765707796), gop.Duration("1h0m0s"), gop.JSONStr(map[string]interface {}{ "a": 1.0, }, `{"a": 1}`), gop.JSONBytes(map[string]interface {}{ "a": 1.0, }, `{"a": 1}`), }golang-github-ysmood-gop-0.4.1/fixtures/expected_with_color.tmpl000066400000000000000000000046551520675366400251670ustar00rootroot00000000000000<36>[]interface {}<39>{ <31>nil<39>, <36>[]int<39>{}, <36>[]interface {}<39>{ <34>true<39>, <34>false<39>, <36>uintptr<39>(<32>23<39>), <36>float32<39>(<32>100.12111<39>), }, <34>true<39>, <32>10<39>, <36>int8<39>(<32>2<39>), <36>gop.Rune<39>(<32>100<39>, <33>'d'<39>), <32>100.121111133<39>, <36>complex64<39>(<32>1+2i<39>), <32>1+2i<39>, <36>[3]int<39>{ <32>1<39>, <32>2<39>, <32>0<39>, }, <35>make<39>(<34>chan<39> <36>int<39>)<37>/* {{.ch1}} */<39>, <35>make<39>(<34>chan<39> <36>string<39>, <32>3<39>)<37>/* {{.ch2}} */<39>, <35>make<39>(<34>chan<39> <36>struct {}<39>)<37>/* {{.ch3}} */<39>, (<36>func(string) int<39>)(<31>nil<39>)<37>/* {{.fn}} */<39>, <36>map[interface {}]interface {}<39>{ <33>`"test"`<39>: <32>10<39>, <33>"a"<39>: <32>1<39>, (<36>interface {}<39>)(<31>nil<39>)<37>/* {{.ref}} */<39>: <32>1<39>, }, <36>unsafe.Pointer<39>(<36>uintptr<39>(<32>{{.ptr}}<39>)), <36>struct { Int int; str string; M map[int]int }<39>{ Int: <32>10<39>, str: <33>"ok"<39>, M: <36>map[int]int<39>{ <32>1<39>: <32>32<39>, }, }, <35>gop.Base64<39>(<33>"YWHi"<39>), <36>[]byte<39>(<33>`bytes<39> <33> bytes`<39>), <36>[]byte<39>(<33>"long long long long string"<39>), <36>byte<39>(<33>'a'<39>), <36>byte<39>(<33>0x1<39>), <36>gop.Rune<39>(<32>22825<39>, <33>'天'<39>), <33>"long long long long string"<39>, <33>`<39> <33>test`<39>, <33>"" +<39> <33> " \n" +<39> <33> "`"<39>, <35>gop.Ptr<39>(<33>"test"<39>).(<36>*string<39>), (<36>*struct { Int int }<39>)(<31>nil<39>), &<36>struct { Int int }<39>{ Int: <32>0<39>, }, &<36>map[int]int<39>{ <32>1<39>: <32>2<39>, <32>3<39>: <32>4<39>, }, &<36>[]int<39>{ <32>1<39>, <32>2<39>, }, &<36>[2]int<39>{ <32>1<39>, <32>2<39>, }, <35>gop.Ptr<39>(<36>[]byte<39>(<33>"\x01\x02"<39>)).(<36>*[]uint8<39>), <35>gop.Time<39>(<33>"2021-08-28T08:36:36.807908+08:00"<39>, <32>63765707796<39>), <36>gop.Duration<39>(<33>"1h0m0s"<39>), <35>gop.JSONStr<39>(<36>map[string]interface {}<39>{ <33>"a"<39>: <32>1.0<39>, }, <33>`{"a": 1}`<39>), <35>gop.JSONBytes<39>(<36>map[string]interface {}<39>{ <33>"a"<39>: <32>1.0<39>, }, <33>`{"a": 1}`<39>), }golang-github-ysmood-gop-0.4.1/format.go000066400000000000000000000133141520675366400201750ustar00rootroot00000000000000// Package gop ... package gop import ( "fmt" "io" "os" "path/filepath" "runtime" "strings" "time" ) // Stdout is the default stdout for gop.P . var Stdout io.Writer = os.Stdout const indentUnit = " " // Theme to color values type Theme func(t Type) []Style // ThemeDefault colors for Sprint var ThemeDefault = func(t Type) []Style { switch t { case TypeName: return []Style{Cyan} case Bool, Chan: return []Style{Blue} case RuneInt32, Byte, String: return []Style{Yellow} case Number: return []Style{Green} case Func: return []Style{Magenta} case Comment: return []Style{White} case Nil: return []Style{Red} case Error: return []Style{Underline, Red} default: return []Style{None} } } // ThemeNone colors for Sprint var ThemeNone = func(t Type) []Style { return []Style{None} } // F is a shortcut for Format with color func F(v interface{}) string { return Format(Tokenize(v), ThemeDefault) } // P pretty print the values func P(values ...interface{}) error { list := []interface{}{} for _, v := range values { list = append(list, F(v)) } pc, file, line, _ := runtime.Caller(1) fn := runtime.FuncForPC(pc).Name() cwd, _ := os.Getwd() file, _ = filepath.Rel(cwd, file) tpl := Stylize("// %s %s:%d (%s)\n", ThemeDefault(Comment)) _, _ = fmt.Fprintf(Stdout, tpl, time.Now().Format(time.RFC3339Nano), file, line, fn) _, err := fmt.Fprintln(Stdout, list...) return err } // Plain is a shortcut for Format with plain color func Plain(v interface{}) string { return Format(Tokenize(v), ThemeNone) } // indentCache holds pre-repeated indent strings to avoid calling // strings.Repeat for every indented line. var indentCache = func() []string { out := make([]string, 33) for i := range out { out[i] = strings.Repeat(indentUnit, i) } return out }() func writeIndent(sb *strings.Builder, depth int) { if depth < len(indentCache) { sb.WriteString(indentCache[depth]) return } for i := 0; i < depth; i++ { sb.WriteString(indentUnit) } } // Format a list of tokens. func Format(ts []Token, theme Theme) string { var out strings.Builder depth := 0 for i, t := range ts { tt := t.Type() if oneOf(tt, SliceOpen, MapOpen, StructOpen) { depth++ } if i < len(ts)-1 && oneOf(ts[i+1].Type(), SliceClose, MapClose, StructClose) { depth-- } styles := theme(tt) switch tt { case SliceOpen, MapOpen, StructOpen: buildStyled(&out, t, styles) out.WriteByte('\n') case SliceItem, MapKey, StructKey: writeIndent(&out, depth) case Colon, InlineComma, Chan: buildStyled(&out, t, styles) out.WriteByte(' ') case Comma: buildStyled(&out, t, styles) out.WriteByte('\n') case SliceClose, MapClose, StructClose: s := out.String() if strings.HasSuffix(s, "{\n") { out.Reset() out.WriteString(s[:len(s)-1]) buildStyled(&out, t, styles) } else { writeIndent(&out, depth) buildStyled(&out, t, styles) } case String: writeReadableString(&out, t, depth, styles) default: buildStyled(&out, t, styles) } } return out.String() } // buildStyled renders t into sb, applying styles when any are active. // Non-Lit tokens always produce single-line output, so we can emit the // escape sequences around t.Build directly and skip the temp builder. func buildStyled(sb *strings.Builder, t Token, styles []Style) { if NoStyle || !hasActiveStyle(styles) { t.Build(sb) return } if l, ok := t.(*Lit); ok { Render(sb, l.L, styles) return } for i := len(styles) - 1; i >= 0; i-- { if styles[i] != None { sb.WriteString(styles[i].Set) } } t.Build(sb) for _, s := range styles { if s != None { sb.WriteString(s.Unset) } } } // writeReadableString handles the String-type token path: it materializes // the raw literal, reshapes it via readableStr, then stylizes the result. func writeReadableString(sb *strings.Builder, t Token, depth int, styles []Style) { var raw string if l, ok := t.(*Lit); ok { raw = l.L } else { var inner strings.Builder t.Build(&inner) raw = inner.String() } s := readableStr(depth, raw) if NoStyle || !hasActiveStyle(styles) { sb.WriteString(s) return } Render(sb, s, styles) } func oneOf(t Type, list ...Type) bool { for _, el := range list { if t == el { return true } } return false } // To make multi-line string block more human readable. // Split newline into two strings, convert "\t" into tab. // Such as format string: "line one \n\t line two" into: // // "line one \n" + // " line two" func readableStr(depth int, s string) string { if (strings.Contains(s, "\n") || strings.Contains(s, `"`)) && !strings.Contains(s, "`") { return "`" + s + "`" } s = fmt.Sprintf("%#v", s) s, _ = replaceEscaped(s, 't', " ") indent := strings.Repeat(indentUnit, depth+1) if n, has := replaceEscaped(s, 'n', "\\n\" +\n"+indent+"\""); has { return "\"\" +\n" + indent + n } return s } // We use a simple state machine to replace escaped char like "\n" func replaceEscaped(s string, escaped rune, new string) (string, bool) { type State int const ( init State = iota preMatch match ) state := init var out strings.Builder var buf strings.Builder has := false onInit := func(r rune) { state = init out.WriteString(buf.String()) out.WriteRune(r) buf.Reset() } onPreMatch := func() { state = preMatch buf.Reset() buf.WriteString("\\") } onEscape := func() { state = match out.WriteString(new) buf.Reset() has = true } for _, r := range s { switch state { case preMatch: switch r { case escaped: onEscape() default: onInit(r) } case match: switch r { case '\\': onPreMatch() default: onInit(r) } default: switch r { case '\\': onPreMatch() default: onInit(r) } } } return out.String(), has } golang-github-ysmood-gop-0.4.1/format_test.go000066400000000000000000000400571520675366400212400ustar00rootroot00000000000000package gop_test import ( "bytes" "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "io" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "text/template" "time" "unsafe" "github.com/ysmood/gop" ) func eq(t *testing.T, a, b interface{}) { t.Helper() if a == b { return } t.Log(a, "[should equal]", b) t.Fail() } // RandStr generates a random string with the specified length func randStr(l int) string { b := make([]byte, (l+1)/2) _, _ = rand.Read(b) return hex.EncodeToString(b)[:l] } func render(value string, data interface{}) string { out := bytes.NewBuffer(nil) t := template.New("") t, err := t.Parse(value) if err != nil { panic(err) } err = t.Execute(out, data) if err != nil { panic(err) } return out.String() } func assertPanic(t *testing.T, fn func()) (val interface{}) { t.Helper() defer func() { t.Helper() val = recover() if val == nil { t.Error("should panic") } }() fn() return } func TestStyle(t *testing.T) { s := gop.Style{Set: "", Unset: ""} eq(t, gop.S("test", s), "test") eq(t, gop.S("", s), "") eq(t, gop.S("", gop.None), "") } func TestTokenize(t *testing.T) { ref := "test" timeStamp, _ := time.Parse(time.RFC3339Nano, "2021-08-28T08:36:36.807908+08:00") fn := func(string) int { return 10 } ch1 := make(chan int) ch2 := make(chan string, 3) ch3 := make(chan struct{}) v := []interface{}{ nil, []int{}, []interface{}{true, false, uintptr(0x17), float32(100.121111133)}, true, 10, int8(2), int32(100), float64(100.121111133), complex64(1 + 2i), complex128(1 + 2i), [3]int{1, 2}, ch1, ch2, ch3, fn, map[interface{}]interface{}{ `"test"`: 10, "a": 1, &ref: 1, }, unsafe.Pointer(&ref), struct { Int int str string M map[int]int }{10, "ok", map[int]int{1: 0x20}}, []byte("aa\xe2"), []byte("bytes\n\tbytes"), []byte("long long long long string"), byte('a'), byte(1), '天', "long long long long string", "\ntest", "\t\n`", &ref, (*struct{ Int int })(nil), &struct{ Int int }{}, &map[int]int{1: 2, 3: 4}, &[]int{1, 2}, &[2]int{1, 2}, &[]byte{1, 2}, timeStamp, time.Hour, `{"a": 1}`, []byte(`{"a": 1}`), } check := func(out string, file string) { t.Helper() tpl, err := os.ReadFile(file) if err != nil { t.Fatal(err) } expected := render(string(tpl), map[string]interface{}{ "ch1": fmt.Sprintf("0x%x", reflect.ValueOf(ch1).Pointer()), "ch2": fmt.Sprintf("0x%x", reflect.ValueOf(ch2).Pointer()), "ch3": fmt.Sprintf("0x%x", reflect.ValueOf(ch3).Pointer()), "fn": fmt.Sprintf("0x%x", reflect.ValueOf(fn).Pointer()), "ptr": fmt.Sprintf("%v", &ref), "ref": fmt.Sprintf("0x%x", reflect.ValueOf(&ref).Pointer()), }) if out != expected { t.Log("check failed") writeFile(t, "tmp/expected.txt", expected) writeFile(t, "tmp/out.txt", out) t.Fail() } } out := gop.StripANSI(gop.F(v)) { b, err := os.ReadFile(filepath.Join("fixtures", "compile_check.go.tmpl")) if err != nil { t.Fatal(err) } code := fmt.Sprintf(string(b), out) f := filepath.Join("tmp", randStr(8), "main.go") err = os.MkdirAll(filepath.Dir(f), 0755) if err != nil { t.Fatal(err) } writeFile(t, f, code) b, err = exec.Command("go", "run", f).CombinedOutput() if err != nil { t.Fatal(string(b)) } } check(out, filepath.Join("fixtures", "expected.tmpl")) out = gop.VisualizeANSI(gop.F(v)) check(out, filepath.Join("fixtures", "expected_with_color.tmpl")) } func TestRef(t *testing.T) { a := [2][]int{{1}} a[1] = a[0] eq(t, gop.Plain(a), `[2][]int{ []int{ 1, }, []int{ 1, }, }`) } type A struct { Int int B *B } type B struct { s string a *A } func TestCircularRef(t *testing.T) { a := A{Int: 10} b := B{"test", &a} a.B = &b eq(t, gop.StripANSI(gop.F(a)), `gop_test.A{ Int: 10, B: &gop_test.B{ s: "test", a: &gop_test.A{ Int: 10, B: gop.Circular("B").(*gop_test.B), }, }, }`) } func TestCircularNilRef(t *testing.T) { arr := []A{{}, {}} eq(t, gop.StripANSI(gop.F(arr)), `[]gop_test.A{ gop_test.A{ Int: 0, B: (*gop_test.B)(nil), }, gop_test.A{ Int: 0, B: (*gop_test.B)(nil), }, }`) } func TestCircularMap(t *testing.T) { a := map[int]interface{}{} a[0] = a ts := gop.Tokenize(a) eq(t, gop.Format(ts, gop.ThemeNone), `map[int]interface {}{ 0: gop.Circular().(map[int]interface {}), }`) } func TestCircularSlice(t *testing.T) { a := [][]interface{}{{nil}, {nil}} a[0][0] = a[1] a[1][0] = a[0][0] ts := gop.Tokenize(a) eq(t, gop.Format(ts, gop.ThemeNone), `[][]interface {}{ []interface {}{ []interface {}{ gop.Circular(0, 0).([]interface {}), }, }, []interface {}{ gop.Circular(1).([]interface {}), }, }`) } func TestCircularMapKey(t *testing.T) { a := map[interface{}]interface{}{} b := map[interface{}]interface{}{} a[&b] = b b[&a] = a ts := gop.Tokenize(b) eq(t, gop.Format(ts, gop.ThemeNone), render(`map[interface {}]interface {}{ (interface {})(nil)/* {{.a}} */: map[interface {}]interface {}{ (interface {})(nil)/* {{.b}} */: gop.Circular().(map[interface {}]interface {}), }, }`, map[string]interface{}{ "a": fmt.Sprintf("0x%x", reflect.ValueOf(&a).Pointer()), "b": fmt.Sprintf("0x%x", reflect.ValueOf(&b).Pointer()), })) } func TestPlain(t *testing.T) { eq(t, gop.Plain(10), "10") } func TestPlainMinify(t *testing.T) { a := map[int]interface{}{ 1: "a", } b := map[int]interface{}{} pa := gop.Plain(a) pb := gop.Plain(b) eq(t, pa, "map[int]interface {}{\n 1: \"a\",\n}") eq(t, pb, "map[int]interface {}{}") } func TestP(t *testing.T) { gop.Stdout = io.Discard _ = gop.P("test") gop.Stdout = os.Stdout } func TestConvertors(t *testing.T) { eq(t, gop.Circular(""), nil) s := randStr(8) eq(t, *gop.Ptr(s).(*string), s) bs := base64.StdEncoding.EncodeToString([]byte(s)) eq(t, string(gop.Base64(bs)), string([]byte(s))) now := time.Now() eq(t, gop.Time(now.Format(time.RFC3339Nano), 1234).Unix(), now.Unix()) eq(t, gop.Duration("10m"), 10*time.Minute) eq(t, gop.JSONStr(nil, "[1, 2]"), "[1, 2]") eq(t, string(gop.JSONBytes(nil, "[1, 2]")), string([]byte("[1, 2]"))) eq(t, gop.Rune(100, 'd'), 'd') eq(t, gop.Rune(100, 'd'), int32(100)) eq(t, gop.GopError("test").Error(), "test") } func TestGetPrivateFieldErr(t *testing.T) { assertPanic(t, func() { gop.GetPrivateField(reflect.ValueOf(1), 0) }) assertPanic(t, func() { gop.GetPrivateFieldByName(reflect.ValueOf(1), "test") }) } func TestTypeName(t *testing.T) { type f float64 type i int type c complex128 type b byte eq(t, gop.Plain(f(1)), "gop_test.f(1.0)") eq(t, gop.Plain(i(1)), "gop_test.i(1)") eq(t, gop.Plain(c(1)), "gop_test.c(1+0i)") eq(t, gop.Plain(b('a')), "gop_test.b(97)") } func TestFixNestedStyle(t *testing.T) { s := gop.S(" 0 "+gop.S(" 1 "+ gop.S(" 2 "+ gop.S(" 3 ", gop.Cyan)+ " 4 ", gop.Blue)+ " 5 ", gop.Red)+" 6 ", gop.BgRed) out := gop.VisualizeANSI(gop.FixNestedStyle(s)) eq(t, out, `<41> 0 <31> 1 <39><34> 2 <39><36> 3 <39><34> 4 <39><31> 5 <39> 6 <49>`) gop.FixNestedStyle("test") } func TestStripANSI(t *testing.T) { eq(t, gop.StripANSI(gop.S("test", gop.Red)), "test") } func TestTheme(t *testing.T) { eq(t, gop.ThemeDefault(gop.Error)[0], gop.Underline) } func TestNil(t *testing.T) { eq(t, gop.Plain(map[string]string(nil)), "map[string]string(nil)") eq(t, gop.Plain(chan int(nil)), "(chan int)(nil)") eq(t, gop.Plain([]string(nil)), "[]string(nil)") eq(t, gop.Plain((func())(nil)), "(func())(nil)") eq(t, gop.Plain((*struct{})(nil)), "(*struct {})(nil)") } func TestJSONArrayBug(t *testing.T) { // Test for the bug fix where JSON arrays were incorrectly checked as objects // The bug was: _, isArr := jv.(map[string]interface{}) instead of jv.([]interface{}) // This caused JSON arrays to never be recognized, so they weren't properly formatted jsonArrayStr := `[1, 2, 3]` result := gop.Plain(jsonArrayStr) // With the fix, JSON arrays should be detected and formatted with gop.JSONStr() // The result should contain "gop.JSONStr(" indicating proper JSON array detection if !strings.Contains(result, "gop.JSONStr(") { t.Errorf("JSON array not properly detected as JSON. Got: %s", result) } // Test with byte array too jsonArrayBytes := []byte(`[{"key": "value"}, 123]`) result2 := gop.Plain(jsonArrayBytes) // With the fix, JSON byte arrays should be detected and formatted with gop.JSONBytes() if !strings.Contains(result2, "gop.JSONBytes(") { t.Errorf("JSON byte array not properly detected as JSON. Got: %s", result2) } // Test that the tokenized version contains the actual parsed array structure if !strings.Contains(result, "[]interface {}") { t.Errorf("JSON array structure not properly tokenized. Got: %s", result) } } func TestMaxDepth(t *testing.T) { // Create a deeply nested structure type Node struct { Value int Next *Node } // Create a chain of 20 nodes var root *Node current := &Node{Value: 0} root = current for i := 1; i < 20; i++ { current.Next = &Node{Value: i} current = current.Next } // Test with default MaxDepth (15) result := gop.Plain(root) if !strings.Contains(result, "gop.GopError(") { t.Errorf("Expected max depth error with default MaxDepth, got: %s", result) } if !strings.Contains(result, "max depth exceeded") { t.Errorf("Expected 'max depth exceeded' message, got: %s", result) } // Test with custom MaxDepth of 5 tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 5}) result = gop.Format(tokens, gop.ThemeNone) if !strings.Contains(result, "gop.GopError(") { t.Errorf("Expected max depth error with MaxDepth=5, got: %s", result) } if !strings.Contains(result, "max depth exceeded") { t.Errorf("Expected 'max depth exceeded' message with MaxDepth=5, got: %s", result) } // Test with MaxDepth of 0 (no limit) tokens = gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0}) result = gop.Format(tokens, gop.ThemeNone) if strings.Contains(result, "max depth exceeded") { t.Errorf("Should not have max depth error with MaxDepth=0, got: %s", result) } // Verify all 20 values are present when no limit for i := 0; i < 20; i++ { if !strings.Contains(result, fmt.Sprintf("Value: %d", i)) { t.Errorf("Missing Value: %d in unlimited depth output", i) } } // Test with deeply nested maps deepMap := map[string]interface{}{ "level1": map[string]interface{}{ "level2": map[string]interface{}{ "level3": map[string]interface{}{ "level4": map[string]interface{}{ "level5": map[string]interface{}{ "level6": "deep value", }, }, }, }, }, } // Test with MaxDepth of 3 tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 3}) result = gop.Format(tokens, gop.ThemeNone) if !strings.Contains(result, "gop.GopError(") { t.Errorf("Expected max depth error with nested maps at MaxDepth=3, got: %s", result) } // Test with MaxDepth of 10 (should be enough for this structure) tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 10}) result = gop.Format(tokens, gop.ThemeNone) if strings.Contains(result, "max depth exceeded") { t.Errorf("Should not have max depth error with MaxDepth=10 for this structure, got: %s", result) } if !strings.Contains(result, "deep value") { t.Errorf("Should contain 'deep value' with sufficient MaxDepth, got: %s", result) } // Test with deeply nested slices var deepSlice interface{} = []interface{}{ []interface{}{ []interface{}{ []interface{}{ []interface{}{ []interface{}{ "deeply nested", }, }, }, }, }, } tokens = gop.TokenizeWithOptions(deepSlice, gop.Options{MaxDepth: 4}) result = gop.Format(tokens, gop.ThemeNone) if !strings.Contains(result, "gop.GopError(") { t.Errorf("Expected max depth error with nested slices at MaxDepth=4, got: %s", result) } } func writeFile(t *testing.T, f, code string) { err := os.WriteFile(f, []byte(code), 0644) if err != nil { t.Fatal(err) } } // stringTok is a non-Lit Token that reports String type. It exists so // tests can exercise the writeReadableString non-Lit branch. type stringTok struct{ s string } func (stringTok) Type() gop.Type { return gop.String } func (t stringTok) Build(sb *strings.Builder) { sb.WriteString(t.s) } func TestStyledToken(t *testing.T) { var sb strings.Builder // Nil Inner: Type returns Nil, Build writes nothing. empty := gop.Styled{} eq(t, empty.Type(), gop.Nil) empty.Build(&sb) eq(t, sb.String(), "") // Non-nil Inner with no active styles: writes inner verbatim. sb.Reset() plain := gop.Styled{Inner: &gop.Lit{T: gop.String, L: "hi"}} eq(t, plain.Type(), gop.String) plain.Build(&sb) eq(t, sb.String(), "hi") // Lit fast path with active style. sb.Reset() litStyled := gop.Styled{ Inner: &gop.Lit{T: gop.String, L: "hi"}, Styles: []gop.Style{gop.Red}, } litStyled.Build(&sb) eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset) // Non-Lit inner with active style: goes through the temp builder path. sb.Reset() nonLitStyled := gop.Styled{ Inner: stringTok{s: "hi"}, Styles: []gop.Style{gop.Red}, } nonLitStyled.Build(&sb) eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset) } func TestRenderNoStyle(t *testing.T) { orig := gop.NoStyle gop.NoStyle = true defer func() { gop.NoStyle = orig }() var sb strings.Builder gop.Render(&sb, "hi", []gop.Style{gop.Red}) eq(t, sb.String(), "hi") } func TestRenderMultilineSkipsNone(t *testing.T) { var sb strings.Builder // Multi-line path with a None entry that must be skipped without altering output. gop.Render(&sb, "a\nb", []gop.Style{gop.None, gop.Red}) eq(t, sb.String(), gop.Red.Set+"a"+gop.Red.Unset+"\n"+gop.Red.Set+"b"+gop.Red.Unset) } func TestRenderMultilineCRLF(t *testing.T) { // CRLF input exercises the \r-trim branch in Render and the \r\n // return in firstNewline; a leading newline also exercises the idx==0 // segment path. var sb strings.Builder gop.Render(&sb, "\r\na\r\nb", []gop.Style{gop.Red}) wrap := func(s string) string { return gop.Red.Set + s + gop.Red.Unset } eq(t, sb.String(), wrap("")+"\r\n"+wrap("a")+"\r\n"+wrap("b")) } func TestWriteIndentDeep(t *testing.T) { // Build a chain of pointers deeper than the indentCache (33 levels) // so Format's indent writer falls through to the per-level loop. type Node struct{ Next *Node } root := &Node{} cur := root for i := 0; i < 40; i++ { cur.Next = &Node{} cur = cur.Next } tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0}) out := gop.Format(tokens, gop.ThemeNone) // The deepest "Next:" field should be indented well beyond 33 levels. deepIndent := strings.Repeat(" ", 40) + "Next:" if !strings.Contains(out, deepIndent) { t.Errorf("expected deep indent past the cache, got:\n%s", out) } } func TestFormatNonLitStringToken(t *testing.T) { // writeReadableString has a non-Lit branch reached only when a // caller passes a Token with Type()==String that isn't a *Lit. tokens := []gop.Token{stringTok{s: "hi"}} eq(t, gop.Format(tokens, gop.ThemeNone), `"hi"`) // With an active theme it also exercises the Render path. themed := gop.Format(tokens, func(tp gop.Type) []gop.Style { if tp == gop.String { return []gop.Style{gop.Red} } return []gop.Style{gop.None} }) eq(t, themed, gop.Red.Set+`"hi"`+gop.Red.Unset) } func TestTokenizeNumberAllKinds(t *testing.T) { type myInt int type myInt8 int8 type myUint uint type myFloat64 float64 type myComplex128 complex128 type myUintptr uintptr cases := []struct { v interface{} want string }{ {int(1), "1"}, {myInt(1), "gop_test.myInt(1)"}, {int8(1), "int8(1)"}, {myInt8(1), "gop_test.myInt8(1)"}, {uint(1), "uint(1)"}, {myUint(1), "gop_test.myUint(1)"}, {uintptr(1), "uintptr(1)"}, {myUintptr(1), "gop_test.myUintptr(1)"}, {float32(1), "float32(1)"}, {float64(1), "1.0"}, {myFloat64(1), "gop_test.myFloat64(1.0)"}, {complex64(1 + 2i), "complex64(1+2i)"}, {complex128(1 + 2i), "1+2i"}, {myComplex128(1 + 2i), "gop_test.myComplex128(1+2i)"}, } for _, c := range cases { got := gop.Plain(c.v) if got != c.want { t.Errorf("gop.Plain(%v) = %q, want %q", c.v, got, c.want) } } } golang-github-ysmood-gop-0.4.1/go.mod000066400000000000000000000000471520675366400174630ustar00rootroot00000000000000module github.com/ysmood/gop go 1.19 golang-github-ysmood-gop-0.4.1/style.go000066400000000000000000000134301520675366400200440ustar00rootroot00000000000000package gop import ( "fmt" "os" "os/exec" "regexp" "strconv" "strings" ) // Style type type Style struct { Set string Unset string } var ( // Bold style Bold = addStyle(1, 22) // Faint style Faint = addStyle(2, 22) // Italic style Italic = addStyle(3, 23) // Underline style Underline = addStyle(4, 24) // Blink style Blink = addStyle(5, 25) // RapidBlink style RapidBlink = addStyle(6, 26) // Invert style Invert = addStyle(7, 27) // Hide style Hide = addStyle(8, 28) // Strike style Strike = addStyle(9, 29) // Black color Black = addStyle(30, 39) // Red color Red = addStyle(31, 39) // Green color Green = addStyle(32, 39) // Yellow color Yellow = addStyle(33, 39) // Blue color Blue = addStyle(34, 39) // Magenta color Magenta = addStyle(35, 39) // Cyan color Cyan = addStyle(36, 39) // White color White = addStyle(37, 39) // BgBlack color BgBlack = addStyle(40, 49) // BgRed color BgRed = addStyle(41, 49) // BgGreen color BgGreen = addStyle(42, 49) // BgYellow color BgYellow = addStyle(43, 49) // BgBlue color BgBlue = addStyle(44, 49) // BgMagenta color BgMagenta = addStyle(45, 49) // BgCyan color BgCyan = addStyle(46, 49) // BgWhite color BgWhite = addStyle(47, 49) // None type None = Style{} ) // Styled wraps an inner Token and applies the given Styles when built. // It is the token form of the legacy Stylize helper. type Styled struct { Inner Token Styles []Style } // Type returns the inner token type. func (s Styled) Type() Type { if s.Inner == nil { return Nil } return s.Inner.Type() } // Build writes the styled rendering of the inner token to sb. func (s Styled) Build(sb *strings.Builder) { if s.Inner == nil { return } if NoStyle || !hasActiveStyle(s.Styles) { s.Inner.Build(sb) return } // Fast path: a Lit already holds its string, skip the temp builder. if l, ok := s.Inner.(*Lit); ok { Render(sb, l.L, s.Styles) return } var inner strings.Builder s.Inner.Build(&inner) Render(sb, inner.String(), s.Styles) } func hasActiveStyle(styles []Style) bool { for _, s := range styles { if s != None { return true } } return false } // Render writes the stylized form of str to sb without allocating // intermediate strings: both the single-line and multi-line paths stream // directly into sb. func Render(sb *strings.Builder, str string, styles []Style) { if NoStyle || !hasActiveStyle(styles) { sb.WriteString(str) return } if !strings.ContainsAny(str, "\r\n") { writeStyleSets(sb, styles) sb.WriteString(str) writeStyleUnsets(sb, styles) return } newline := firstNewline(str) remaining := str first := true for { idx := strings.IndexByte(remaining, '\n') if idx < 0 { if !first { sb.WriteString(newline) } writeStyleSets(sb, styles) sb.WriteString(remaining) writeStyleUnsets(sb, styles) return } end := idx if idx > 0 && remaining[idx-1] == '\r' { end = idx - 1 } if !first { sb.WriteString(newline) } first = false writeStyleSets(sb, styles) sb.WriteString(remaining[:end]) writeStyleUnsets(sb, styles) remaining = remaining[idx+1:] } } func writeStyleSets(sb *strings.Builder, styles []Style) { for i := len(styles) - 1; i >= 0; i-- { if styles[i] != None { sb.WriteString(styles[i].Set) } } } func writeStyleUnsets(sb *strings.Builder, styles []Style) { for _, s := range styles { if s != None { sb.WriteString(s.Unset) } } } func firstNewline(s string) string { idx := strings.IndexByte(s, '\n') if idx > 0 && s[idx-1] == '\r' { return "\r\n" } return "\n" } // S is the shortcut for Stylize. func S(str string, styles ...Style) string { return Stylize(str, styles) } // Stylize wraps str with the given styles. func Stylize(str string, styles []Style) string { if NoStyle || !hasActiveStyle(styles) { return str } var sb strings.Builder Render(&sb, str, styles) return sb.String() } // NoStyle respects https://no-color.org/ and "tput colors" var NoStyle = func() bool { _, noColor := os.LookupEnv("NO_COLOR") b, _ := exec.Command("tput", "colors").CombinedOutput() n, _ := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 32) return noColor || n == 0 }() // RegANSI token var RegANSI = regexp.MustCompile(`\x1b\[\d+m`) // StripANSI tokens func StripANSI(str string) string { return RegANSI.ReplaceAllString(str, "") } var regNum = regexp.MustCompile(`\d+`) // VisualizeANSI tokens func VisualizeANSI(str string) string { return RegANSI.ReplaceAllStringFunc(str, func(s string) string { return "<" + regNum.FindString(s) + ">" }) } // FixNestedStyle like // // 12345 // // into // // 12345 func FixNestedStyle(s string) string { out := "" stacks := map[string][]string{} i := 0 l := 0 r := 0 for i < len(s) { loc := RegANSI.FindStringIndex(s[i:]) if loc == nil { break } l, r = i+loc[0], i+loc[1] token := s[l:r] out += s[i:l] unset := GetStyle(token).Unset if unset == "" { unset = token } if _, has := stacks[unset]; !has { stacks[unset] = []string{} } stack := stacks[unset] if len(stack) == 0 { stack = append(stack, token) out += token } else { if token == GetStyle(last(stack)).Unset { out += token stack = stack[:len(stack)-1] if len(stack) > 0 { out += last(stack) } } else { out += GetStyle(last(stack)).Unset stack = append(stack, token) out += token } } stacks[unset] = stack i = r } return out + s[i:] } // GetStyle from available styles func GetStyle(s string) Style { return styleSetMap[s] } func last(list []string) string { return list[len(list)-1] } var styleSetMap = map[string]Style{} func addStyle(set, unset int) Style { s := Style{ fmt.Sprintf("\x1b[%dm", set), fmt.Sprintf("\x1b[%dm", unset), } styleSetMap[s.Set] = s return s } golang-github-ysmood-gop-0.4.1/token.go000066400000000000000000000416611520675366400200330ustar00rootroot00000000000000package gop import ( "encoding/base64" "encoding/json" "reflect" "sort" "strconv" "strings" "time" "unicode" "unicode/utf8" ) // Type of token type Type int const ( // Nil type Nil Type = iota // Bool type Bool // Number type Number // Float type Float // Complex type Complex // String type String // Byte type Byte // RuneInt32 type RuneInt32 // Chan type Chan // Func type Func // Error type Error // Comment type Comment // TypeName type TypeName // ParenOpen type ParenOpen // ParenClose type ParenClose // Dot type Dot // And type And // SliceOpen type SliceOpen // SliceItem type SliceItem // InlineComma type InlineComma // Comma type Comma // SliceClose type SliceClose // MapOpen type MapOpen // MapKey type MapKey // Colon type Colon // MapClose type MapClose // StructOpen type StructOpen // StructKey type StructKey // StructField type StructField // StructClose type StructClose ) // Token represents a symbol in value layout. Build appends the // token's rendered form to sb so the literal can be computed // lazily and avoid allocating intermediate strings. type Token interface { Type() Type Build(sb *strings.Builder) } // Lit is a token with a fixed literal string. type Lit struct { T Type L string } // Type returns the token type. func (l *Lit) Type() Type { return l.T } // Build writes the literal to sb. func (l *Lit) Build(sb *strings.Builder) { sb.WriteString(l.L) } // IntTok lazily renders a signed integer as a Number token. type IntTok int64 // Type returns Number. func (IntTok) Type() Type { return Number } // Build appends the decimal representation to sb. func (n IntTok) Build(sb *strings.Builder) { var buf [20]byte sb.Write(strconv.AppendInt(buf[:0], int64(n), 10)) } // UintTok lazily renders an unsigned integer as a Number token. type UintTok uint64 // Type returns Number. func (UintTok) Type() Type { return Number } // Build appends the decimal representation to sb. func (n UintTok) Build(sb *strings.Builder) { var buf [20]byte sb.Write(strconv.AppendUint(buf[:0], uint64(n), 10)) } // Float64Tok lazily renders a float64 as a Number token, appending ".0" when the value is integral. type Float64Tok float64 // Type returns Number. func (Float64Tok) Type() Type { return Number } // Build appends the formatted value to sb. func (f Float64Tok) Build(sb *strings.Builder) { var buf [32]byte s := strconv.AppendFloat(buf[:0], float64(f), 'f', -1, 64) sb.Write(s) hasDot := false for _, b := range s { if b == '.' { hasDot = true break } } if !hasDot { sb.WriteString(".0") } } // FloatTok lazily renders a float at the given bit size as a Number token. type FloatTok struct { V float64 Bits int } // Type returns Number. func (FloatTok) Type() Type { return Number } // Build appends the formatted value to sb. func (f FloatTok) Build(sb *strings.Builder) { var buf [32]byte sb.Write(strconv.AppendFloat(buf[:0], f.V, 'f', -1, f.Bits)) } // ComplexTok lazily renders a complex value at the given bit size as a Number token, // stripping the parentheses that strconv.FormatComplex adds. type ComplexTok struct { V complex128 Bits int } // Type returns Number. func (ComplexTok) Type() Type { return Number } // Build appends the formatted value to sb. func (c ComplexTok) Build(sb *strings.Builder) { s := strconv.FormatComplex(c.V, 'f', -1, c.Bits) sb.WriteString(s[1 : len(s)-1]) } // PtrTok lazily renders a uintptr as a Number token in 0xHEX form. type PtrTok uintptr // Type returns Number. func (PtrTok) Type() Type { return Number } // Build appends the hex representation to sb. func (p PtrTok) Build(sb *strings.Builder) { sb.WriteString("0x") var buf [20]byte sb.Write(strconv.AppendUint(buf[:0], uint64(p), 16)) } // CommentPtrTok lazily renders a uintptr as a Comment token in /* 0xHEX */ form. type CommentPtrTok uintptr // Type returns Comment. func (CommentPtrTok) Type() Type { return Comment } // Build appends the wrapped hex representation to sb. func (p CommentPtrTok) Build(sb *strings.Builder) { sb.WriteString("/* 0x") var buf [20]byte sb.Write(strconv.AppendUint(buf[:0], uint64(p), 16)) sb.WriteString(" */") } // RuneTok lazily renders a rune as a RuneInt32 token (quoted). type RuneTok rune // Type returns RuneInt32. func (RuneTok) Type() Type { return RuneInt32 } // Build appends the quoted rune to sb. func (r RuneTok) Build(sb *strings.Builder) { sb.WriteString(strconv.QuoteRune(rune(r))) } // ByteTok lazily renders a byte as a Byte token, quoted when graphic else as 0xHEX. type ByteTok byte // Type returns Byte. func (ByteTok) Type() Type { return Byte } // Build appends the rendered byte to sb. func (b ByteTok) Build(sb *strings.Builder) { r := rune(b) if unicode.IsGraphic(r) { sb.WriteString(strconv.QuoteRune(r)) return } sb.WriteString("0x") var buf [4]byte sb.Write(strconv.AppendUint(buf[:0], uint64(b), 16)) } // Pre-allocated singletons for common fixed-literal tokens. Reusing // these avoids per-token heap allocations during tokenization. var ( tokParenOpen Token = &Lit{ParenOpen, "("} tokParenClose Token = &Lit{ParenClose, ")"} tokDot Token = &Lit{Dot, "."} tokAnd Token = &Lit{And, "&"} tokSliceOpen Token = &Lit{SliceOpen, "{"} tokSliceClose Token = &Lit{SliceClose, "}"} tokSliceItem Token = &Lit{SliceItem, ""} tokInlineComma Token = &Lit{InlineComma, ","} tokComma Token = &Lit{Comma, ","} tokMapOpen Token = &Lit{MapOpen, "{"} tokMapClose Token = &Lit{MapClose, "}"} tokMapKey Token = &Lit{MapKey, ""} tokColon Token = &Lit{Colon, ":"} tokStructOpen Token = &Lit{StructOpen, "{"} tokStructClose Token = &Lit{StructClose, "}"} tokChan Token = &Lit{Chan, "chan"} tokNil Token = &Lit{Nil, "nil"} tokTrue Token = &Lit{Bool, "true"} tokFalse Token = &Lit{Bool, "false"} tokFuncMake Token = &Lit{Func, "make"} tokFuncCircular Token = &Lit{Func, SymbolCircular} tokFuncGopError Token = &Lit{Func, SymbolGopError} tokFuncBase64 Token = &Lit{Func, SymbolBase64} tokFuncTime Token = &Lit{Func, SymbolTime} tokFuncJSONStr Token = &Lit{Func, SymbolJSONStr} tokFuncJSONBytes Token = &Lit{Func, SymbolJSONBytes} tokFuncPtr Token = &Lit{Func, SymbolPtr} tokStrMaxDepth Token = &Lit{String, "max depth exceeded"} tokTNGopRune Token = &Lit{TypeName, "gop.Rune"} tokTNByte Token = &Lit{TypeName, "byte"} tokTNBytes Token = &Lit{TypeName, "[]byte"} tokTNUnsafePtr Token = &Lit{TypeName, "unsafe.Pointer"} tokTNUintptr Token = &Lit{TypeName, "uintptr"} tokTNDuration Token = &Lit{TypeName, SymbolDuration} ) // DefaultOptions for Tokenize. var DefaultOptions = Options{ MaxDepth: 15, } // Tokenize a random Go value with [DefaultOptions]. func Tokenize(v interface{}) []Token { return TokenizeWithOptions(v, DefaultOptions) } // Options controls tokenization. type Options struct { // MaxDepth limits the depth of tokenization for nested structures. // If less than 1 there is no limit. MaxDepth int } // TokenizeWithOptions tokenizes v with the given Options. func TokenizeWithOptions(v interface{}, opts Options) []Token { tz := tokenizer{Options: opts, global: map[uintptr]path{}, path: path{}} return tz.tokenize(reflect.ValueOf(v)) } func tokenize(v reflect.Value) []Token { tz := tokenizer{global: map[uintptr]path{}, path: path{}} return tz.tokenize(v) } type path []interface{} func (p path) tokens(opts Options) []Token { ts := []Token{} for i, seg := range p { ts = append(ts, TokenizeWithOptions(seg, opts)...) if i < len(p)-1 { ts = append(ts, tokInlineComma) } } return ts } type tokenizer struct { Options global map[uintptr]path path path } func (tz *tokenizer) push(p interface{}) { if v := reflect.ValueOf(p); v.Kind() == reflect.Ptr { p = v.Pointer() } tz.path = append(tz.path, p) } func (tz *tokenizer) pop() { tz.path = tz.path[:len(tz.path)-1] } func (tz *tokenizer) circular(v reflect.Value) ([]Token, func()) { cleanup := func() {} switch v.Kind() { case reflect.Ptr, reflect.Map, reflect.Slice: ptr := v.Pointer() if ptr == 0 { return nil, cleanup } if prev, has := tz.global[ptr]; has { ts := []Token{tokFuncCircular, tokParenOpen} ts = append(ts, prev.tokens(tz.Options)...) return append(ts, tokParenClose, tokDot, tokParenOpen, typeName(v.Type().String()), tokParenClose), cleanup } tz.global[ptr] = tz.path cleanup = func() { delete(tz.global, ptr) } } return nil, cleanup } func (tz *tokenizer) tokenize(v reflect.Value) []Token { if tz.MaxDepth > 0 && len(tz.path) >= tz.MaxDepth { return []Token{tokFuncGopError, tokParenOpen, tokStrMaxDepth, tokParenClose} } if ts, has := tz.tokenizeSpecial(v); has { return ts } { ts, cleanup := tz.circular(v) defer cleanup() if ts != nil { return ts } } switch v.Kind() { case reflect.Interface: return tz.tokenize(v.Elem()) case reflect.Bool: if v.Bool() { return []Token{tokTrue} } return []Token{tokFalse} case reflect.String: return tokenizeString(v) case reflect.Chan: if v.IsNil() { return []Token{tokParenOpen, typeName(v.Type().String()), tokParenClose, tokParenOpen, tokNil, tokParenClose} } if v.Cap() == 0 { return []Token{tokFuncMake, tokParenOpen, tokChan, typeName(v.Type().Elem().String()), tokParenClose, CommentPtrTok(v.Pointer())} } return []Token{tokFuncMake, tokParenOpen, tokChan, typeName(v.Type().Elem().String()), tokInlineComma, IntTok(v.Cap()), tokParenClose, CommentPtrTok(v.Pointer())} case reflect.Func: if v.IsNil() { return []Token{tokParenOpen, typeName(v.Type().String()), tokParenClose, tokParenOpen, tokNil, tokParenClose} } return []Token{tokParenOpen, &Lit{TypeName, v.Type().String()}, tokParenClose, tokParenOpen, tokNil, tokParenClose, CommentPtrTok(v.Pointer())} case reflect.Ptr: return tz.tokenizePtr(v) case reflect.UnsafePointer: return []Token{tokTNUnsafePtr, tokParenOpen, tokTNUintptr, tokParenOpen, PtrTok(v.Pointer()), tokParenClose, tokParenClose} case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Uintptr, reflect.Complex64, reflect.Complex128: return tokenizeNumber(v) default: // Slice, Array, Map, Struct — the remaining kinds reflect can surface here. return tz.tokenizeCollection(v) } } func (tz *tokenizer) tokenizeSpecial(v reflect.Value) ([]Token, bool) { if v.Kind() == reflect.Invalid { return []Token{tokNil}, true } else if r, ok := v.Interface().(rune); ok && unicode.IsGraphic(r) { return tokenizeRuneInt32(r), true } else if b, ok := v.Interface().(byte); ok { return tokenizeByte(b), true } else if t, ok := v.Interface().(time.Time); ok { return tokenizeTime(t), true } else if d, ok := v.Interface().(time.Duration); ok { return tokenizeDuration(d), true } return tz.tokenizeJSON(v) } func (tz *tokenizer) tokenizeCollection(v reflect.Value) []Token { var ts []Token switch v.Kind() { case reflect.Slice, reflect.Array: if v.Kind() == reflect.Slice && v.IsNil() { return []Token{typeName(v.Type().String()), tokParenOpen, tokNil, tokParenClose} } if data, ok := v.Interface().([]byte); ok { ts = tokenizeBytes(data) break } ts = make([]Token, 0, v.Len()*4+3) ts = append(ts, typeName(v.Type().String())) ts = append(ts, tokSliceOpen) for i := 0; i < v.Len(); i++ { el := v.Index(i) ts = append(ts, tokSliceItem) tz.push(i) ts = append(ts, tz.tokenize(el)...) tz.pop() ts = append(ts, tokComma) } ts = append(ts, tokSliceClose) case reflect.Map: if v.IsNil() { return []Token{typeName(v.Type().String()), tokParenOpen, tokNil, tokParenClose} } ts = make([]Token, 0, v.Len()*6+3) ts = append(ts, typeName(v.Type().String())) keys := v.MapKeys() sort.Slice(keys, func(i, j int) bool { return compare(keys[i].Interface(), keys[j].Interface()) < 0 }) ts = append(ts, tokMapOpen) for _, k := range keys { ts = append(ts, tokMapKey) if k.Kind() == reflect.Interface && k.Elem().Kind() == reflect.Ptr { ts = append(ts, tokenizeMapKey(k)...) } else { ts = append(ts, tokenize(k)...) } ts = append(ts, tokColon) tz.push(k.Interface()) ts = append(ts, tz.tokenize(v.MapIndex(k))...) tz.pop() ts = append(ts, tokComma) } ts = append(ts, tokMapClose) case reflect.Struct: t := v.Type() ts = make([]Token, 0, v.NumField()*6+3) ts = append(ts, typeName(t.String())) ts = append(ts, tokStructOpen) for i := 0; i < v.NumField(); i++ { name := t.Field(i).Name ts = append(ts, tokStructKey) ts = append(ts, &Lit{StructField, name}) f := v.Field(i) if !f.CanInterface() { f = GetPrivateField(v, i) } ts = append(ts, tokColon) tz.push(name) ts = append(ts, tz.tokenize(f)...) tz.pop() ts = append(ts, tokComma) } ts = append(ts, tokStructClose) } return ts } var tokStructKey Token = &Lit{StructKey, ""} func tokenizeNumber(v reflect.Value) []Token { tName := v.Type().String() switch v.Kind() { case reflect.Int: if tName != "int" { return []Token{typeName(tName), tokParenOpen, IntTok(v.Int()), tokParenClose} } return []Token{IntTok(v.Int())} case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return []Token{typeName(tName), tokParenOpen, IntTok(v.Int()), tokParenClose} case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return []Token{typeName(tName), tokParenOpen, UintTok(v.Uint()), tokParenClose} case reflect.Float32: return []Token{typeName(tName), tokParenOpen, FloatTok{V: v.Float(), Bits: 32}, tokParenClose} case reflect.Float64: if tName != "float64" { return []Token{typeName(tName), tokParenOpen, Float64Tok(v.Float()), tokParenClose} } return []Token{Float64Tok(v.Float())} case reflect.Complex64: return []Token{typeName(tName), tokParenOpen, ComplexTok{V: v.Complex(), Bits: 64}, tokParenClose} default: // reflect.Complex128 — callers dispatch only numeric kinds here. if tName != "complex128" { return []Token{typeName(tName), tokParenOpen, ComplexTok{V: v.Complex(), Bits: 128}, tokParenClose} } return []Token{ComplexTok{V: v.Complex(), Bits: 128}} } } func tokenizeRuneInt32(r rune) []Token { return []Token{ tokTNGopRune, tokParenOpen, IntTok(int64(r)), tokInlineComma, RuneTok(r), tokParenClose, } } func tokenizeByte(b byte) []Token { return []Token{tokTNByte, tokParenOpen, ByteTok(b), tokParenClose} } func tokenizeTime(t time.Time) []Token { ext := GetPrivateFieldByName(reflect.ValueOf(t), "ext").Int() return []Token{tokFuncTime, tokParenOpen, &Lit{String, t.Format(time.RFC3339Nano)}, tokInlineComma, IntTok(ext), tokParenClose} } func tokenizeDuration(d time.Duration) []Token { return []Token{tokTNDuration, tokParenOpen, &Lit{String, d.String()}, tokParenClose} } func tokenizeString(v reflect.Value) []Token { return []Token{&Lit{String, v.String()}} } func tokenizeBytes(data []byte) []Token { if utf8.Valid(data) { return []Token{tokTNBytes, tokParenOpen, &Lit{String, string(data)}, tokParenClose} } return []Token{tokFuncBase64, tokParenOpen, &Lit{String, base64.StdEncoding.EncodeToString(data)}, tokParenClose} } func tokenizeMapKey(v reflect.Value) []Token { return []Token{ tokParenOpen, typeName(v.Type().String()), tokParenClose, tokParenOpen, tokNil, tokParenClose, CommentPtrTok(v.Elem().Pointer()), } } func (tz *tokenizer) tokenizePtr(v reflect.Value) []Token { if v.Elem().Kind() == reflect.Invalid { return []Token{ tokParenOpen, typeName(v.Type().String()), tokParenClose, tokParenOpen, tokNil, tokParenClose} } needFn := false switch v.Elem().Kind() { case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: if _, ok := v.Elem().Interface().([]byte); ok { needFn = true } default: needFn = true } if needFn { ts := []Token{tokFuncPtr, tokParenOpen} ts = append(ts, tz.tokenize(v.Elem())...) ts = append(ts, tokParenClose, tokDot, tokParenOpen, typeName(v.Type().String()), tokParenClose) return ts } ts := []Token{tokAnd} ts = append(ts, tz.tokenize(v.Elem())...) return ts } func (tz *tokenizer) tokenizeJSON(v reflect.Value) ([]Token, bool) { var jv interface{} ts := []Token{} s := "" if v.Kind() == reflect.String { s = v.String() err := json.Unmarshal([]byte(s), &jv) if err != nil { return nil, false } ts = append(ts, tokFuncJSONStr) } else if b, ok := v.Interface().([]byte); ok { err := json.Unmarshal(b, &jv) if err != nil { return nil, false } s = string(b) ts = append(ts, tokFuncJSONBytes) } _, isObj := jv.(map[string]interface{}) _, isArr := jv.([]interface{}) if isObj || isArr { ts = append(ts, tokParenOpen) ts = append(ts, TokenizeWithOptions(jv, tz.Options)...) ts = append(ts, tokInlineComma, &Lit{String, s}, tokParenClose) return ts, true } return nil, false } func typeName(t string) Token { return &Lit{TypeName, t} } golang-github-ysmood-gop-0.4.1/utils.go000066400000000000000000000017541520675366400200520ustar00rootroot00000000000000package gop import ( "fmt" "reflect" "strings" "unsafe" ) // GetPrivateField via field index // TODO: we can use a LRU cache for the copy of the values, but it might be trivial for just testing. func GetPrivateField(v reflect.Value, i int) reflect.Value { if v.Kind() != reflect.Struct { panic("expect v to be a struct") } copied := reflect.New(v.Type()).Elem() copied.Set(v) f := copied.Field(i) return reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() } // GetPrivateFieldByName is similar with GetPrivateField func GetPrivateFieldByName(v reflect.Value, name string) reflect.Value { if v.Kind() != reflect.Struct { panic("expect v to be a struct") } copied := reflect.New(v.Type()).Elem() copied.Set(v) f := copied.FieldByName(name) return reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() } // compare returns the float value of x minus y func compare(x, y interface{}) int { return strings.Compare(fmt.Sprintf("%#v", x), fmt.Sprintf("%#v", y)) }