diff --git a/pkg/json/check_empty.go b/pkg/json/check_empty.go new file mode 100644 index 00000000..7f0b1b53 --- /dev/null +++ b/pkg/json/check_empty.go @@ -0,0 +1,55 @@ +package json + +import "reflect" + +type checkEmptyFunc func(v reflect.Value) bool + +var checkEmptyFuncs = map[reflect.Kind]checkEmptyFunc{ + reflect.String: checkStringEmpty, + reflect.Int: checkIntEmpty, + reflect.Int8: checkIntEmpty, + reflect.Int16: checkIntEmpty, + reflect.Int32: checkIntEmpty, + reflect.Int64: checkIntEmpty, + reflect.Uint: checkUintEmpty, + reflect.Uint8: checkUintEmpty, + reflect.Uint16: checkUintEmpty, + reflect.Uint32: checkUintEmpty, + reflect.Uint64: checkUintEmpty, + reflect.Float32: checkFloatEmpty, + reflect.Float64: checkFloatEmpty, + reflect.Bool: checkBoolEmpty, + reflect.Slice: checkLenEmpty, + reflect.Map: checkLenEmpty, + reflect.Array: checkLenEmpty, + reflect.Chan: reflect.Value.IsNil, + reflect.Func: reflect.Value.IsNil, + reflect.Interface: reflect.Value.IsNil, + reflect.Pointer: reflect.Value.IsNil, + reflect.Struct: reflect.Value.IsZero, + reflect.UnsafePointer: reflect.Value.IsNil, +} + +func checkStringEmpty(v reflect.Value) bool { + return v.String() == "" +} + +func checkIntEmpty(v reflect.Value) bool { + return v.Int() == 0 +} + +func checkUintEmpty(v reflect.Value) bool { + return v.Uint() == 0 +} + +func checkFloatEmpty(v reflect.Value) bool { + return v.Float() == 0 +} + +func checkBoolEmpty(v reflect.Value) bool { + return !v.Bool() +} + +func checkLenEmpty(v reflect.Value) bool { + return v.Len() == 0 +} diff --git a/pkg/json/encoder.go b/pkg/json/encoder.go new file mode 100644 index 00000000..a8f2a490 --- /dev/null +++ b/pkg/json/encoder.go @@ -0,0 +1,17 @@ +package json + +import "io" + +type Encoder struct { + w io.Writer +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} + +func (e *Encoder) Encode(v any) error { + data, _ := Marshal(v) + _, err := e.w.Write(data) + return err +} diff --git a/pkg/json/json.go b/pkg/json/json.go new file mode 100644 index 00000000..470c65b5 --- /dev/null +++ b/pkg/json/json.go @@ -0,0 +1,70 @@ +package json + +import ( + "reflect" + "sync" + + "github.com/bytedance/sonic" +) + +type Marshaler interface { + MarshalJSONTo(buf []byte) []byte +} + +var ( + Unmarshal = sonic.Unmarshal + Valid = sonic.Valid + NewDecoder = sonic.ConfigDefault.NewDecoder +) + +// Marshal returns the JSON encoding of v. +// +// It's like json.Marshal, but with some differences: +// +// - It's ~4-5x faster in most cases. +// +// - It also supports custom Marshaler interface (MarshalJSONTo(buf []byte) []byte) +// to allow further optimizations. +// +// - It leverages the strutils library. +// +// - It drops the need to implement Marshaler or json.Marshaler by supports extra field tags: +// +// `byte_size` to format the field to human readable size. +// +// `unix_time` to format the uint64 field to string date-time without specifying MarshalJSONTo. +// +// `use_marshaler` to force using the custom marshaler for primitive types declaration (e.g. `type Status int`). +// +// - It correct the behavior of *url.URL and time.Duration. +// +// - It does not support maps other than string-keyed maps. +func Marshal(v any) ([]byte, error) { + buf := newBytes() + defer putBytes(buf) + return cloneBytes(appendMarshal(reflect.ValueOf(v), buf)), nil +} + +func MarshalTo(v any, buf []byte) []byte { + return appendMarshal(reflect.ValueOf(v), buf) +} + +const bufSize = 1024 + +var bytesPool = sync.Pool{ + New: func() any { + return make([]byte, 0, bufSize) + }, +} + +func newBytes() []byte { + return bytesPool.Get().([]byte) +} + +func putBytes(buf []byte) { + bytesPool.Put(buf[:0]) +} + +func cloneBytes(buf []byte) (res []byte) { + return append(res, buf...) +} diff --git a/pkg/json/map.go b/pkg/json/map.go new file mode 100644 index 00000000..86eff006 --- /dev/null +++ b/pkg/json/map.go @@ -0,0 +1,24 @@ +package json + +import ( + "reflect" +) + +type Map[V any] map[string]V + +func (m Map[V]) MarshalJSONTo(buf []byte) []byte { + buf = append(buf, '{') + i := 0 + n := len(m) + for k, v := range m { + buf = AppendString(buf, k) + buf = append(buf, ':') + buf = appendMarshal(reflect.ValueOf(v), buf) + if i != n-1 { + buf = append(buf, ',') + } + i++ + } + buf = append(buf, '}') + return buf +} diff --git a/pkg/json/map_slice.go b/pkg/json/map_slice.go new file mode 100644 index 00000000..c1878ce0 --- /dev/null +++ b/pkg/json/map_slice.go @@ -0,0 +1,18 @@ +package json + +type MapSlice[V any] []Map[V] + +func (s MapSlice[V]) MarshalJSONTo(buf []byte) []byte { + buf = append(buf, '[') + i := 0 + n := len(s) + for _, entry := range s { + buf = entry.MarshalJSONTo(buf) + if i != n-1 { + buf = append(buf, ',') + } + i++ + } + buf = append(buf, ']') + return buf +} diff --git a/pkg/json/marshal.go b/pkg/json/marshal.go new file mode 100644 index 00000000..a22f9fd9 --- /dev/null +++ b/pkg/json/marshal.go @@ -0,0 +1,269 @@ +package json + +import ( + "encoding" + stdJSON "encoding/json" + + "fmt" + "net" + "net/url" + "reflect" + "strconv" + "time" + + "github.com/puzpuzpuz/xsync/v3" +) + +type marshalFunc func(v reflect.Value, buf []byte) []byte + +var ( + marshalFuncByKind map[reflect.Kind]marshalFunc + + marshalFuncsByType = newCacheMap[reflect.Type, marshalFunc]() + flattenFieldsCache = newCacheMap[reflect.Type, []*field]() + + nilValue = reflect.ValueOf(nil) +) + +func init() { + marshalFuncByKind = map[reflect.Kind]marshalFunc{ + reflect.String: appendString, + reflect.Bool: appendBool, + reflect.Int: appendInt, + reflect.Int8: appendInt, + reflect.Int16: appendInt, + reflect.Int32: appendInt, + reflect.Int64: appendInt, + reflect.Uint: appendUint, + reflect.Uint8: appendUint, + reflect.Uint16: appendUint, + reflect.Uint32: appendUint, + reflect.Uint64: appendUint, + reflect.Float32: appendFloat, + reflect.Float64: appendFloat, + reflect.Map: appendMap, + reflect.Slice: appendArray, + reflect.Array: appendArray, + reflect.Pointer: appendPtrInterface, + reflect.Interface: appendPtrInterface, + } + // pre-caching some frequently used types + marshalFuncsByType.Store(reflect.TypeFor[*url.URL](), appendStringer) + marshalFuncsByType.Store(reflect.TypeFor[net.IP](), appendStringer) + marshalFuncsByType.Store(reflect.TypeFor[*net.IPNet](), appendStringer) + marshalFuncsByType.Store(reflect.TypeFor[time.Time](), appendTime) + marshalFuncsByType.Store(reflect.TypeFor[time.Duration](), appendDuration) +} + +func newCacheMap[K comparable, V any]() *xsync.MapOf[K, V] { + return xsync.NewMapOf[K, V]( + xsync.WithGrowOnly(), + xsync.WithPresize(50), + ) +} + +func must(buf []byte, err error) []byte { + if err != nil { + panic(fmt.Errorf("custom json marshal error: %w", err)) + } + return buf +} + +func appendMarshal(v reflect.Value, buf []byte) []byte { + if v == nilValue { + return append(buf, "null"...) + } + kind := v.Kind() + if kind == reflect.Struct { + if res, ok := appendWithCachedFunc(v, buf); ok { + return res + } + return appendStruct(v, buf) + } + marshalFunc, ok := marshalFuncByKind[kind] + if !ok { + panic(fmt.Errorf("unsupported type: %s", v.Type())) + } + return marshalFunc(v, buf) +} + +func appendWithCachedFunc(v reflect.Value, buf []byte) (res []byte, ok bool) { + marshalFunc, ok := marshalFuncsByType.Load(v.Type()) + if ok { + return marshalFunc(v, buf), true + } + return nil, false +} + +func appendBool(v reflect.Value, buf []byte) []byte { + return strconv.AppendBool(buf, v.Bool()) +} + +func appendInt(v reflect.Value, buf []byte) []byte { + return strconv.AppendInt(buf, v.Int(), 10) +} + +func appendUint(v reflect.Value, buf []byte) []byte { + return strconv.AppendUint(buf, v.Uint(), 10) +} + +func appendFloat(v reflect.Value, buf []byte) []byte { + return strconv.AppendFloat(buf, v.Float(), 'f', 2, 64) +} + +func appendWithCustomMarshaler(v reflect.Value, buf []byte) (res []byte, ok bool) { + switch vv := v.Interface().(type) { + case Marshaler: + cacheMarshalFunc(v.Type(), appendWithMarshalTo) + return vv.MarshalJSONTo(buf), true + case fmt.Stringer: + cacheMarshalFunc(v.Type(), appendStringer) + return AppendString(buf, vv.String()), true + case stdJSON.Marshaler: + cacheMarshalFunc(v.Type(), appendStdJSONMarshaler) + return append(buf, must(vv.MarshalJSON())...), true + case encoding.BinaryAppender: + cacheMarshalFunc(v.Type(), appendBinaryAppender) + //FIXME: append escaped + return must(vv.AppendBinary(buf)), true + case encoding.TextAppender: + cacheMarshalFunc(v.Type(), appendTextAppender) + //FIXME: append escaped + return must(vv.AppendText(buf)), true + case encoding.TextMarshaler: + cacheMarshalFunc(v.Type(), appendTestMarshaler) + return AppendString(buf, must(vv.MarshalText())), true + case encoding.BinaryMarshaler: + cacheMarshalFunc(v.Type(), appendBinaryMarshaler) + return AppendString(buf, must(vv.MarshalBinary())), true + } + return nil, false +} + +func mustAppendWithCustomMarshaler(v reflect.Value, buf []byte) []byte { + res, ok := appendWithCustomMarshaler(v, buf) + if !ok { + panic(fmt.Errorf("tag %q used but no marshaler implemented: %s", tagUseMarshaler, v.Type())) + } + return res +} + +func appendKV(k reflect.Value, v reflect.Value, buf []byte) []byte { + buf = AppendString(buf, k.String()) + buf = append(buf, ':') + return appendMarshal(v, buf) +} + +func appendStruct(v reflect.Value, buf []byte) []byte { + if res, ok := appendWithCustomMarshaler(v, buf); ok { + return res + } + buf = append(buf, '{') + oldN := len(buf) + fields := flattenFields(v.Type()) + + for _, f := range fields { + cur := v.Field(f.index) + if f.omitEmpty && f.checkEmpty(cur) { + continue + } + if !f.hasInner { + buf = f.appendKV(cur, buf) + buf = append(buf, ',') + } else { + if f.isPtr { + cur = cur.Elem() + } + for _, inner := range f.inner { + buf = inner.appendKV(cur.Field(inner.index), buf) + buf = append(buf, ',') + } + } + } + + n := len(buf) + if oldN != n { + buf = buf[:n-1] + } + return append(buf, '}') +} + +func appendMap(v reflect.Value, buf []byte) []byte { + if v.Type().Key().Kind() != reflect.String { + panic(fmt.Errorf("map key must be string: %s", v.Type())) + } + buf = append(buf, '{') + i := 0 + oldN := len(buf) + iter := v.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + buf = appendKV(k, v, buf) + buf = append(buf, ',') + i++ + } + n := len(buf) + if oldN != n { + buf = buf[:n-1] + } + return append(buf, '}') +} + +func appendArray(v reflect.Value, buf []byte) []byte { + switch v.Type().Elem().Kind() { + case reflect.String: + return appendStringSlice(v, buf) + case reflect.Uint8: // byte + return appendBytesAsBase64(v, buf) + } + buf = append(buf, '[') + oldN := len(buf) + for i := range v.Len() { + buf = appendMarshal(v.Index(i), buf) + buf = append(buf, ',') + } + n := len(buf) + if oldN != n { + buf = buf[:n-1] + } + return append(buf, ']') +} + +func cacheMarshalFunc(t reflect.Type, marshalFunc marshalFunc) { + marshalFuncsByType.Store(t, marshalFunc) +} + +func appendPtrInterface(v reflect.Value, buf []byte) []byte { + return appendMarshal(v.Elem(), buf) +} + +func appendWithMarshalTo(v reflect.Value, buf []byte) []byte { + return v.Interface().(Marshaler).MarshalJSONTo(buf) +} + +func appendStringer(v reflect.Value, buf []byte) []byte { + return AppendString(buf, v.Interface().(fmt.Stringer).String()) +} + +func appendStdJSONMarshaler(v reflect.Value, buf []byte) []byte { + return append(buf, must(v.Interface().(stdJSON.Marshaler).MarshalJSON())...) +} + +func appendBinaryAppender(v reflect.Value, buf []byte) []byte { + //FIXME: append escaped + return must(v.Interface().(encoding.BinaryAppender).AppendBinary(buf)) +} + +func appendTextAppender(v reflect.Value, buf []byte) []byte { + //FIXME: append escaped + return must(v.Interface().(encoding.TextAppender).AppendText(buf)) +} + +func appendTestMarshaler(v reflect.Value, buf []byte) []byte { + return AppendString(buf, must(v.Interface().(encoding.TextMarshaler).MarshalText())) +} + +func appendBinaryMarshaler(v reflect.Value, buf []byte) []byte { + return AppendString(buf, must(v.Interface().(encoding.BinaryMarshaler).MarshalBinary())) +} diff --git a/pkg/json/marshal_test.go b/pkg/json/marshal_test.go new file mode 100644 index 00000000..02d15997 --- /dev/null +++ b/pkg/json/marshal_test.go @@ -0,0 +1,529 @@ +package json_test + +import ( + stdJSON "encoding/json" + "fmt" + "maps" + "reflect" + "runtime/debug" + "strconv" + "testing" + + "github.com/bytedance/sonic" + "github.com/stretchr/testify/require" + "github.com/yusing/go-proxy/internal/utils/strutils" + . "github.com/yusing/go-proxy/pkg/json" +) + +func init() { + debug.SetMemoryLimit(1024 * 1024) + debug.SetMaxStack(1024 * 1024) +} + +type testStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Score float64 `json:"score"` + Empty *struct { + Value string `json:"value,omitempty"` + } `json:"empty,omitempty"` +} + +type stringer struct { + testStruct +} + +func (s stringer) String() string { + return s.Name +} + +type customMarshaler struct { + Value string +} + +func (cm customMarshaler) MarshalJSONTo(buf []byte) []byte { + return append(buf, []byte(`{"custom":"`+cm.Value+`"}`)...) +} + +type jsonMarshaler struct { + Value string +} + +func (jm jsonMarshaler) MarshalJSON() ([]byte, error) { + return []byte(`{"json_marshaler":"` + jm.Value + `"}`), nil +} + +type withJSONTag struct { + Value string `json:"value"` +} + +type withJSONOmitEmpty struct { + Value string `json:"value,omitempty"` +} + +type withJSONStringTag struct { + Value int64 `json:"value,string"` +} + +type withJSONOmit struct { + Value string `json:"-"` +} + +type withJSONByteSize struct { + Value uint64 `json:"value,byte_size"` +} + +type withJSONUnixTime struct { + Value int64 `json:"value,unix_time"` +} + +type primitiveWithMarshaler int + +func (p primitiveWithMarshaler) MarshalJSONTo(buf []byte) []byte { + return fmt.Appendf(buf, `%q`, strconv.Itoa(int(p))) +} + +type withTagUseMarshaler struct { + Value primitiveWithMarshaler `json:"value,use_marshaler"` +} + +type Anonymous struct { + Value string `json:"value"` + Value2 int `json:"value2"` +} + +type withAnonymous struct { + Anonymous +} + +type withPointerAnonymous struct { + *Anonymous +} + +type selfReferencing struct { + Self *selfReferencing `json:"self"` +} + +var testData = map[string]any{ + "string": "test string", + "number": 42, + "float": 3.14159, + "bool": true, + "null_value": nil, + "array": []any{1, "2", 3.3, true, false, nil}, + "object": map[string]any{ + "nested": "value", + "count": 10, + }, +} + +func TestMarshal(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "string", + input: "test", + expected: `"test"`, + }, + { + name: "bool_true", + input: true, + expected: `true`, + }, + { + name: "bool_false", + input: false, + expected: `false`, + }, + { + name: "int", + input: 42, + expected: `42`, + }, + { + name: "uint", + input: uint(42), + expected: `42`, + }, + { + name: "float", + input: 3.14, + expected: `3.14`, + }, + { + name: "slice", + input: []int{1, 2, 3}, + expected: `[1,2,3]`, + }, + { + name: "array", + input: [3]int{4, 5, 6}, + expected: `[4,5,6]`, + }, + { + name: "slice_of_struct", + input: []testStruct{{Name: "John", Age: 30, Score: 8.5}, {Name: "Jane", Age: 25, Score: 9.5}}, + expected: `[{"name":"John","age":30,"score":8.50},{"name":"Jane","age":25,"score":9.50}]`, + }, + { + name: "slice_of_struct_pointer", + input: []*testStruct{{Name: "John", Age: 30, Score: 8.5}, {Name: "Jane", Age: 25, Score: 9.5}}, + expected: `[{"name":"John","age":30,"score":8.50},{"name":"Jane","age":25,"score":9.50}]`, + }, + { + name: "slice_of_map", + input: []map[string]any{{"key1": "value1"}, {"key2": "value2"}}, + expected: `[{"key1":"value1"},{"key2":"value2"}]`, + }, + { + name: "struct", + input: testStruct{Name: "John", Age: 30, Score: 8.5}, + expected: `{"name":"John","age":30,"score":8.50}`, + }, + { + name: "struct_pointer", + input: &testStruct{Name: "Jane", Age: 25, Score: 9.5}, + expected: `{"name":"Jane","age":25,"score":9.50}`, + }, + { + name: "byte_slice", + input: []byte("test"), + expected: `"dGVzdA=="`, + }, + { + name: "custom_marshaler", + input: customMarshaler{Value: "test"}, + expected: `{"custom":"test"}`, + }, + { + name: "custom_marshaler_pointer", + input: &customMarshaler{Value: "test"}, + expected: `{"custom":"test"}`, + }, + { + name: "json_marshaler", + input: jsonMarshaler{Value: "test"}, + expected: `{"json_marshaler":"test"}`, + }, + { + name: "json_marshaler_pointer", + input: &jsonMarshaler{Value: "test"}, + expected: `{"json_marshaler":"test"}`, + }, + { + name: "stringer", + input: stringer{testStruct: testStruct{Name: "Bob", Age: 20, Score: 9.5}}, + expected: `"Bob"`, + }, + { + name: "stringer_pointer", + input: &stringer{testStruct: testStruct{Name: "Bob", Age: 20, Score: 9.5}}, + expected: `"Bob"`, + }, + { + name: "with_json_tag", + input: withJSONTag{Value: "test"}, + expected: `{"value":"test"}`, + }, + { + name: "with_json_tag_pointer", + input: &withJSONTag{Value: "test"}, + expected: `{"value":"test"}`, + }, + { + name: "with_json_omit_empty", + input: withJSONOmitEmpty{Value: "test"}, + expected: `{"value":"test"}`, + }, + { + name: "with_json_omit_empty_pointer", + input: &withJSONOmitEmpty{Value: "test"}, + expected: `{"value":"test"}`, + }, + { + name: "with_json_omit_empty_empty", + input: withJSONOmitEmpty{}, + expected: `{}`, + }, + { + name: "with_json_omit_empty_pointer_empty", + input: &withJSONOmitEmpty{}, + expected: `{}`, + }, + { + name: "with_json_omit", + input: withJSONOmit{Value: "test"}, + expected: `{}`, + }, + { + name: "with_json_omit_pointer", + input: &withJSONOmit{Value: "test"}, + expected: `{}`, + }, + { + name: "with_json_string_tag", + input: withJSONStringTag{Value: 1234567890}, + expected: `{"value":"1234567890"}`, + }, + { + name: "with_json_string_tag_pointer", + input: &withJSONStringTag{Value: 1234567890}, + expected: `{"value":"1234567890"}`, + }, + { + name: "with_json_byte_size", + input: withJSONByteSize{Value: 1024}, + expected: fmt.Sprintf(`{"value":"%s"}`, strutils.FormatByteSize(1024)), + }, + { + name: "with_json_byte_size_pointer", + input: &withJSONByteSize{Value: 1024}, + expected: fmt.Sprintf(`{"value":"%s"}`, strutils.FormatByteSize(1024)), + }, + { + name: "with_json_unix_time", + input: withJSONUnixTime{Value: 1713033600}, + expected: fmt.Sprintf(`{"value":"%s"}`, strutils.FormatUnixTime(1713033600)), + }, + { + name: "with_json_unix_time_pointer", + input: &withJSONUnixTime{Value: 1713033600}, + expected: fmt.Sprintf(`{"value":"%s"}`, strutils.FormatUnixTime(1713033600)), + }, + { + name: "with_tag_use_marshaler", + input: withTagUseMarshaler{Value: primitiveWithMarshaler(42)}, + expected: `{"value":"42"}`, + }, + { + name: "with_tag_use_marshaler_pointer", + input: &withTagUseMarshaler{Value: primitiveWithMarshaler(42)}, + expected: `{"value":"42"}`, + }, + { + name: "with_anonymous", + input: withAnonymous{Anonymous: Anonymous{Value: "test", Value2: 1}}, + expected: `{"value":"test","value2":1}`, + }, + { + name: "with_anonymous_pointer", + input: &withAnonymous{Anonymous: Anonymous{Value: "test", Value2: 1}}, + expected: `{"value":"test","value2":1}`, + }, + { + name: "with_pointer_anonymous", + input: &withPointerAnonymous{Anonymous: &Anonymous{Value: "test", Value2: 1}}, + expected: `{"value":"test","value2":1}`, + }, + { + name: "with_pointer_anonymous_nil", + input: &withPointerAnonymous{Anonymous: nil}, + expected: `{}`, + }, + { + // NOTE: not fixing this until needed + // GoDoxy does not have any type with exported self-referencing fields + name: "self_referencing", + input: func() *selfReferencing { + s := &selfReferencing{} + s.Self = s + return s + }(), + expected: `{"self":{"self":{"self":{"self":null}}}}`, + }, + { + name: "nil", + input: nil, + expected: `null`, + }, + { + name: "nil_pointer", + input: (*int)(nil), + expected: `null`, + }, + { + name: "nil_slice", + input: []int(nil), + expected: `[]`, + }, + { + name: "nil_map", + input: map[string]int(nil), + expected: `{}`, + }, + { + name: "nil_map_pointer", + input: (*map[string]int)(nil), + expected: `null`, + }, + { + name: "nil_slice_pointer", + input: (*[]int)(nil), + expected: `null`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, _ := Marshal(tt.input) + require.Equal(t, tt.expected, string(result)) + }) + } + + mapTests := []struct { + name string + input any + }{ + { + name: "map", + input: map[string]int{"one": 1, "two": 2}, + }, + { + name: "map_of_struct", + input: map[string]testStruct{"one": {Name: "John", Age: 30, Score: 8.5}, "two": {Name: "Jane", Age: 25, Score: 9.5}}, + }, + { + name: "complex_map", + input: testData, + }, + } + + for _, tt := range mapTests { + t.Run(tt.name, func(t *testing.T) { + result, _ := Marshal(tt.input) + verify := reflect.MakeMap(reflect.TypeOf(tt.input)) + if err := stdJSON.Unmarshal(result, &verify); err != nil { + t.Fatalf("Unmarshal(%v) error: %v", result, err) + } + iter := verify.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + vv := reflect.ValueOf(tt.input).MapIndex(k).Interface() + if !v.Equal(reflect.ValueOf(vv)) { + t.Errorf("Marshal([%s]) = %v, want %v", k, v, vv) + } + } + }) + } +} + +func TestMapAndMapSlice(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "Map", + input: Map[string]{"key1": "value1", "key2": "value2"}, + expected: `{"key1":"value1","key2":"value2"}`, + }, + { + name: "MapSlice", + input: MapSlice[string]{{"key1": "value1"}, {"key2": "value2"}}, + expected: `[{"key1":"value1"},{"key2":"value2"}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, _ := Marshal(tt.input) + if string(result) != tt.expected { + t.Errorf("Marshal(%v) = %s, want %s", tt.input, string(result), tt.expected) + } + }) + } +} + +func TestMarshalSyntacticEquivalence(t *testing.T) { + testData := []any{ + "test\r\nstring", + 42, + 3.14, + true, + nil, + []int{1, 2, 3, 4, 5}, + map[string]any{ + "nested": "value", + "count": 10, + "bytes": []byte("test"), + "a": "a\x1b[31m", + }, + testStruct{Name: "Test", Age: 30, Score: 9.8}, + } + + for i, data := range testData { + custom, _ := Marshal(data) + stdlib, err := stdJSON.Marshal(data) + if err != nil { + t.Fatalf("Test %d: Standard Marshal error: %v", i, err) + } + + t.Logf("custom: %s\n", custom) + t.Logf("stdlib: %s\n", stdlib) + + // Unmarshal both into maps to compare structure equivalence + var customMap, stdlibMap any + if err := stdJSON.Unmarshal(custom, &customMap); err != nil { + t.Fatalf("Test %d: Unmarshal custom error: %v", i, err) + } + if err := stdJSON.Unmarshal(stdlib, &stdlibMap); err != nil { + t.Fatalf("Test %d: Unmarshal stdlib error: %v", i, err) + } + + if !reflect.DeepEqual(customMap, stdlibMap) { + t.Errorf("Test %d: Marshal output not equivalent.\nCustom: %s\nStdLib: %s", + i, string(custom), string(stdlib)) + } + } +} + +func BenchmarkMarshalNoStructStdLib(b *testing.B) { + b.Run("StdLib", func(b *testing.B) { + for b.Loop() { + _, _ = stdJSON.Marshal(testData) + } + }) + + b.Run("Sonic", func(b *testing.B) { + for b.Loop() { + _, _ = sonic.Marshal(testData) + } + }) + + b.Run("Custom", func(b *testing.B) { + for b.Loop() { + _, _ = Marshal(testData) + } + }) +} + +func BenchmarkMarshalStruct(b *testing.B) { + withStruct := maps.Clone(testData) + withStruct["struct1"] = withAnonymous{Anonymous: Anonymous{Value: "one", Value2: 1}} + withStruct["struct2"] = &withPointerAnonymous{Anonymous: &Anonymous{Value: "two", Value2: 2}} + withStruct["struct3"] = &testStruct{Name: "three", Age: 30, Score: 9.8} + b.ResetTimer() + + b.Run("StdLib", func(b *testing.B) { + for b.Loop() { + _, _ = stdJSON.Marshal(withStruct) + } + }) + + b.Run("Sonic", func(b *testing.B) { + for b.Loop() { + _, _ = sonic.Marshal(withStruct) + } + }) + + b.Run("Custom", func(b *testing.B) { + for b.Loop() { + _, _ = Marshal(withStruct) + } + }) +} diff --git a/pkg/json/special.go b/pkg/json/special.go new file mode 100644 index 00000000..acf43cdc --- /dev/null +++ b/pkg/json/special.go @@ -0,0 +1,60 @@ +package json + +import ( + "encoding" + "fmt" + "reflect" + "time" + + "github.com/yusing/go-proxy/internal/utils/strutils" +) + +func isIntFloat(t reflect.Kind) bool { + return t >= reflect.Bool && t <= reflect.Float64 +} + +func appendStringRepr(v reflect.Value, buf []byte) []byte { // for json tag `string` + kind := v.Kind() + if isIntFloat(kind) { + marshalFunc, _ := marshalFuncByKind[kind] + buf = append(buf, '"') + buf = marshalFunc(v, buf) + buf = append(buf, '"') + return buf + } + switch vv := v.Interface().(type) { + case fmt.Stringer: + buf = AppendString(buf, vv.String()) + case encoding.TextMarshaler: + buf = append(buf, must(vv.MarshalText())...) + case encoding.TextAppender: + buf = must(vv.AppendText(buf)) + default: + panic(fmt.Errorf("tag %q used but type is non-stringable: %s", tagString, v.Type())) + } + return buf +} + +func appendTime(v reflect.Value, buf []byte) []byte { + buf = append(buf, '"') + buf = strutils.AppendTime(v.Interface().(time.Time), buf) + return append(buf, '"') +} + +func appendDuration(v reflect.Value, buf []byte) []byte { + buf = append(buf, '"') + buf = strutils.AppendDuration(v.Interface().(time.Duration), buf) + return append(buf, '"') +} + +func appendByteSize(v reflect.Value, buf []byte) []byte { + buf = append(buf, '"') + buf = strutils.AppendByteSize(v.Interface().(uint64), buf) + return append(buf, '"') +} + +func appendUnixTime(v reflect.Value, buf []byte) []byte { + buf = append(buf, '"') + buf = strutils.AppendTime(time.Unix(v.Interface().(int64), 0), buf) + return append(buf, '"') +} diff --git a/pkg/json/string.go b/pkg/json/string.go new file mode 100644 index 00000000..30d6b301 --- /dev/null +++ b/pkg/json/string.go @@ -0,0 +1,334 @@ +package json + +import ( + "encoding/base64" + "reflect" + "unicode/utf8" +) + +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// safeSet, htmlSafeSet, hex and AppendString are copied from encoding/json. + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML