From 4e6336a38619ab4661b895ccd4e3c04bb1ceccbb Mon Sep 17 00:00:00 2001 From: Yi Duan Date: Mon, 29 Apr 2024 13:15:40 +0800 Subject: [PATCH] feat:(thrift) nested thrift type constructor API (#41) * feat:(thrift) support construction of nested-typed nodes * tmp * downgrade testify * add example * return error for unsupported type * add `NewNodeAny()` * resolve comments * rebase --- thrift/binary.go | 389 +++++++++++++++++++++++++++++++++ thrift/binary_test.go | 80 +++++++ thrift/generic/example_test.go | 39 ++++ thrift/generic/node.go | 145 +++++++++--- thrift/generic/node_test.go | 90 ++++++++ thrift/generic/option.go | 7 + 6 files changed, 717 insertions(+), 33 deletions(-) create mode 100644 thrift/generic/node_test.go diff --git a/thrift/binary.go b/thrift/binary.go index 27f920d3..f6246125 100644 --- a/thrift/binary.go +++ b/thrift/binary.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "math" + "reflect" "strconv" "strings" "sync" @@ -1292,6 +1293,390 @@ func (p *BinaryProtocol) DecodeText(val string, desc *TypeDescriptor, disallowUn } } +// GoType2ThriftType a go primitive type to a thrift type +// The rules is: +// - bool -> BOOL +// - byte/int8 -> BYTE +// - int16 -> I16 +// - int32 -> I32 +// - int64/int -> I64 +// - int -> I64 +// - float64/float32 -> DOUBLE +// - string/[]byte -> STRING +// - []interface{} -> LIST +// - map[FieldID]interface{} -> STRUCT +// - map[(int|string|interface{})]interface{} -> MAP +func GoType2ThriftType(val interface{}) (Type, error) { + _, ok := val.(map[FieldID]interface{}) + if ok { + return STRUCT, nil + } + _, ok = val.([]byte) + if ok { + return STRING, nil + } + switch reflect.TypeOf(val).Kind() { + case reflect.Bool: + return BOOL, nil + case reflect.Int8, reflect.Uint8: + return BYTE, nil + case reflect.Int16, reflect.Uint16: + return I16, nil + case reflect.Int32, reflect.Uint32: + return I32, nil + case reflect.Int64, reflect.Uint64, reflect.Int, reflect.Uint: + return I64, nil + case reflect.Float64: + return DOUBLE, nil + case reflect.String: + return STRING, nil + case reflect.Slice: + return LIST, nil + case reflect.Map: + return MAP, nil + case reflect.Struct: + return STRUCT, nil + case reflect.Ptr: + return GoType2ThriftType(reflect.ValueOf(val).Elem().Interface()) + default: + return STOP, errUnsupportedType + } +} + +// ReadAny reads a thrift value from buffer and convert it to go primitive type +// It basicallly obeys rules in `GoType2ThriftType`. +// Specially, +// - For INT(8/16/32/64) type, the return type is corresponding int8/int16/int32/int64 by default; +// - For MAP type, the output key type could be string, int or interface{}, depends on the input key's thrift type. +// - for STRUCT type, the return type is map[thrift.FieldID]interface{}. +func (p *BinaryProtocol) ReadAny(typ Type, strAsBinary bool, byteAsInt8 bool) (interface{}, error) { + switch typ { + case BOOL: + return p.ReadBool() + case BYTE: + if byteAsInt8 { + n, e := p.ReadByte() + return int8(n), e + } + return p.ReadByte() + case I16: + return p.ReadI16() + case I32: + return p.ReadI32() + case I64: + return p.ReadI64() + case DOUBLE: + return p.ReadDouble() + case STRING: + if strAsBinary { + return p.ReadBinary(false) + } + return p.ReadString(false) + case LIST, SET: + elemType, size, e := p.ReadListBegin() + if e != nil { + return nil, e + } + ret := make([]interface{}, 0, size) + for i := 0; i < size; i++ { + v, e := p.ReadAny(elemType, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + ret = append(ret, v) + } + return ret, p.ReadListEnd() + case MAP: + keyType, valueType, size, e := p.ReadMapBegin() + if e != nil { + return nil, e + } + if keyType == STRING { + ret := make(map[string]interface{}, size) + for i := 0; i < size; i++ { + k, e := p.ReadString(false) + if e != nil { + return nil, e + } + v, e := p.ReadAny(valueType, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + ret[k] = v + } + return ret, p.ReadMapEnd() + } else if keyType.IsInt() { + ret := make(map[int]interface{}, size) + for i := 0; i < size; i++ { + k, e := p.ReadInt(keyType) + if e != nil { + return nil, e + } + v, e := p.ReadAny(valueType, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + ret[k] = v + } + return ret, p.ReadMapEnd() + } else { + m := make(map[interface{}]interface{}, size) + for i := 0; i < size; i++ { + k, e := p.ReadAny(keyType, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + v, e := p.ReadAny(valueType, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + switch x := k.(type) { + case map[string]interface{}: + m[&x] = v + case map[int]interface{}: + m[&x] = v + case map[interface{}]interface{}: + m[&x] = v + case []interface{}: + m[&x] = v + case map[FieldID]interface{}: + m[&x] = v + default: + m[k] = v + } + } + return m, p.ReadMapEnd() + } + case STRUCT: + ret := make(map[FieldID]interface{}) + for { + _, typ, id, err := p.ReadFieldBegin() + if err != nil { + return nil, err + } + if typ == STOP { + return ret, nil + } + v, e := p.ReadAny(typ, strAsBinary, byteAsInt8) + if e != nil { + return nil, e + } + ret[id] = v + } + default: + return nil, errUnsupportedType + } +} + +// WriteAny write any go primitive type to thrift data, and return top level thrift type +// It basically obeys rules in `GoType2ThriftType`. +// Specially, +// - for MAP type, the key type should be string or int8/int16/int32/int64/int or interface{}. +// - for STRUCT type, the val type should be map[thrift.FieldID]interface{}. +func (p *BinaryProtocol) WriteAny(val interface{}, sliceAsSet bool) (Type, error) { + switch v := val.(type) { + case bool: + return BOOL, p.WriteBool(v) + case byte: + return BYTE, p.WriteByte(v) + case int8: + return BYTE, p.WriteByte(byte(v)) + case int16: + return I16, p.WriteI16(v) + case int32: + return I32, p.WriteI32(v) + case int64: + return I64, p.WriteI64(v) + case int: + return I64, p.WriteI64(int64(v)) + case float64: + return DOUBLE, p.WriteDouble(v) + case float32: + return DOUBLE, p.WriteDouble(float64(v)) + case string: + return STRING, p.WriteString(v) + case []byte: + return STRING, p.WriteBinary(v) + case []interface{}: + if len(v) == 0 { + return 0, fmt.Errorf("empty []interface is not supported") + } + et, e := GoType2ThriftType(v[0]) + if e != nil { + return 0, e + } + if sliceAsSet { + e = p.WriteSetBegin(et, len(v)) + if e != nil { + return 0, e + } + } else { + e = p.WriteListBegin(et, len(v)) + if e != nil { + return 0, e + } + } + for _, vv := range v { + if _, e := p.WriteAny(vv, sliceAsSet); e != nil { + return 0, e + } + } + return LIST, p.WriteListEnd() + case map[string]interface{}: + if len(v) == 0 { + return 0, fmt.Errorf("empty map[string]interface is not supported") + } + var firstVal interface{} + for _, vv := range v { + firstVal = vv + break + } + et, e := GoType2ThriftType(firstVal) + if e != nil { + return 0, e + } + e = p.WriteMapBegin(STRING, et, len(v)) + if e != nil { + return 0, e + } + for k, vv := range v { + if e := p.WriteString(k); e != nil { + return 0, e + } + if _, e := p.WriteAny(vv, sliceAsSet); e != nil { + return 0, e + } + } + return MAP, p.WriteMapEnd() + case map[byte]interface{}, map[int]interface{}, map[int8]interface{}, map[int16]interface{}, map[int32]interface{}, map[int64]interface{}: + vr := reflect.ValueOf(v) + if vr.Len() == 0 { + return 0, fmt.Errorf("empty map[int]interface{} is not supported") + } + it := vr.MapRange() + it.Next() + firstKey := it.Key().Interface() + firstVal := it.Value().Interface() + kt, e := GoType2ThriftType(firstKey) + if e != nil { + return 0, e + } + et, e := GoType2ThriftType(firstVal) + if e != nil { + return 0, e + } + e = p.WriteMapBegin(kt, et, vr.Len()) + if e != nil { + return 0, e + } + if _, e := p.WriteAny(firstKey, sliceAsSet); e != nil { + return 0, e + } + if _, e := p.WriteAny(firstVal, sliceAsSet); e != nil { + return 0, e + } + for it.Next() { + if _, e := p.WriteAny(it.Key().Interface(), sliceAsSet); e != nil { + return 0, e + } + if _, e := p.WriteAny(it.Value().Interface(), sliceAsSet); e != nil { + return 0, e + } + } + return MAP, p.WriteMapEnd() + case map[interface{}]interface{}: + if len(v) == 0 { + return 0, fmt.Errorf("empty map[int]interface{} is not supported") + } + var firstVal, firstKey interface{} + for kk, vv := range v { + firstVal = vv + firstKey = kk + break + } + kt, e := GoType2ThriftType(firstKey) + if e != nil { + return 0, e + } + et, e := GoType2ThriftType(firstVal) + if e != nil { + return 0, e + } + e = p.WriteMapBegin(kt, et, len(v)) + if e != nil { + return 0, e + } + for k, vv := range v { + switch kt := k.(type) { + case *map[string]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[int]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[int8]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[int16]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[int32]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[int64]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[FieldID]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *map[interface{}]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + case *[]interface{}: + if _, err := p.WriteAny(*kt, sliceAsSet); err != nil { + return 0, err + } + default: + if _, err := p.WriteAny(k, sliceAsSet); err != nil { + return 0, err + } + } + if _, e := p.WriteAny(vv, sliceAsSet); e != nil { + return 0, e + } + } + return MAP, p.WriteMapEnd() + case map[FieldID]interface{}: + e := p.WriteStructBegin("") + if e != nil { + return 0, e + } + for k, vv := range v { + ft, e := GoType2ThriftType(vv) + if e != nil { + return 0, e + } + if e := p.WriteFieldBegin("", ft, k); e != nil { + return 0, e + } + if _, e := p.WriteAny(vv, sliceAsSet); e != nil { + return 0, e + } + } + return STRUCT, p.WriteFieldStop() + default: + return 0, errUnsupportedType + } +} + // WriteAnyWithDesc explain desc and val and write them into buffer // - LIST/SET will be converted from []interface{} // - MAP will be converted from map[string]interface{} or map[int]interface{} @@ -1560,6 +1945,10 @@ func (p *BinaryProtocol) WriteAnyWithDesc(desc *TypeDescriptor, val interface{}, if err := p.WriteAnyWithDesc(desc.Key(), *kt, cast, disallowUnknown, useFieldName); err != nil { return err } + default: + if err := p.WriteAnyWithDesc(desc.Key(), k, cast, disallowUnknown, useFieldName); err != nil { + return err + } } if e := p.WriteAnyWithDesc(desc.Elem(), v, cast, disallowUnknown, useFieldName); e != nil { return e diff --git a/thrift/binary_test.go b/thrift/binary_test.go index e54a0431..99262c01 100644 --- a/thrift/binary_test.go +++ b/thrift/binary_test.go @@ -23,8 +23,11 @@ import ( "os" "runtime" "runtime/debug" + "strings" "testing" "time" + + "github.com/stretchr/testify/require" ) var ( @@ -91,3 +94,80 @@ func TestBinaryProtocol_ReadAnyWithDesc(t *testing.T) { } fmt.Printf("%#v", v) } + +func TestBinaryProtocol_WriteAny_ReadAny(t *testing.T) { + type args struct { + val interface{} + sliceAsSet bool + strAsBinary bool + byteAsInt8 bool + } + tests := []struct { + name string + args args + wantErr bool + want interface{} + }{ + {"bool", args{true, false, false, false}, false, true}, + {"byte", args{byte(1), false, false, false}, false, byte(1)}, + {"byte", args{byte(1), false, false, true}, false, int8(1)}, + {"i16", args{int16(1), false, false, false}, false, int16(1)}, + {"i32", args{int32(1), false, false, false}, false, int32(1)}, + {"i64", args{int64(1), false, false, false}, false, int64(1)}, + {"int", args{1, false, false, false}, false, int64(1)}, + {"f32", args{float32(1.0), false, false, false}, false, float64(1.0)}, + {"f64", args{1.0, false, false, false}, false, float64(1.0)}, + {"string", args{"1", false, false, false}, false, "1"}, + {"string2binary", args{"1", false, true, false}, false, []byte{'1'}}, + {"binary2string", args{[]byte{1}, false, false, false}, false, string("\x01")}, + {"binary2binary", args{[]byte{1}, false, true, false}, false, []byte{1}}, + {"list", args{[]interface{}{int32(1)}, false, false, false}, false, []interface{}{int32(1)}}, + {"set", args{[]interface{}{int64(1)}, true, false, false}, false, []interface{}{int64(1)}}, + {"int map", args{map[int]interface{}{1: byte(1)}, false, false, false}, false, map[int]interface{}{1: byte(1)}}, + {"int map error", args{map[int64]interface{}{1: byte(1)}, false, false, true}, false, map[int]interface{}{1: int8(1)}}, + {"int map empty", args{map[int64]interface{}{}, false, false, false}, true, nil}, + {"string map", args{map[string]interface{}{"1": "1"}, false, false, true}, false, map[string]interface{}{"1": "1"}}, + {"string map error", args{map[string]interface{}{"1": []int{1}}, false, false, true}, true, nil}, + {"string map empty", args{map[string]interface{}{}, false, false, true}, true, nil}, + {"any map", args{map[interface{}]interface{}{1.1: "1"}, false, false, false}, false, map[interface{}]interface{}{1.1: "1"}}, + {"any map + key list", args{map[interface{}]interface{}{&[]interface{}{1}: "1"}, false, false, false}, false, map[interface{}]interface{}{&[]interface{}{int64(1)}: "1"}}, + {"any map + val list", args{map[interface{}]interface{}{1.1: []interface{}{"1"}}, false, true, false}, false, map[interface{}]interface{}{1.1: []interface{}{[]byte{'1'}}}}, + {"any map empty", args{map[interface{}]interface{}{}, false, false, false}, true, nil}, + {"struct", args{map[FieldID]interface{}{FieldID(1): 1.1}, false, false, false}, false, map[FieldID]interface{}{FieldID(1): 1.1}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + println("case:", tt.name) + p := &BinaryProtocol{} + typ, err := p.WriteAny(tt.args.val, tt.args.sliceAsSet) + if (err != nil) != tt.wantErr { + t.Fatalf("BinaryProtocol.WriteAny() error = %v, wantErr %v", err, tt.wantErr) + } + fmt.Printf("buf:%+v\n", p.RawBuf()) + got, err := p.ReadAny(typ, tt.args.strAsBinary, tt.args.byteAsInt8) + if (err != nil) != tt.wantErr { + t.Fatalf("BinaryProtocol.ReadAny() error = %v, wantErr %v", err, tt.wantErr) + } + fmt.Printf("got:%#v\n", got) + if strings.Contains(tt.name, "any map + key") { + em := tt.want.(map[interface{}]interface{}) + gm := got.(map[interface{}]interface{}) + require.Equal(t, len(em), len(gm)) + var firstK, firstV interface{} + for k, v := range em { + firstK, firstV = k, v + break + } + var firstKgot, firstVgot interface{} + for k, v := range gm { + firstKgot, firstVgot = k, v + break + } + require.Equal(t, firstK, firstKgot) + require.Equal(t, firstV, firstVgot) + } else { + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/thrift/generic/example_test.go b/thrift/generic/example_test.go index 83d88cee..41b76438 100644 --- a/thrift/generic/example_test.go +++ b/thrift/generic/example_test.go @@ -2,6 +2,7 @@ package generic import ( "fmt" + "reflect" "github.com/cloudwego/dynamicgo/thrift" ) @@ -10,6 +11,44 @@ var opts = &Options{ UseNativeSkip: true, } +func ExampleNewTypedNode() { + // make a map> node + ret := NewTypedNode(thrift.MAP, thrift.LIST, thrift.STRING, PathNode{ + Path: NewPathStrKey("1"), + Node: NewNodeList([]interface{}{int32(1), int32(2)}), + }) + + // print raw data + fmt.Printf("buf:%+v\n", ret.Raw()) + + // print interface + val, err := ret.Interface(opts) + fmt.Printf("val:%#v\n", val) + if err != nil { + panic(err) + } + if !reflect.DeepEqual(val, map[string]interface{}{"1": []interface{}{int(1), int(2)}}) { + panic("not equal") + } + + // make a struct{1:map} node + ret = NewTypedNode(thrift.STRUCT, 0, 0, PathNode{ + Path: NewPathFieldId(1), + Node: NewNodeMap(map[interface{}]interface{}{"1": []byte{1}}, &Options{}), + }) + // print interface + opts.CastStringAsBinary = true + opts.MapStructById = true + val, err = ret.Interface(opts) + fmt.Printf("val:%#v\n", val) + if err != nil { + panic(err) + } + if !reflect.DeepEqual(val, map[thrift.FieldID]interface{}{thrift.FieldID(1): map[string]interface{}{"1": []byte{1}}}) { + panic("not equal") + } +} + func ExampleValue_SetByPath() { // pack root value desc := getExampleDesc() diff --git a/thrift/generic/node.go b/thrift/generic/node.go index ee271597..0eb3557f 100644 --- a/thrift/generic/node.go +++ b/thrift/generic/node.go @@ -27,6 +27,8 @@ import ( "github.com/cloudwego/dynamicgo/thrift" ) +var defaultOpts = &Options{} + // Node is a generic wrap of raw thrift data type Node struct { t thrift.Type @@ -72,32 +74,19 @@ func (self Node) offset() unsafe.Pointer { return rt.AddPtr(self.v, uintptr(self.l)) } -// func NewNodeAny(val interface{}) Node { -// vt := reflect.ValueOf(val) -// switch vt.Kind() { -// case reflect.Int8: -// return NewNodeInt8(int8(vt.Int())) -// case reflect.Int16: -// return NewNodeInt16(int16(vt.Int())) -// case reflect.Int32: -// return NewNodeInt32(int32(vt.Int())) -// case reflect.Int64: -// return NewNodeInt64(vt.Int()) -// case reflect.Float32, reflect.Float64: -// return NewNodeDouble(vt.Float()) -// case reflect.String: -// return NewNodeString(vt.String()) -// case reflect.Bool: -// return NewNodeBool(vt.Bool()) -// case reflect.Slice: -// if vt.Type().Elem().Kind() == reflect.Uint8 { -// return NewNodeBinary(vt.Bytes()) -// } -// panic("not supported") -// default: -// panic("not supported") -// } -// } +// NewNodeAny convert a go premitive type to Node. +// NOTICE: It only accepts LIMITED types. See `thrift.GoType2ThriftType()` for detailed conversion rules. +// +// opts provides options when making the node. +// Notice: only `Option.SliceAsSet` is effective now +func NewNodeAny(val interface{}, opts *Options) Node { + p := thrift.NewBinaryProtocol(make([]byte, 0, DefaultNodeBufferSize)) + if tt, err := p.WriteAny(val, opts.SliceAsSet); err != nil { + panic(err) + } else { + return NewNode(tt, p.Buf) + } +} // NewNodeBool converts a bool value to a BOOL node func NewNodeBool(val bool) Node { @@ -157,18 +146,82 @@ func NewNodeBinary(val []byte) Node { return NewNode(thrift.STRING, buf) } +// NewNodeList creates a LIST node. +// The element thrift type depends on vals' concrete type, +// thus there must be at least one val. +// +// NOTICE: all recursive sub slice will be regard as LIST too +func NewNodeList(vals []interface{}) Node { + p := thrift.NewBinaryProtocol(make([]byte, 0, len(vals)*DefaultNodeBufferSize)) + if _, err := p.WriteAny(vals, false); err != nil { + panic(err) + } + return NewNode(thrift.LIST, p.Buf) +} + +// NewNodeSet creates a SET node. +// The element thrift type depends on vals' concrete type, +// thus there must be at least one val. +// +// NOTICE: all recursive sub slice will be regard as SET too +func NewNodeSet(vals []interface{}) Node { + p := thrift.NewBinaryProtocol(make([]byte, 0, len(vals)*DefaultNodeBufferSize)) + if _, err := p.WriteAny(vals, true); err != nil { + panic(err) + } + return NewNode(thrift.SET, p.Buf) +} + +// NewNodeMap creates a MAP node. +// The thrift type of key and element depends on kvs' concrete type, +// thus there must be at least one kv. +// +// opts provides options when making the node. +// Notice: only `Option.SliceAsSet` is effective now +func NewNodeMap(kvs map[interface{}]interface{}, opts *Options) Node { + p := thrift.NewBinaryProtocol(make([]byte, 0, len(kvs)*DefaultNodeBufferSize*2)) + if _, err := p.WriteAny(kvs, opts.SliceAsSet); err != nil { + panic(err) + } + return NewNode(thrift.MAP, p.Buf) +} + +// NewNodeStruct creates a STRUCT node. +// The thrift type of element depends on vals' concrete type, +// thus there must be at least one field. +// +// opts provides options when making the node. +// Notice: only `Option.SliceAsSet` is effective now +func NewNodeStruct(fields map[thrift.FieldID]interface{}, opts *Options) Node { + p := thrift.NewBinaryProtocol(make([]byte, 0, len(fields)*DefaultNodeBufferSize)) + if _, err := p.WriteAny(fields, opts.SliceAsSet); err != nil { + panic(err) + } + return NewNode(thrift.STRUCT, p.Buf) +} + // NewTypedNode creates a new Node with the given typ, -// including element type (for LIST/SET/MAP) and key type (for MAP) -func NewTypedNode(typ thrift.Type, et thrift.Type, kt thrift.Type) (ret Node){ +// including element type (for LIST/SET/MAP) and key type (for MAP), +// and its children nodes. +// Its children PathNode sholud be according to typ: +// - STRUCT: PathTypeFieldId path and any-typed node +// - LIST/SET: PathTypeIndex path and et typed node +// - MAP: PathStrKey/PathIntKey/PathBinKey according to kt path and et typed node +// - scalar(STRING|I08|I16|I32|I64|BOOL|DOUBLE): the children is itself. +func NewTypedNode(typ thrift.Type, et thrift.Type, kt thrift.Type, children ...PathNode) (ret Node) { if !typ.Valid() { panic("invalid node type") } + if len(children) == 0 { + // NOTICE: dummy node just for PathNode.Node to work + return newNode(typ, et, kt, nil) + } + switch typ { case thrift.LIST, thrift.SET: if !et.Valid() { panic("invalid element type") } - ret.et = et case thrift.MAP: if !et.Valid() { panic("invalid element type") @@ -176,11 +229,37 @@ func NewTypedNode(typ thrift.Type, et thrift.Type, kt thrift.Type) (ret Node){ if !kt.Valid() { panic("invalid key type") } - ret.et = et - ret.kt = kt + case thrift.STRUCT: + break + default: + if len(children) != 1 { + panic("should only pass one children for scalar type!") + } + return newNode(typ, et, kt, children[0].Node.Raw()) + } + + // for nested type, marshal the children to bytes + tmp := PathNode{ + Next: children, + } + tmp.Node.t = typ + tmp.Node.et = et + tmp.Node.kt = kt + bs, err := tmp.Marshal(defaultOpts) + if err != nil { + panic(err) + } + return newNode(typ, et, kt, bs) +} + +func newNode(typ thrift.Type, et thrift.Type, kt thrift.Type, bs []byte) Node { + return Node{ + t: typ, + et: et, + kt: kt, + v: rt.GetBytePtr(bs), + l: len(bs), } - ret.t = typ - return } // Fork forks the node to a new node, copy underlying data as well diff --git a/thrift/generic/node_test.go b/thrift/generic/node_test.go new file mode 100644 index 00000000..2e1eaef3 --- /dev/null +++ b/thrift/generic/node_test.go @@ -0,0 +1,90 @@ +/** + * Copyright 2023 CloudWeGo Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package generic + +import ( + "fmt" + "strings" + "testing" + + "github.com/cloudwego/dynamicgo/thrift" + "github.com/stretchr/testify/require" +) + +func TestNewTypedNode(t *testing.T) { + type args struct { + typ thrift.Type + et thrift.Type + kt thrift.Type + children []PathNode + } + tests := []struct { + name string + args args + opts Options + wantRet interface{} + }{ + {"bool", args{thrift.BOOL, 0, 0, []PathNode{{Node: NewNodeBool(true)}}}, Options{}, true}, + {"string", args{thrift.STRING, 0, 0, []PathNode{{Node: NewNodeString("1")}}}, Options{}, "1"}, + {"binary", args{thrift.STRING, 0, 0, []PathNode{{Node: NewNodeBinary([]byte{1})}}}, Options{CastStringAsBinary: true}, []byte{1}}, + {"byte", args{thrift.BYTE, 0, 0, []PathNode{{Node: NewNodeByte(1)}}}, Options{}, int(1)}, + {"i16", args{thrift.I16, 0, 0, []PathNode{{Node: NewNodeInt16(1)}}}, Options{}, int(1)}, + {"i32", args{thrift.I32, 0, 0, []PathNode{{Node: NewNodeInt32(1)}}}, Options{}, int(1)}, + {"i64", args{thrift.I64, 0, 0, []PathNode{{Node: NewNodeInt64(1)}}}, Options{}, int(1)}, + {"double", args{thrift.DOUBLE, 0, 0, []PathNode{{Node: NewNodeDouble(1)}}}, Options{}, float64(1)}, + {"list", args{thrift.LIST, thrift.BYTE, 0, []PathNode{{Path: NewPathIndex(0), Node: NewNodeByte(1)}}}, Options{}, []interface{}{int(1)}}, + {"set", args{thrift.SET, thrift.BYTE, 0, []PathNode{{Path: NewPathIndex(0), Node: NewNodeByte(1)}}}, Options{}, []interface{}{int(1)}}, + {"int map", args{thrift.MAP, thrift.BYTE, thrift.BYTE, []PathNode{{Path: NewPathIntKey(1), Node: NewNodeByte(1)}}}, Options{}, map[int]interface{}{int(1): int(1)}}, + {"string map", args{thrift.MAP, thrift.BYTE, thrift.STRING, []PathNode{{Path: NewPathStrKey("1"), Node: NewNodeByte(1)}}}, Options{}, map[string]interface{}{"1": int(1)}}, + {"any map + key list", args{thrift.MAP, thrift.BYTE, thrift.LIST, []PathNode{{Path: NewPathBinKey(NewNodeList([]interface{}{1, 2}).Raw()), Node: NewNodeByte(1)}}}, Options{}, map[interface{}]interface{}{&[]interface{}{int(1), int(2)}: int(1)}}, + {"any map + key map", args{thrift.MAP, thrift.BYTE, thrift.MAP, []PathNode{{Path: NewPathBinKey(NewNodeMap(map[interface{}]interface{}{1: 2}, &Options{}).Raw()), Node: NewNodeByte(1)}}}, Options{}, map[interface{}]interface{}{&map[int]interface{}{1: 2}: int(1)}}, + {"struct", args{thrift.STRUCT, 0, 0, []PathNode{{Path: NewPathFieldId(1), Node: NewNodeByte(1)}}}, Options{MapStructById: true}, map[thrift.FieldID]interface{}{thrift.FieldID(1): int(1)}}, + {"struct + struct", args{thrift.STRUCT, 0, 0, []PathNode{{Path: NewPathFieldId(1), Node: NewNodeStruct(map[thrift.FieldID]interface{}{1: 1}, &Options{})}}}, Options{MapStructById: true}, map[thrift.FieldID]interface{}{thrift.FieldID(1): map[thrift.FieldID]interface{}{thrift.FieldID(1): int(1)}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + println(tt.name) + ret := NewTypedNode(tt.args.typ, tt.args.et, tt.args.kt, tt.args.children...) + fmt.Printf("buf:%+v\n", ret.Raw()) + val, err := ret.Interface(&tt.opts) + fmt.Printf("val:%#v\n", val) + if err != nil { + t.Errorf("NewTypedNode() error = %v", err) + return + } + if strings.Contains(tt.name, "map + key") { + em := tt.wantRet.(map[interface{}]interface{}) + gm := val.(map[interface{}]interface{}) + require.Equal(t, len(em), len(gm)) + var firstK, firstV interface{} + for k, v := range em { + firstK, firstV = k, v + break + } + var firstKgot, firstVgot interface{} + for k, v := range gm { + firstKgot, firstVgot = k, v + break + } + require.Equal(t, firstK, firstKgot) + require.Equal(t, firstV, firstVgot) + } else { + require.Equal(t, tt.wantRet, val) + } + }) + } +} diff --git a/thrift/generic/option.go b/thrift/generic/option.go index d3822a22..c16f36ac 100644 --- a/thrift/generic/option.go +++ b/thrift/generic/option.go @@ -87,6 +87,9 @@ type Options struct { // DescriptorToPathNodeWriteDefualt indicates writing empty value for default fields for API `DescriptorToPathNode` DescriptorToPathNodeWriteDefualt bool + + // SliceAsSet indicates `NewNodeAny()` to covert go slice to SET instead of LIST + SliceAsSet bool } var ( @@ -95,4 +98,8 @@ var ( // StoreChildrenByIdShreshold is the minimum id to store children node by hash. StoreChildrenByIntHashShreshold = DefaultNodeSliceCap + + // DefaultNodeBufferSize indicates every element buffer size for one complex-type Node, + // including `NewNodeList()\NewNodeSet()\NewNodeMap()\NewNodeStruct()\NewNodeAny()` + DefaultNodeBufferSize = 64 )