mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
feat: custom json marshaler implementation
This commit is contained in:
parent
75fd8d1fdc
commit
cdfc9d553b
10 changed files with 1479 additions and 0 deletions
55
pkg/json/check_empty.go
Normal file
55
pkg/json/check_empty.go
Normal file
|
@ -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
|
||||
}
|
17
pkg/json/encoder.go
Normal file
17
pkg/json/encoder.go
Normal file
|
@ -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
|
||||
}
|
70
pkg/json/json.go
Normal file
70
pkg/json/json.go
Normal file
|
@ -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...)
|
||||
}
|
24
pkg/json/map.go
Normal file
24
pkg/json/map.go
Normal file
|
@ -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
|
||||
}
|
18
pkg/json/map_slice.go
Normal file
18
pkg/json/map_slice.go
Normal file
|
@ -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
|
||||
}
|
269
pkg/json/marshal.go
Normal file
269
pkg/json/marshal.go
Normal file
|
@ -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()))
|
||||
}
|
529
pkg/json/marshal_test.go
Normal file
529
pkg/json/marshal_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
60
pkg/json/special.go
Normal file
60
pkg/json/special.go
Normal file
|
@ -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, '"')
|
||||
}
|
334
pkg/json/string.go
Normal file
334
pkg/json/string.go
Normal file
|
@ -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 <script> tags, without any additional escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), the backslash character ("\"), HTML opening and closing
|
||||
// tags ("<" and ">"), and the ampersand ("&").
|
||||
var htmlSafeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': false,
|
||||
'\'': 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,
|
||||
'<': false,
|
||||
'=': true,
|
||||
'>': false,
|
||||
'?': 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,
|
||||
}
|
||||
|
||||
const hex = "0123456789abcdef"
|
||||
|
||||
// String returns a quoted, escaped string or []byte.
|
||||
func String[Bytes []byte | string](src Bytes) string {
|
||||
return string(AppendString(nil, src))
|
||||
}
|
||||
|
||||
// AppendString append a quoted, escaped string or []byte to dst.
|
||||
func AppendString[Bytes []byte | string](dst []byte, src Bytes) []byte {
|
||||
dst = append(dst, '"')
|
||||
start := 0
|
||||
for i := 0; i < len(src); {
|
||||
if b := src[i]; b < utf8.RuneSelf {
|
||||
if htmlSafeSet[b] || safeSet[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
dst = append(dst, src[start:i]...)
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
dst = append(dst, '\\', b)
|
||||
case '\b':
|
||||
dst = append(dst, '\\', 'b')
|
||||
case '\f':
|
||||
dst = append(dst, '\\', 'f')
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \b, \f, \n, \r and \t.
|
||||
// If escapeHTML is set, it also escapes <, >, and &
|
||||
// because they can lead to security holes when
|
||||
// user-controlled strings are rendered into JSON
|
||||
// and served to some browsers.
|
||||
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// TODO(https://go.dev/issue/56948): Use generic utf8 functionality.
|
||||
// For now, cast only a small portion of byte slices to a string
|
||||
// so that it can be stack allocated. This slows down []byte slightly
|
||||
// due to the extra copy, but keeps string performance roughly the same.
|
||||
n := len(src) - i
|
||||
if n > utf8.UTFMax {
|
||||
n = utf8.UTFMax
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(string(src[i : i+n]))
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
dst = append(dst, src[start:i]...)
|
||||
dst = append(dst, `\ufffd`...)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// U+2028 is LINE SEPARATOR.
|
||||
// U+2029 is PARAGRAPH SEPARATOR.
|
||||
// They are both technically valid characters in JSON strings,
|
||||
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||
// and can lead to security holes there. It is valid JSON to
|
||||
// escape them, so we do so unconditionally.
|
||||
// See https://en.wikipedia.org/wiki/JSON#Safety.
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
dst = append(dst, src[start:i]...)
|
||||
dst = append(dst, '\\', 'u', '2', '0', '2', hex[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
dst = append(dst, src[start:]...)
|
||||
dst = append(dst, '"')
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendString(v reflect.Value, buf []byte) []byte {
|
||||
return AppendString(buf, v.String())
|
||||
}
|
||||
|
||||
var (
|
||||
typeStringSlice = reflect.TypeOf([]string{})
|
||||
typeBytes = reflect.TypeOf([]byte{})
|
||||
)
|
||||
|
||||
func appendStringSlice(v reflect.Value, buf []byte) []byte {
|
||||
str := v.Convert(typeStringSlice).Interface().([]string)
|
||||
buf = append(buf, '[')
|
||||
oldN := len(buf)
|
||||
for _, s := range str {
|
||||
buf = AppendString(buf, s)
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
n := len(buf)
|
||||
if oldN != n {
|
||||
buf = buf[:n-1]
|
||||
}
|
||||
return append(buf, ']')
|
||||
}
|
||||
|
||||
func appendBytesAsBase64(v reflect.Value, buf []byte) []byte {
|
||||
buf = append(buf, '"')
|
||||
buf = base64.StdEncoding.AppendEncode(buf, v.Convert(typeBytes).Interface().([]byte))
|
||||
return append(buf, '"')
|
||||
}
|
103
pkg/json/struct.go
Normal file
103
pkg/json/struct.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type field struct {
|
||||
quotedNameWithCol string
|
||||
|
||||
index int
|
||||
inner []*field
|
||||
hasInner bool
|
||||
isPtr bool
|
||||
omitEmpty bool // true when json tag is "omitempty" or field is pointer to anonymous struct
|
||||
checkEmpty checkEmptyFunc
|
||||
marshal marshalFunc
|
||||
}
|
||||
|
||||
const (
|
||||
tagOmitEmpty = "omitempty"
|
||||
tagString = "string" // https://pkg.go.dev/github.com/yusing/go-proxy/pkg/json#Marshal
|
||||
tagByteSize = "byte_size"
|
||||
tagUnixTime = "unix_time"
|
||||
tagUseMarshaler = "use_marshaler"
|
||||
)
|
||||
|
||||
func flattenFields(t reflect.Type) []*field {
|
||||
fields, ok := flattenFieldsCache.Load(t)
|
||||
if ok {
|
||||
return fields
|
||||
}
|
||||
|
||||
fields = make([]*field, 0, t.NumField())
|
||||
for i := range t.NumField() {
|
||||
structField := t.Field(i)
|
||||
if !structField.IsExported() {
|
||||
continue
|
||||
}
|
||||
kind := structField.Type.Kind()
|
||||
f := &field{
|
||||
index: i,
|
||||
isPtr: kind == reflect.Pointer,
|
||||
}
|
||||
jsonTag, ok := structField.Tag.Lookup("json")
|
||||
if ok {
|
||||
if jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
parts := strutils.SplitComma(jsonTag)
|
||||
if len(parts) > 1 {
|
||||
switch parts[1] {
|
||||
case tagOmitEmpty:
|
||||
f.omitEmpty = true
|
||||
case tagString:
|
||||
f.marshal = appendStringRepr
|
||||
case tagByteSize:
|
||||
f.marshal = appendByteSize
|
||||
case tagUnixTime:
|
||||
f.marshal = appendUnixTime
|
||||
case tagUseMarshaler:
|
||||
f.marshal = mustAppendWithCustomMarshaler
|
||||
default:
|
||||
panic(fmt.Errorf("unknown json tag: %s", parts[1]))
|
||||
}
|
||||
f.quotedNameWithCol = parts[0]
|
||||
} else {
|
||||
f.quotedNameWithCol = jsonTag
|
||||
}
|
||||
}
|
||||
|
||||
if f.quotedNameWithCol == "" { // e.g. json:",omitempty"
|
||||
f.quotedNameWithCol = structField.Name
|
||||
}
|
||||
if f.marshal == nil {
|
||||
f.marshal = appendMarshal
|
||||
}
|
||||
if structField.Anonymous {
|
||||
if structField.Type.Kind() == reflect.Pointer {
|
||||
f.inner = flattenFields(structField.Type.Elem())
|
||||
f.omitEmpty = true
|
||||
} else {
|
||||
f.inner = flattenFields(structField.Type)
|
||||
}
|
||||
f.hasInner = len(f.inner) > 0
|
||||
}
|
||||
fields = append(fields, f)
|
||||
if f.omitEmpty {
|
||||
f.checkEmpty = checkEmptyFuncs[kind]
|
||||
}
|
||||
f.quotedNameWithCol = strconv.Quote(f.quotedNameWithCol) + ":"
|
||||
}
|
||||
|
||||
flattenFieldsCache.Store(t, fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
func (f *field) appendKV(v reflect.Value, buf []byte) []byte {
|
||||
return f.marshal(v, append(buf, f.quotedNameWithCol...))
|
||||
}
|
Loading…
Add table
Reference in a new issue