diff --git a/go.mod b/go.mod index 53f57d97..df50b355 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/jhump/protoreflect v1.8.2 github.com/klauspost/cpuid/v2 v2.2.9 github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 google.golang.org/protobuf v1.33.0 ) diff --git a/go.sum b/go.sum index c1151c7a..13c89d5d 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/thrift/idl.go b/thrift/idl.go index 02434488..3df59b99 100644 --- a/thrift/idl.go +++ b/thrift/idl.go @@ -558,6 +558,31 @@ type compilingInstance struct { type compilingCache map[string]*compilingInstance +func thriftNamespace(ast *parser.Thrift) string { + if ast == nil || ast.Namespaces == nil || len(ast.Namespaces) == 0 { + return "" + } + var any, star string + for _, ns := range ast.Namespaces { + if ns == nil || ns.Name == "" { + continue + } + if ns.Language == "go" { + return ns.Name + } + if ns.Language == "*" { + star = ns.Name + } + if any == "" { + any = ns.Name + } + } + if star != "" { + return star + } + return any +} + // arg cache: // only support self reference on the same file // cross file self reference complicate matters @@ -924,3 +949,68 @@ func makeDefaultValue(typ *TypeDescriptor, val *parser.ConstValue, tree *parser. } return nil, nil } + +// NewDescriptorByName parse thrift IDL and return the specified (file+typeName) type descriptor. +// The includes is the thrift file content map, and its keys are specific including thrift file path, values are the thrift file content. +// file is the main thrift file path, name is the type name to parse (supports format like "package.TypeName" for cross-file references). +// Returns a complete TypeDescriptor with all referenced types resolved. +func (opts Options) NewDescriptorByName(ctx context.Context, file string, name string, includes map[string]string) (*TypeDescriptor, error) { + // Parse the main IDL file and all includes + tree, err := parseIDLContent(file, includes[file], includes) + if err != nil { + return nil, err + } + + // Resolve all symbols in the AST + if err := semantic.ResolveSymbols(tree); err != nil { + return nil, err + } + + // Create a compilingCache to handle recursive type references + structsCache := compilingCache{} + + // Parse the type using the parseType function + // First, we need to create a parser.Type that matches the requested type name + typePkg, typeName := util.SplitSubfix(name) + var targetTree *parser.Thrift = tree + + // If cross-file reference, resolve the file reference + if typePkg != "" { + ref, ok := tree.GetReference(typePkg) + if !ok { + return nil, fmt.Errorf("miss reference: %s in file: %s", typePkg, file) + } + targetTree = ref + // Reset cache for cross-file reference + structsCache = compilingCache{} + } + + // Verify the type exists in the target file + if _, ok := targetTree.GetStruct(typeName); !ok { + if _, ok := targetTree.GetUnion(typeName); !ok { + if _, ok := targetTree.GetException(typeName); !ok { + if _, ok := targetTree.GetTypedef(typeName); !ok { + if _, ok := targetTree.GetEnum(typeName); !ok { + return nil, fmt.Errorf("missing type: %s in file: %s", name, file) + } + // Handle enum type - return as i32 or i64 based on options + if opts.ParseEnumAsInt64 { + return builtinTypes["i64"], nil + } + return builtinTypes["i32"], nil + } + // Handle typedef - recursively parse the underlying type + typDef, _ := targetTree.GetTypedef(typeName) + return parseType(ctx, typDef.Type, targetTree, structsCache, 0, opts, nil, Request) + } + } + } + + // Create a parser.Type for the target type + parserType := &parser.Type{ + Name: name, + } + + // Parse the type descriptor + return parseType(ctx, parserType, tree, structsCache, 0, opts, nil, Request) +} diff --git a/thrift/idl_test.go b/thrift/idl_test.go index 74880415..8aa6a466 100644 --- a/thrift/idl_test.go +++ b/thrift/idl_test.go @@ -488,3 +488,169 @@ func TestParseWithServiceName(t *testing.T) { require.Nil(t, p) require.Equal(t, err.Error(), "the idl service name UnknownService is not in the idl. Please check your idl") } + +func TestNewDescriptorByName(t *testing.T) { + t.Run("WithIncludes", func(t *testing.T) { + // Test cross-file struct parsing + mainPath := "main.thrift" + mainContent := ` + namespace go kitex.test.server + include "base.thrift" + + struct UserRequest { + 1: required string username + 2: base.User user + } + ` + + basePath := "base.thrift" + baseContent := ` + namespace go kitex.test.server + + struct User { + 1: required i64 id + 2: required string name + 3: optional string email + } + ` + + includes := map[string]string{ + mainPath: mainContent, + basePath: baseContent, + } + + opts := Options{} + + // Test parsing UserRequest which references User from another file + desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "UserRequest", includes) + require.NoError(t, err) + require.NotNil(t, desc) + require.Equal(t, STRUCT, desc.Type()) + require.Equal(t, "UserRequest", desc.Struct().Name()) + + // Verify fields + usernameField := desc.Struct().FieldByKey("username") + require.NotNil(t, usernameField) + require.Equal(t, STRING, usernameField.Type().Type()) + + userField := desc.Struct().FieldByKey("user") + require.NotNil(t, userField) + require.Equal(t, STRUCT, userField.Type().Type()) + require.Equal(t, "User", userField.Type().Struct().Name()) + + // Verify nested User struct fields + userStruct := userField.Type().Struct() + idField := userStruct.FieldByKey("id") + require.NotNil(t, idField) + require.Equal(t, I64, idField.Type().Type()) + + nameField := userStruct.FieldByKey("name") + require.NotNil(t, nameField) + require.Equal(t, STRING, nameField.Type().Type()) + + emailField := userStruct.FieldByKey("email") + require.NotNil(t, emailField) + require.Equal(t, STRING, emailField.Type().Type()) + }) + + t.Run("PackageNotation", func(t *testing.T) { + // Test cross-file reference using package notation (e.g., "base.User") + mainPath := "main.thrift" + mainContent := ` + namespace go kitex.test.server + include "base.thrift" + ` + + basePath := "base.thrift" + baseContent := ` + namespace go kitex.test.base + + struct User { + 1: required i64 id + 2: required string name + } + ` + + includes := map[string]string{ + mainPath: mainContent, + basePath: baseContent, + } + + opts := Options{} + + // Test parsing User struct with cross-file reference notation + desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "base.User", includes) + + require.NoError(t, err) + require.NotNil(t, desc) + require.Equal(t, STRUCT, desc.Type()) + require.Equal(t, "User", desc.Struct().Name()) + + // Verify fields + idField := desc.Struct().FieldByKey("id") + require.NotNil(t, idField) + require.Equal(t, I64, idField.Type().Type()) + }) + + // Test with mixed relative and absolute paths + t.Run("WithMixedPaths", func(t *testing.T) { + mainPath := "a/b/main.thrift" + mainContent := ` + namespace go kitex.test.server + include "../c/base.thrift" + include "a/b/ref.thrift" + + struct UserRequest { + 1: required string username + 2: base.User user + 3: ref.Placeholder placeholder + } + ` + + basePath := "a/c/base.thrift" + baseContent := ` + namespace go kitex.test.server + + struct User { + 1: required i64 id + 2: required string name + } + ` + refPath := "a/b/ref.thrift" + refContent := ` + namespace go kitex.test.server + + struct Placeholder { + 1: required string info + } + ` + + includes := map[string]string{ + mainPath: mainContent, + basePath: baseContent, + refPath: refContent, + } + + opts := Options{} + + // Test parsing UserRequest with relative path that goes up directories + desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "UserRequest", includes) + + require.NoError(t, err) + require.NotNil(t, desc) + require.Equal(t, STRUCT, desc.Type()) + require.Equal(t, "UserRequest", desc.Struct().Name()) + + // Verify nested User struct is correctly resolved via relative path + userField := desc.Struct().FieldByKey("user") + require.NotNil(t, userField) + require.Equal(t, STRUCT, userField.Type().Type()) + require.Equal(t, "User", userField.Type().Struct().Name()) + + // Verify nested Placeholder struct is correctly resolved via direct relative path + placeholderField := desc.Struct().FieldByKey("placeholder") + require.NotNil(t, placeholderField) + require.Equal(t, STRUCT, placeholderField.Type().Type()) + require.Equal(t, "Placeholder", placeholderField.Type().Struct().Name()) + }) +} diff --git a/trim/all_test.go b/trim/all_test.go new file mode 100644 index 00000000..49dd1763 --- /dev/null +++ b/trim/all_test.go @@ -0,0 +1,1359 @@ +package trim + +import ( + "encoding/json" + "sync" + "testing" + + "github.com/cloudwego/dynamicgo/proto" + "github.com/cloudwego/dynamicgo/proto/binary" + "github.com/cloudwego/dynamicgo/thrift" + "github.com/cloudwego/thriftgo/generator/golang/extension/unknown" + "github.com/stretchr/testify/require" +) + +func BenchmarkFetchAndAssign(b *testing.B) { + src := makeSampleFetch(3, 3) + desc := makeDesc(3, 3, true) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + m, err := fetchAny(desc, src) + if err != nil { + b.Fatalf("FetchAny failed: %v", err) + } + dest := &sampleAssign{} + err = assignAny(desc, m, dest) + if err != nil { + b.Fatalf("AssignAny failed: %v", err) + } + } +} + +func TestFetchAndAssign(t *testing.T) { + src := makeSampleFetch(3, 3) + srcjson, err := json.Marshal(src) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + desc := makeDesc(3, 3, true) + + wg := sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + m, err := fetchAny(desc, src) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + mjson, err := json.Marshal(m) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + require.Equal(t, string(srcjson), string(mjson)) + + dest := makeSampleAssign(3, 3) + err = assignAny(desc, m, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + destjson, err := json.Marshal(dest) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + var srcAny interface{} + if err := json.Unmarshal(srcjson, &srcAny); err != nil { + t.Fail() + } + var destAny interface{} + if err := json.Unmarshal(destjson, &destAny); err != nil { + t.Fail() + } + require.Equal(t, srcAny, destAny) + }() + } + + wg.Wait() +} + +func fetchAny(desc *Descriptor, any interface{}) (interface{}, error) { + if desc != nil { + desc.Normalize() + } + fetcher := &Fetcher{} + return fetcher.FetchAny(desc, any) +} + +// ===================== UnknownFields FetchAndAssign Tests ===================== +// These tests verify the mapping between thrift _unknownFields and protobuf XXX_unrecognized, +// using the Descriptor as the field mapping source (NOT treating thrift ID as protobuf ID directly). + +// thriftFetchStruct simulates a thrift struct with some fields in _unknownFields +type thriftFetchStruct struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + _unknownFields unknown.Fields `json:"-"` +} + +// protoAssignStruct simulates a protobuf struct that has different field layout +// Note: protobuf field IDs are different from thrift field IDs! +type protoAssignStruct struct { + // field_a maps to protobuf field ID 10 (different from thrift ID 1) + FieldA int `protobuf:"varint,10,req,name=field_a" json:"field_a,omitempty"` + // field_b maps to protobuf field ID 20 + FieldB string `protobuf:"bytes,20,opt,name=field_b" json:"field_b,omitempty"` + // field_c maps to protobuf field ID 30 + FieldC int64 `protobuf:"varint,30,opt,name=field_c" json:"field_c,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +// protoAssignStructSmall has only field_a, other fields will go to XXX_unrecognized +type protoAssignStructSmall struct { + FieldA int `protobuf:"varint,10,req,name=field_a" json:"field_a,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +// TestFetchAndAssign_UnknownToUnrecognized tests: +// fetch._unknownFields -> assign.XXX_unrecognized +// Thrift struct has field in _unknownFields, which should be encoded to protobuf XXX_unrecognized +// using the descriptor's ID mapping (descriptor ID -> protobuf field ID). +func TestFetchAndAssign_UnknownToUnrecognized(t *testing.T) { + // Create thrift struct with field_a as known and field_b in _unknownFields + src := &thriftFetchStruct{ + FieldA: 42, + } + + // Encode field_b (thrift ID=2) and field_c (thrift ID=3) into _unknownFields + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRING, 2) // field_b, thrift ID=2 + p.WriteString("hello from unknown") + p.WriteFieldBegin("", thrift.I64, 3) // field_c, thrift ID=3 + p.WriteI64(12345) + src._unknownFields = p.Buf + + // Descriptor provides the mapping: + // - field_a: Thrift ID=1, will be fetched as "field_a" + // - field_b: Thrift ID=2, will be fetched as "field_b" + // - field_c: Thrift ID=3, will be fetched as "field_c" + // Note: these descriptor IDs are Thrift IDs, NOT protobuf IDs! + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, // Thrift ID 1 + {Name: "field_b", ID: 2}, // Thrift ID 2 -> will become protobuf field 20 + {Name: "field_c", ID: 3}, // Thrift ID 3 -> will become protobuf field 30 + }, + } + + // Fetch from thrift struct + fetched, err := fetchAny(desc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + require.Equal(t, 42, fetchedMap["field_a"]) + require.Equal(t, "hello from unknown", fetchedMap["field_b"]) + require.Equal(t, int64(12345), fetchedMap["field_c"]) + + // Assign to protobuf struct that only has field_a defined + // field_b and field_c should go to XXX_unrecognized with protobuf IDs from descriptor + dest := &protoAssignStructSmall{} + + // Create a new descriptor for assign that maps names to protobuf field IDs + // The key point: descriptor.ID here represents the TARGET protobuf field ID + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 10}, // Protobuf ID 10 + {Name: "field_b", ID: 20}, // Protobuf ID 20 -> will go to XXX_unrecognized + {Name: "field_c", ID: 30}, // Protobuf ID 30 -> will go to XXX_unrecognized + }, + } + + err = assignAny(assignDesc, fetched, dest) + require.NoError(t, err) + + // Verify field_a is assigned correctly + require.Equal(t, 42, dest.FieldA) + + // Verify field_b and field_c are in XXX_unrecognized with correct protobuf IDs + require.NotEmpty(t, dest.XXX_unrecognized) + + // Decode XXX_unrecognized to verify field IDs + bp := binary.NewBinaryProtol(dest.XXX_unrecognized) + defer binary.FreeBinaryProtocol(bp) + + foundFieldB := false + foundFieldC := false + + for bp.Read < len(bp.Buf) { + fieldNum, wireType, _, err := bp.ConsumeTag() + require.NoError(t, err) + + switch fieldNum { + case 20: // field_b with protobuf ID 20 + require.Equal(t, proto.BytesType, wireType) // length-delimited for string + val, err := bp.ReadString(true) + require.NoError(t, err) + require.Equal(t, "hello from unknown", val) + foundFieldB = true + case 30: // field_c with protobuf ID 30 + require.Equal(t, proto.VarintType, wireType) // varint for int64 + val, err := bp.ReadInt64() + require.NoError(t, err) + require.Equal(t, int64(12345), val) + foundFieldC = true + default: + t.Errorf("unexpected field number in XXX_unrecognized: %d", fieldNum) + } + } + + require.True(t, foundFieldB, "field_b not found in XXX_unrecognized") + require.True(t, foundFieldC, "field_c not found in XXX_unrecognized") +} + +// TestFetchAndAssign_UnknownToKnown tests: +// fetch._unknownFields -> assign.已知字段 +// Thrift struct has field in _unknownFields, which should be assigned to protobuf known field +func TestFetchAndAssign_UnknownToKnown(t *testing.T) { + // Create thrift struct with only field_a as known + src := &thriftFetchStruct{ + FieldA: 42, + } + + // Encode field_b (thrift ID=2) into _unknownFields + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRING, 2) + p.WriteString("from thrift unknown") + src._unknownFields = p.Buf + + // Descriptor for fetching (Thrift IDs) + fetchDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, // Thrift ID 1 + {Name: "field_b", ID: 2}, // Thrift ID 2, in _unknownFields + }, + } + + // Fetch from thrift struct + fetched, err := fetchAny(fetchDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + require.Equal(t, 42, fetchedMap["field_a"]) + require.Equal(t, "from thrift unknown", fetchedMap["field_b"]) + + // Assign to protobuf struct that has field_b as a known field + dest := &protoAssignStruct{} + + // Descriptor for assigning (Protobuf IDs) + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 10}, // Protobuf ID 10 + {Name: "field_b", ID: 20}, // Protobuf ID 20, is a known field in dest + }, + } + + err = assignAny(assignDesc, fetched, dest) + require.NoError(t, err) + + // Verify both fields are assigned correctly to known fields + require.Equal(t, 42, dest.FieldA) + require.Equal(t, "from thrift unknown", dest.FieldB) + + // XXX_unrecognized should be empty since all fields are known + require.Empty(t, dest.XXX_unrecognized) +} + +// TestFetchAndAssign_KnownToUnrecognized tests: +// fetch.已知字段 -> assign.XXX_unrecognized +// Thrift struct has field as known, which should be encoded to protobuf XXX_unrecognized +// because protobuf struct doesn't have that field defined +func TestFetchAndAssign_KnownToUnrecognized(t *testing.T) { + // thriftFetchStructFull has more known fields than protoAssignStructSmall + type thriftFetchStructFull struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + FieldB string `thrift:"FieldB,2" json:"field_b,omitempty"` + FieldC int64 `thrift:"FieldC,3" json:"field_c,omitempty"` + } + + // Create thrift struct with all fields as known + src := &thriftFetchStructFull{ + FieldA: 42, + FieldB: "known field in thrift", + FieldC: 98765, + } + + // Descriptor for fetching (Thrift IDs) + fetchDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, // Thrift ID 1 + {Name: "field_b", ID: 2}, // Thrift ID 2 + {Name: "field_c", ID: 3}, // Thrift ID 3 + }, + } + + // Fetch from thrift struct + fetched, err := fetchAny(fetchDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + require.Equal(t, 42, fetchedMap["field_a"]) + require.Equal(t, "known field in thrift", fetchedMap["field_b"]) + require.Equal(t, int64(98765), fetchedMap["field_c"]) + + // Assign to protobuf struct that only has field_a + // field_b and field_c should go to XXX_unrecognized + dest := &protoAssignStructSmall{} + + // Descriptor for assigning (Protobuf IDs) + // Note: IDs here are protobuf field IDs, different from thrift IDs! + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "TestStruct", + Children: []Field{ + {Name: "field_a", ID: 10}, // Protobuf ID 10, is a known field in dest + {Name: "field_b", ID: 20}, // Protobuf ID 20, NOT in dest -> XXX_unrecognized + {Name: "field_c", ID: 30}, // Protobuf ID 30, NOT in dest -> XXX_unrecognized + }, + } + + err = assignAny(assignDesc, fetched, dest) + require.NoError(t, err) + + // Verify field_a is assigned correctly + require.Equal(t, 42, dest.FieldA) + + // Verify field_b and field_c are in XXX_unrecognized with protobuf IDs + require.NotEmpty(t, dest.XXX_unrecognized) + + // Decode XXX_unrecognized to verify + bp := binary.NewBinaryProtol(dest.XXX_unrecognized) + defer binary.FreeBinaryProtocol(bp) + + foundFieldB := false + foundFieldC := false + + for bp.Read < len(bp.Buf) { + fieldNum, wireType, _, err := bp.ConsumeTag() + require.NoError(t, err) + + switch fieldNum { + case 20: // field_b with protobuf ID 20 + require.Equal(t, proto.BytesType, wireType) // length-delimited for string + val, err := bp.ReadString(true) + require.NoError(t, err) + require.Equal(t, "known field in thrift", val) + foundFieldB = true + case 30: // field_c with protobuf ID 30 + require.Equal(t, proto.VarintType, wireType) // varint for int64 + val, err := bp.ReadInt64() + require.NoError(t, err) + require.Equal(t, int64(98765), val) + foundFieldC = true + default: + t.Errorf("unexpected field number in XXX_unrecognized: %d", fieldNum) + } + } + + require.True(t, foundFieldB, "field_b not found in XXX_unrecognized") + require.True(t, foundFieldC, "field_c not found in XXX_unrecognized") +} + +// TestFetchAndAssign_MixedScenario tests a complex scenario with all three cases: +// 1. fetch._unknownFields -> assign.XXX_unrecognized +// 2. fetch._unknownFields -> assign.known field +// 3. fetch.known field -> assign.XXX_unrecognized +func TestFetchAndAssign_MixedScenario(t *testing.T) { + // thriftMixedStruct has some known fields, some in _unknownFields + type thriftMixedStruct struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + FieldD string `thrift:"FieldD,4" json:"field_d,omitempty"` // known in thrift, will go to XXX_unrecognized + _unknownFields unknown.Fields `json:"-"` + } + + // protoMixedStruct has different field layout + type protoMixedStruct struct { + FieldA int `protobuf:"varint,10,req,name=field_a" json:"field_a,omitempty"` + FieldB string `protobuf:"bytes,20,opt,name=field_b" json:"field_b,omitempty"` // will receive from thrift _unknownFields + XXX_unrecognized []byte `json:"-"` + } + + // Create thrift struct + src := &thriftMixedStruct{ + FieldA: 100, + FieldD: "thrift known -> pb unknown", + } + + // Encode field_b (thrift ID=2) and field_c (thrift ID=3) into _unknownFields + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRING, 2) // field_b -> will go to pb known field + p.WriteString("thrift unknown -> pb known") + p.WriteFieldBegin("", thrift.I32, 3) // field_c -> will go to pb XXX_unrecognized + p.WriteI32(999) + src._unknownFields = p.Buf + + // Fetch descriptor (Thrift IDs) + fetchDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "MixedStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, // known in thrift + {Name: "field_b", ID: 2}, // in thrift _unknownFields + {Name: "field_c", ID: 3}, // in thrift _unknownFields + {Name: "field_d", ID: 4}, // known in thrift + }, + } + + // Fetch from thrift struct + fetched, err := fetchAny(fetchDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + require.Equal(t, 100, fetchedMap["field_a"]) + require.Equal(t, "thrift unknown -> pb known", fetchedMap["field_b"]) + require.Equal(t, int32(999), fetchedMap["field_c"]) + require.Equal(t, "thrift known -> pb unknown", fetchedMap["field_d"]) + + // Assign descriptor (Protobuf IDs) + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "MixedStruct", + Children: []Field{ + {Name: "field_a", ID: 10}, // known in pb + {Name: "field_b", ID: 20}, // known in pb (from thrift _unknownFields) + {Name: "field_c", ID: 30}, // NOT in pb -> XXX_unrecognized + {Name: "field_d", ID: 40}, // NOT in pb -> XXX_unrecognized + }, + } + + dest := &protoMixedStruct{} + err = assignAny(assignDesc, fetched, dest) + require.NoError(t, err) + + // Verify known fields + require.Equal(t, 100, dest.FieldA) + require.Equal(t, "thrift unknown -> pb known", dest.FieldB) // from thrift _unknownFields to pb known + + // Verify XXX_unrecognized contains field_c and field_d + require.NotEmpty(t, dest.XXX_unrecognized) + + bp := binary.NewBinaryProtol(dest.XXX_unrecognized) + defer binary.FreeBinaryProtocol(bp) + + foundFieldC := false + foundFieldD := false + + for bp.Read < len(bp.Buf) { + fieldNum, wireType, _, err := bp.ConsumeTag() + require.NoError(t, err) + + switch fieldNum { + case 30: // field_c with protobuf ID 30 + require.Equal(t, proto.VarintType, wireType) // varint for int32 + val, err := bp.ReadInt64() // stored as varint (int64) + require.NoError(t, err) + require.Equal(t, int64(999), val) + foundFieldC = true + case 40: // field_d with protobuf ID 40 + require.Equal(t, proto.BytesType, wireType) // length-delimited for string + val, err := bp.ReadString(true) + require.NoError(t, err) + require.Equal(t, "thrift known -> pb unknown", val) + foundFieldD = true + default: + t.Errorf("unexpected field number in XXX_unrecognized: %d", fieldNum) + } + } + + require.True(t, foundFieldC, "field_c (from thrift _unknownFields) not found in pb XXX_unrecognized") + require.True(t, foundFieldD, "field_d (from thrift known) not found in pb XXX_unrecognized") +} + +// TestFetchAndAssign_NestedStructWithUnknownFields tests nested struct handling +func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { + // Thrift struct with nested struct in _unknownFields + type thriftOuter struct { + ID int `thrift:"ID,1" json:"id,omitempty"` + _unknownFields unknown.Fields `json:"-"` + } + + // Proto struct with nested struct as known field + type protoInner struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Value int `protobuf:"varint,2,opt,name=value" json:"value,omitempty"` + } + type protoOuter struct { + ID int `protobuf:"varint,10,req,name=id" json:"id,omitempty"` + Inner *protoInner `protobuf:"bytes,20,opt,name=inner" json:"inner,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + // Create thrift struct with nested struct in _unknownFields + src := &thriftOuter{ + ID: 1, + } + + // Encode nested struct (thrift ID=2) into _unknownFields + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRUCT, 2) // inner struct, thrift ID=2 + p.WriteFieldBegin("", thrift.STRING, 1) // inner.name + p.WriteString("nested name") + p.WriteFieldBegin("", thrift.I32, 2) // inner.value + p.WriteI32(123) + p.WriteFieldStop() // end inner struct + src._unknownFields = p.Buf + + // Fetch descriptor + fetchDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Outer", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Inner", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "value", ID: 2}, + }, + }, + }, + }, + } + + // Fetch from thrift struct + fetched, err := fetchAny(fetchDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + require.Equal(t, 1, fetchedMap["id"]) + + innerMap, ok := fetchedMap["inner"].(map[string]interface{}) + require.True(t, ok) + require.Equal(t, "nested name", innerMap["name"]) + require.Equal(t, int32(123), innerMap["value"]) + + // Assign descriptor (Protobuf IDs) + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Outer", + Children: []Field{ + {Name: "id", ID: 10}, + { + Name: "inner", + ID: 20, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Inner", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "value", ID: 2}, + }, + }, + }, + }, + } + + dest := &protoOuter{} + err = assignAny(assignDesc, fetched, dest) + require.NoError(t, err) + + // Verify + require.Equal(t, 1, dest.ID) + require.NotNil(t, dest.Inner) + require.Equal(t, "nested name", dest.Inner.Name) + require.Equal(t, 123, dest.Inner.Value) +} + +// ===================== Shallow Descriptor Tests ===================== +// These tests verify FetchAndAssign correctness when Descriptor is shallower than the actual objects +// or when Descriptor is missing some fields. + +// TestFetchAndAssign_ShallowDescriptor tests when Descriptor depth < object depth +// Only the fields specified in the Descriptor should be fetched/assigned +func TestFetchAndAssign_ShallowDescriptor(t *testing.T) { + // Use makeSampleFetch to create a deep nested structure (depth=3) + // But use a shallow descriptor (depth=1) that only fetches top-level fields + src := makeSampleFetch(2, 3) // width=2, depth=3 + + // Create a shallow descriptor (depth=1) - no nested Desc for Children + shallowDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, // scalar field, no Desc needed + {Name: "field_e", ID: 5}, // scalar field, no Desc needed + // field_b, field_c, field_d are complex types but no Desc -> fetch as-is + {Name: "field_b", ID: 2}, // list field, no Desc -> fetch raw value + {Name: "field_d", ID: 4}, // pointer to struct, no Desc -> fetch raw value + }, + } + + // Fetch with shallow descriptor + fetched, err := fetchAny(shallowDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Verify scalar fields + require.Equal(t, 1, fetchedMap["field_a"]) + require.Equal(t, "1", fetchedMap["field_e"]) + + // field_b should be the raw slice (not recursively processed) + fieldB, ok := fetchedMap["field_b"] + require.True(t, ok) + require.NotNil(t, fieldB) + + // field_d should be the raw pointer value (not recursively processed) + fieldD, ok := fetchedMap["field_d"] + require.True(t, ok) + require.NotNil(t, fieldD) + + // Now assign with the same shallow descriptor + dest := makeSampleAssign(2, 3) + err = assignAny(shallowDesc, fetched, dest) + require.NoError(t, err) + + // Verify scalar fields are correctly assigned + require.Equal(t, 1, dest.FieldA) + require.Equal(t, "1", dest.FieldE) + + // Verify complex fields are assigned (even though descriptor is shallow) + require.NotNil(t, dest.FieldB) + require.NotNil(t, dest.FieldD) +} + +// TestFetchAndAssign_MissingFieldsInDescriptor tests when Descriptor is missing some fields +// Only the fields specified in the Descriptor should be fetched/assigned +func TestFetchAndAssign_MissingFieldsInDescriptor(t *testing.T) { + // Create a thrift struct with many fields + type thriftFullStruct struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + FieldB string `thrift:"FieldB,2" json:"field_b,omitempty"` + FieldC int64 `thrift:"FieldC,3" json:"field_c,omitempty"` + FieldD string `thrift:"FieldD,4" json:"field_d,omitempty"` + FieldE int32 `thrift:"FieldE,5" json:"field_e,omitempty"` + } + + // Create a protobuf struct with same fields + type protoFullStruct struct { + FieldA int `protobuf:"varint,1,req,name=field_a" json:"field_a,omitempty"` + FieldB string `protobuf:"bytes,2,opt,name=field_b" json:"field_b,omitempty"` + FieldC int64 `protobuf:"varint,3,opt,name=field_c" json:"field_c,omitempty"` + FieldD string `protobuf:"bytes,4,opt,name=field_d" json:"field_d,omitempty"` + FieldE int32 `protobuf:"varint,5,opt,name=field_e" json:"field_e,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftFullStruct{ + FieldA: 100, + FieldB: "hello", + FieldC: 12345, + FieldD: "world", + FieldE: 999, + } + + // Descriptor only includes field_a, field_c, field_e (missing field_b, field_d) + partialDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "PartialStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_c", ID: 3}, + {Name: "field_e", ID: 5}, + }, + } + + // Fetch with partial descriptor + fetched, err := fetchAny(partialDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Only specified fields should be fetched + require.Equal(t, 100, fetchedMap["field_a"]) + require.Equal(t, int64(12345), fetchedMap["field_c"]) + require.Equal(t, int32(999), fetchedMap["field_e"]) + + // Missing fields should NOT be in the fetched map + _, hasFieldB := fetchedMap["field_b"] + _, hasFieldD := fetchedMap["field_d"] + require.False(t, hasFieldB, "field_b should not be fetched") + require.False(t, hasFieldD, "field_d should not be fetched") + + // Assign with partial descriptor + dest := &protoFullStruct{ + FieldB: "original_b", // pre-set values + FieldD: "original_d", + } + + err = assignAny(partialDesc, fetched, dest) + require.NoError(t, err) + + // Verify only specified fields are assigned + require.Equal(t, 100, dest.FieldA) + require.Equal(t, int64(12345), dest.FieldC) + require.Equal(t, int32(999), dest.FieldE) + + // Pre-existing values for missing fields should be preserved + require.Equal(t, "original_b", dest.FieldB) + require.Equal(t, "original_d", dest.FieldD) +} + +// TestFetchAndAssign_NestedMissingFields tests nested structs with missing fields in Descriptor +func TestFetchAndAssign_NestedMissingFields(t *testing.T) { + // Thrift nested struct + type thriftInner struct { + Name string `thrift:"Name,1" json:"name,omitempty"` + Value int `thrift:"Value,2" json:"value,omitempty"` + Extra string `thrift:"Extra,3" json:"extra,omitempty"` + } + type thriftOuter struct { + ID int `thrift:"ID,1" json:"id,omitempty"` + Inner *thriftInner `thrift:"Inner,2" json:"inner,omitempty"` + Tag string `thrift:"Tag,3" json:"tag,omitempty"` + } + + // Proto nested struct + type protoInner struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Value int `protobuf:"varint,2,opt,name=value" json:"value,omitempty"` + Extra string `protobuf:"bytes,3,opt,name=extra" json:"extra,omitempty"` + } + type protoOuter struct { + ID int `protobuf:"varint,1,req,name=id" json:"id,omitempty"` + Inner *protoInner `protobuf:"bytes,2,opt,name=inner" json:"inner,omitempty"` + Tag string `protobuf:"bytes,3,opt,name=tag" json:"tag,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftOuter{ + ID: 1, + Inner: &thriftInner{ + Name: "inner_name", + Value: 42, + Extra: "extra_data", + }, + Tag: "outer_tag", + } + + // Descriptor: outer has id and inner, but inner only has name (missing value and extra) + // Also missing outer.tag + partialDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Outer", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Inner", + Children: []Field{ + {Name: "name", ID: 1}, // only fetch name, not value or extra + }, + }, + }, + // tag (ID: 3) is missing from descriptor + }, + } + + // Fetch with partial descriptor + fetched, err := fetchAny(partialDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Verify outer fields + require.Equal(t, 1, fetchedMap["id"]) + _, hasTag := fetchedMap["tag"] + require.False(t, hasTag, "tag should not be fetched") + + // Verify inner struct - only name should be present + innerMap, ok := fetchedMap["inner"].(map[string]interface{}) + require.True(t, ok) + require.Equal(t, "inner_name", innerMap["name"]) + + _, hasValue := innerMap["value"] + _, hasExtra := innerMap["extra"] + require.False(t, hasValue, "inner.value should not be fetched") + require.False(t, hasExtra, "inner.extra should not be fetched") + + // Assign with partial descriptor + dest := &protoOuter{ + Tag: "original_tag", + Inner: &protoInner{ + Value: 999, + Extra: "original_extra", + }, + } + + err = assignAny(partialDesc, fetched, dest) + require.NoError(t, err) + + // Verify assigned fields + require.Equal(t, 1, dest.ID) + require.Equal(t, "original_tag", dest.Tag) // preserved + + require.NotNil(t, dest.Inner) + require.Equal(t, "inner_name", dest.Inner.Name) + // These should be preserved from original dest.Inner + require.Equal(t, 999, dest.Inner.Value) + require.Equal(t, "original_extra", dest.Inner.Extra) +} + +// TestFetchAndAssign_DescShallowerThanNestedList tests when Descriptor is shallow for list of structs +func TestFetchAndAssign_DescShallowerThanNestedList(t *testing.T) { + // Thrift struct with list of nested structs + type thriftItem struct { + Name string `thrift:"Name,1" json:"name,omitempty"` + Value int `thrift:"Value,2" json:"value,omitempty"` + } + type thriftContainer struct { + Items []*thriftItem `thrift:"Items,1" json:"items,omitempty"` + } + + // Proto struct + type protoItem struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Value int `protobuf:"varint,2,opt,name=value" json:"value,omitempty"` + } + type protoContainer struct { + Items []*protoItem `protobuf:"bytes,1,rep,name=items" json:"items,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftContainer{ + Items: []*thriftItem{ + {Name: "item1", Value: 100}, + {Name: "item2", Value: 200}, + {Name: "item3", Value: 300}, + }, + } + + // Shallow descriptor - no Desc for items, so items are fetched as-is + shallowDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Container", + Children: []Field{ + {Name: "items", ID: 1}, // No Desc, so list is fetched as raw value + }, + } + + // Fetch with shallow descriptor + fetched, err := fetchAny(shallowDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Items should be fetched as raw slice + items, ok := fetchedMap["items"] + require.True(t, ok) + require.NotNil(t, items) + + // Assign with shallow descriptor + dest := &protoContainer{} + err = assignAny(shallowDesc, fetched, dest) + require.NoError(t, err) + + // Verify items are assigned + require.Len(t, dest.Items, 3) + require.Equal(t, "item1", dest.Items[0].Name) + require.Equal(t, 100, dest.Items[0].Value) + require.Equal(t, "item2", dest.Items[1].Name) + require.Equal(t, 200, dest.Items[1].Value) + require.Equal(t, "item3", dest.Items[2].Name) + require.Equal(t, 300, dest.Items[2].Value) +} + +// TestFetchAndAssign_DescShallowerThanNestedMap tests when Descriptor is shallow for map of structs +func TestFetchAndAssign_DescShallowerThanNestedMap(t *testing.T) { + // Thrift struct with map of nested structs + type thriftItem struct { + Name string `thrift:"Name,1" json:"name,omitempty"` + Value int `thrift:"Value,2" json:"value,omitempty"` + } + type thriftContainer struct { + Data map[string]*thriftItem `thrift:"Data,1" json:"data,omitempty"` + } + + // Proto struct + type protoItem struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Value int `protobuf:"varint,2,opt,name=value" json:"value,omitempty"` + } + type protoContainer struct { + Data map[string]*protoItem `protobuf:"bytes,1,rep,name=data" json:"data,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftContainer{ + Data: map[string]*thriftItem{ + "key1": {Name: "item1", Value: 100}, + "key2": {Name: "item2", Value: 200}, + }, + } + + // Shallow descriptor - no Desc for map values + shallowDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Container", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "DataMap", + Children: []Field{ + {Name: "*"}, // Wildcard, but no Desc for values -> fetch as raw + }, + }, + }, + }, + } + + // Fetch with shallow descriptor + fetched, err := fetchAny(shallowDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Data should be fetched as map + data, ok := fetchedMap["data"].(map[string]interface{}) + require.True(t, ok) + require.Len(t, data, 2) + + // Assign with shallow descriptor + dest := &protoContainer{} + err = assignAny(shallowDesc, fetched, dest) + require.NoError(t, err) + + // Verify data is assigned + require.Len(t, dest.Data, 2) + require.Equal(t, "item1", dest.Data["key1"].Name) + require.Equal(t, 100, dest.Data["key1"].Value) + require.Equal(t, "item2", dest.Data["key2"].Name) + require.Equal(t, 200, dest.Data["key2"].Value) +} + +// TestFetchAndAssign_PartialMapKeys tests when Descriptor only specifies some map keys +func TestFetchAndAssign_PartialMapKeys(t *testing.T) { + // Thrift struct with map + type thriftContainer struct { + Data map[string]int `thrift:"Data,1" json:"data,omitempty"` + } + + // Proto struct + type protoContainer struct { + Data map[string]int `protobuf:"bytes,1,rep,name=data" json:"data,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftContainer{ + Data: map[string]int{ + "key1": 100, + "key2": 200, + "key3": 300, + "key4": 400, + }, + } + + // Descriptor only specifies key1 and key3 (not key2, key4) + partialDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Container", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "DataMap", + Children: []Field{ + {Name: "key1"}, + {Name: "key3"}, + // key2 and key4 are not specified + }, + }, + }, + }, + } + + // Fetch with partial descriptor + fetched, err := fetchAny(partialDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // Only specified keys should be fetched + data, ok := fetchedMap["data"].(map[string]interface{}) + require.True(t, ok) + require.Len(t, data, 2) + require.Equal(t, 100, data["key1"]) + require.Equal(t, 300, data["key3"]) + + _, hasKey2 := data["key2"] + _, hasKey4 := data["key4"] + require.False(t, hasKey2, "key2 should not be fetched") + require.False(t, hasKey4, "key4 should not be fetched") + + // Assign with partial descriptor + dest := &protoContainer{ + Data: map[string]int{ + "key2": 999, // pre-existing key + }, + } + err = assignAny(partialDesc, fetched, dest) + require.NoError(t, err) + + // Verify only specified keys are assigned, pre-existing keys might be overwritten depending on implementation + // Based on current implementation, the map is replaced + require.Equal(t, 100, dest.Data["key1"]) + require.Equal(t, 300, dest.Data["key3"]) +} + +// TestFetchAndAssign_EmptyDescriptor tests when Descriptor has no children +func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { + type thriftStruct struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + FieldB string `thrift:"FieldB,2" json:"field_b,omitempty"` + } + + type protoStruct struct { + FieldA int `protobuf:"varint,1,req,name=field_a" json:"field_a,omitempty"` + FieldB string `protobuf:"bytes,2,opt,name=field_b" json:"field_b,omitempty"` + XXX_unrecognized []byte `json:"-"` + } + + src := &thriftStruct{ + FieldA: 100, + FieldB: "hello", + } + + // Empty descriptor - no children + emptyDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "EmptyStruct", + Children: []Field{}, + } + + // Fetch with empty descriptor + fetched, err := fetchAny(emptyDesc, src) + require.NoError(t, err) + + fetchedMap, ok := fetched.(map[string]interface{}) + require.True(t, ok) + + // No fields should be fetched + require.Len(t, fetchedMap, 0) + + // Assign with empty descriptor + dest := &protoStruct{ + FieldA: 999, + FieldB: "original", + } + err = assignAny(emptyDesc, fetched, dest) + require.NoError(t, err) + + // Original values should be preserved + require.Equal(t, 999, dest.FieldA) + require.Equal(t, "original", dest.FieldB) +} + +// ===================== Descriptor.String() Tests ===================== + +// TestDescriptor_String_Scalar tests String() for scalar type descriptor +func TestDescriptor_String_Scalar(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Leaf, + Type: "ScalarType", + } + + result := desc.String() + require.Equal(t, "-", result) +} + +// TestDescriptor_String_EmptyStruct tests String() for struct with no children +func TestDescriptor_String_EmptyStruct(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "EmptyStruct", + } + + result := desc.String() + require.Equal(t, "{}", result) +} + +// TestDescriptor_String_SimpleStruct tests String() for struct with scalar fields +func TestDescriptor_String_SimpleStruct(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SimpleStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_b", ID: 2}, + }, + } + + result := desc.String() + expected := `{ + "field_a": -, + "field_b": - +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_NestedStruct tests String() for nested struct +func TestDescriptor_String_NestedStruct(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "OuterStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "InnerStruct", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "value", ID: 2}, + }, + }, + }, + }, + } + + result := desc.String() + expected := `{ + "field_a": -, + "inner": { + "name": -, + "value": - + } +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_MapWithWildcard tests String() for map with "*" key +func TestDescriptor_String_MapWithWildcard(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "ContainerStruct", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "data", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "DataMap", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "ItemStruct", + Children: []Field{ + {Name: "name", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + result := desc.String() + expected := `{ + "id": -, + "data": { + "*": { + "name": - + } + } +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_MapWithSpecificKeys tests String() for map with specific keys +func TestDescriptor_String_MapWithSpecificKeys(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_StrMap, + Type: "SpecificKeyMap", + Children: []Field{ + {Name: "key1"}, + {Name: "key2"}, + { + Name: "key3", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "NestedType", + Children: []Field{ + {Name: "value", ID: 1}, + }, + }, + }, + }, + } + + result := desc.String() + expected := `{ + "key1": -, + "key2": -, + "key3": { + "value": - + } +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_DeeplyNested tests String() for deeply nested structure +func TestDescriptor_String_DeeplyNested(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Level1", + Children: []Field{ + { + Name: "level2", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Level2", + Children: []Field{ + { + Name: "level3", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Level3", + Children: []Field{ + {Name: "value", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + result := desc.String() + expected := `{ + "level2": { + "level3": { + "value": - + } + } +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_CircularReference tests String() handles circular references +func TestDescriptor_String_CircularReference(t *testing.T) { + // Create a descriptor that references itself + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SelfRef", + Children: []Field{ + {Name: "value", ID: 1}, + }, + } + // Add self-reference + desc.Children = append(desc.Children, Field{ + Name: "self", + ID: 2, + Desc: desc, // circular reference + }) + + result := desc.String() + expected := `{ + "value": -, + "self": +}` + require.Equal(t, expected, result) +} + +// TestDescriptor_String_MixedTypes tests String() for mixed struct and map types +func TestDescriptor_String_MixedTypes(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "MixedStruct", + Children: []Field{ + {Name: "scalar_field", ID: 1}, + { + Name: "struct_field", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "NestedStruct", + Children: []Field{ + {Name: "a", ID: 1}, + }, + }, + }, + { + Name: "map_field", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "NestedMap", + Children: []Field{ + {Name: "*"}, + }, + }, + }, + { + Name: "scalar_desc", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Leaf, + Type: "ScalarType", + }, + }, + }, + } + + result := desc.String() + expected := `{ + "scalar_field": -, + "struct_field": { + "a": - + }, + "map_field": { + "*": - + }, + "scalar_desc": - +}` + require.Equal(t, expected, result) +} diff --git a/trim/assign.go b/trim/assign.go new file mode 100644 index 00000000..3c3276f1 --- /dev/null +++ b/trim/assign.go @@ -0,0 +1,1216 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/cloudwego/dynamicgo/proto/binary" + "github.com/cloudwego/dynamicgo/proto/protowire" +) + +// AssignOptions contains options for AssignAny +type AssignOptions struct { + // DisallowNotDefined if true, returns ErrNotFound when a field/index/key is not found + DisallowNotDefined bool +} + +type Assigner struct { + AssignOptions +} + +// AssignAny assigns values from src (map[string]interface{}) to dest (protobuf struct) according to desc. +// For fields that exist in src but not in dest's struct definition (unknown fields): +// - They will be encoded to XXX_unrecognized field using protobuf binary encoding +// - Their raw values will also be stored in XXX_NoUnkeyedLiteral field (if present) as map[string]interface{} +// with field names as keys +// +// Warning: desc must be normalized before calling this method. +func (a Assigner) AssignAny(desc *Descriptor, src interface{}, dest interface{}) error { + if src == nil || dest == nil || desc == nil { + return nil + } + + // desc.Normalize() + + destValue := reflect.ValueOf(dest) + if destValue.Kind() != reflect.Ptr { + return fmt.Errorf("dest must be a pointer to struct") + } + + // Initialize path stack from pool + stack := getStackFrames() + defer putStackFrames(stack) + + return assignValue(desc, src, destValue.Elem(), &a.AssignOptions, stack) +} + +// pbStructFieldInfo caches field mapping information for a protobuf struct type +type pbStructFieldInfo struct { + // nameToFieldIndex maps field name (from protobuf tag) to struct field index + nameToFieldIndex map[string]int + // nameToFieldID maps field name to protobuf field ID + nameToFieldID map[string]int + // idToFieldIndex maps protobuf field ID to struct field index + idToFieldIndex map[int]int + // unrecognizedIndex is the index of XXX_unrecognized field, -1 if not present + unrecognizedIndex int + // noUnkeyedLiteralIndex is the index of XXX_NoUnkeyedLiteral field, -1 if not present + noUnkeyedLiteralIndex int +} + +// pbFieldCache caches the struct field info for each type +var pbFieldCache sync.Map // map[reflect.Type]*pbStructFieldInfo + +var ( + pbUnknownFieldName = "XXX_unrecognized" + pbUnknownFieldNameOnce sync.Once + NoUnkeyedLiteralFieldName = "XXX_NoUnkeyedLiteral" + NoUnkeyedLiteralFieldNameOnce sync.Once +) + +// SetPBUnknownFieldName sets the name of the field used to store unknown fields in protobuf structs +func SetPBUnknownFieldName(name string) { + pbUnknownFieldNameOnce.Do(func() { + pbUnknownFieldName = name + }) +} + +// SetPBNoUnkeyedLiteralFieldName sets the name of the field used to store unknown fields as raw values in protobuf structs +func SetPBNoUnkeyedLiteralFieldName(name string) { + NoUnkeyedLiteralFieldNameOnce.Do(func() { + NoUnkeyedLiteralFieldName = name + }) +} + +// getPBStructFieldInfo returns cached struct field info for the given protobuf type +func getPBStructFieldInfo(t reflect.Type) *pbStructFieldInfo { + if cached, ok := pbFieldCache.Load(t); ok { + return cached.(*pbStructFieldInfo) + } + + // Build the field info + numField := t.NumField() + info := &pbStructFieldInfo{ + nameToFieldIndex: make(map[string]int, numField), + nameToFieldID: make(map[string]int, numField), + idToFieldIndex: make(map[int]int, numField), + unrecognizedIndex: -1, + noUnkeyedLiteralIndex: -1, + } + + for i := 0; i < numField; i++ { + field := t.Field(i) + + // Check for XXX_unrecognized field + if field.Name == pbUnknownFieldName { + info.unrecognizedIndex = i + continue + } + + // Check for XXX_NoUnkeyedLiteral field + if field.Name == NoUnkeyedLiteralFieldName { + info.noUnkeyedLiteralIndex = i + continue + } + + tag := field.Tag.Get("protobuf") + if tag == "" { + continue + } + + // Parse protobuf tag: "varint,1,req,name=field_a" or "bytes,2,opt,name=field_b" + // Format: wireType,fieldID,cardinality,name=fieldName,... + parts := strings.Split(tag, ",") + if len(parts) < 4 { + continue + } + + // Parse field ID (second part) + fieldID, err := strconv.Atoi(parts[1]) + if err != nil { + continue + } + + // Parse field name (look for name=xxx) + var fieldName string + for _, part := range parts[3:] { + if strings.HasPrefix(part, "name=") { + fieldName = strings.TrimPrefix(part, "name=") + break + } + } + if fieldName == "" { + continue + } + + info.nameToFieldIndex[fieldName] = i + info.nameToFieldID[fieldName] = fieldID + info.idToFieldIndex[fieldID] = i + } + + // Store in cache (use LoadOrStore to handle concurrent initialization) + actual, _ := pbFieldCache.LoadOrStore(t, info) + return actual.(*pbStructFieldInfo) +} + +// assignValue is the internal implementation that works with reflect.Value directly +func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions, stack *pathStack) error { + if src == nil { + return nil + } + + // Dereference pointers on dest + for destValue.Kind() == reflect.Ptr { + if destValue.IsNil() { + // Allocate new value + destValue.Set(reflect.New(destValue.Type().Elem())) + } + destValue = destValue.Elem() + } + + switch desc.Kind { + case TypeKind_Struct: + return assignStruct(desc, src, destValue, opt, stack) + case TypeKind_StrMap: + return assignStrMap(desc, src, destValue, opt, stack) + case TypeKind_List: + return assignList(desc, src, destValue, opt, stack) + default: + return assignLeaf(reflect.ValueOf(src), destValue) + } +} + +// assignStruct handles TypeKind_Struct assignment +func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions, stack *pathStack) error { + srcMap, ok := src.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} for struct at %s, got %T", stack.buildPath(), src) + } + + if destValue.Kind() != reflect.Struct { + return fmt.Errorf("expected struct destination at %s, got %v", stack.buildPath(), destValue.Kind()) + } + + // Get cached field info for this type + fieldInfo := getPBStructFieldInfo(destValue.Type()) + + // Track which fields in srcMap are assigned to struct fields + unassignedFields := make(map[string]interface{}, len(srcMap)) + + // Iterate through srcMap and assign values + descFieldMap := desc.names + for key, value := range srcMap { + if value == nil { + continue + } + + // Find the descriptor field for this key + descField, hasDescField := descFieldMap[key] + fieldID := 0 + if hasDescField { + fieldID = descField.ID + } + + // Find struct field by name using cached index map + fieldIdx, found := fieldInfo.nameToFieldIndex[key] + if found { + fieldValue := destValue.Field(fieldIdx) + + // Make sure field is settable + if !fieldValue.CanSet() { + continue + } + + // Push field onto stack + stack.push(TypeKind_Struct, key, fieldID) + + // If field has a child descriptor, recursively assign + var err error + if hasDescField && descField.Desc != nil { + err = assignValueToField(descField.Desc, value, fieldValue, opt, stack) + } else { + // Otherwise, assign the value directly + err = assignLeaf(reflect.ValueOf(value), fieldValue) + } + + // Pop field from stack + stack.pop() + + if err != nil { + return err + } + } else if hasDescField { + // Field exists in descriptor but not in struct - encode to XXX_unrecognized + // This will be handled below + unassignedFields[key] = value + } else if opt.DisallowNotDefined { + // Include path in error message + stack.push(TypeKind_Struct, key, fieldID) + path := stack.buildPath() + stack.pop() + return ErrNotFound{Parent: desc, Field: Field{Name: key}, Msg: fmt.Sprintf("field '%s' not found in struct at path %s", key, path)} + } + } + + // Encode unassigned fields (from descriptor) to XXX_unrecognized + if len(unassignedFields) > 0 && fieldInfo.unrecognizedIndex >= 0 { + unrecognizedValue := destValue.Field(fieldInfo.unrecognizedIndex) + if unrecognizedValue.CanSet() { + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + for key, val := range unassignedFields { + // Encode this field to XXX_unrecognized + field := descFieldMap[key] + if err := encodeUnknownField(bp, field.ID, val, field.Desc); err != nil { + return fmt.Errorf("failed to encode unknown field '%s': %w", key, err) + } + } + + if len(bp.Buf) > 0 { + // Append to existing XXX_unrecognized if any + existingBytes := unrecognizedValue.Bytes() + newBytes := make([]byte, len(existingBytes)+len(bp.Buf)) + copy(newBytes, existingBytes) + copy(newBytes[len(existingBytes):], bp.Buf) + unrecognizedValue.SetBytes(newBytes) + } + } + } + + // Store unassigned fields as raw values to XXX_NoUnkeyedLiteral + if len(unassignedFields) > 0 && fieldInfo.noUnkeyedLiteralIndex >= 0 { + noUnkeyedLiteralValue := destValue.Field(fieldInfo.noUnkeyedLiteralIndex) + if noUnkeyedLiteralValue.CanSet() { + // Check if it's a map[string]interface{} or compatible type + if noUnkeyedLiteralValue.Kind() == reflect.Map { + // Initialize map if nil + if noUnkeyedLiteralValue.IsNil() { + noUnkeyedLiteralValue.Set(reflect.MakeMap(noUnkeyedLiteralValue.Type())) + } + + // Set each unassigned field to the map + for key, value := range unassignedFields { + noUnkeyedLiteralValue.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value)) + } + } + } + } + + return nil +} + +// assignValueToField assigns a value to a field, handling pointer allocation +func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Value, opt *AssignOptions, stack *pathStack) error { + // Handle pointer fields - allocate if needed + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + // Skip allocating for empty non-leaf sources to avoid clobbering existing data + if isEmptyNonLeaf(desc, src) { + return nil + } + + fieldValue.Set(reflect.New(fieldValue.Type().Elem())) + } + return assignValue(desc, src, fieldValue.Elem(), opt, stack) + } + return assignValue(desc, src, fieldValue, opt, stack) +} + +// isEmptyNonLeaf returns true if src is an empty composite value for the given descriptor. +// Used to avoid allocating new nested structs or slices when there is nothing to assign. +func isEmptyNonLeaf(desc *Descriptor, src interface{}) bool { + if desc == nil || src == nil { + return false + } + + switch desc.Kind { + case TypeKind_Struct, TypeKind_StrMap: + if m, ok := src.(map[string]interface{}); ok { + return len(m) == 0 + } + case TypeKind_List: + if s, ok := src.([]interface{}); ok { + return len(s) == 0 + } + } + + return false +} + +// assignStrMap handles TypeKind_StrMap assignment +func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions, stack *pathStack) error { + srcMap, ok := src.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} for strmap at %s, got %T", stack.buildPath(), src) + } + + if destValue.Kind() != reflect.Map { + return fmt.Errorf("expected map destination at %s, got %v", stack.buildPath(), destValue.Kind()) + } + + // Find wildcard or keyed descriptors + var wildcardDesc *Descriptor + if len(desc.Children) == 1 && desc.Children[0].Name == "*" { + wildcardDesc = desc.Children[0].Desc + } + keyDescMap := desc.names + + // Create a new map if nil + if destValue.IsNil() { + destValue.Set(reflect.MakeMap(destValue.Type())) + } + + elemType := destValue.Type().Elem() + + for key, value := range srcMap { + keyValue := reflect.ValueOf(key) + existing := destValue.MapIndex(keyValue) + elemValue := reflect.New(elemType).Elem() + if existing.IsValid() { + elemValue.Set(existing) + } + + // Find the appropriate descriptor for this entry + var childDesc *Descriptor + if wildcardDesc != nil { + childDesc = wildcardDesc + } else if child, ok := keyDescMap[key]; ok { + childDesc = child.Desc + } + + // Skip assigning empty non-leaf nodes to avoid overwriting existing values + if childDesc != nil && isEmptyNonLeaf(childDesc, value) { + if !existing.IsValid() { + continue + } + // keep existing value untouched + continue + } + + if value == nil { + // Preserve existing entry if present; otherwise set zero value + if existing.IsValid() { + destValue.SetMapIndex(keyValue, existing) + } + continue + } + + // Push map key onto stack + stack.push(TypeKind_StrMap, key, 0) + + var err error + if childDesc != nil { + if elemType.Kind() == reflect.Ptr { + ptrValue := elemValue + if elemValue.IsNil() { + ptrValue = reflect.New(elemType.Elem()) + } + err = assignValue(childDesc, value, ptrValue.Elem(), opt, stack) + if err == nil { + elemValue = ptrValue + } + } else { + err = assignValue(childDesc, value, elemValue, opt, stack) + } + } else { + err = assignLeaf(reflect.ValueOf(value), elemValue) + } + + // Pop map key from stack + stack.pop() + + if err != nil { + return err + } + + destValue.SetMapIndex(keyValue, elemValue) + } + + return nil +} + +// assignList handles TypeKind_List assignment +func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions, stack *pathStack) error { + srcSlice, ok := src.([]interface{}) + if !ok { + return fmt.Errorf("expected []interface{} for list at %s, got %T", stack.buildPath(), src) + } + + if destValue.Kind() != reflect.Slice && destValue.Kind() != reflect.Array { + return fmt.Errorf("expected slice or array destination at %s, got %v", stack.buildPath(), destValue.Kind()) + } + + childrenLen := len(desc.Children) + + // Fast path: only wildcard descriptor ("*" means all elements) + if childrenLen == 1 && desc.Children[0].Name == "*" { + wildcardDesc := desc.Children[0].Desc + srcLen := len(srcSlice) + + // Handle array (fixed size) + if destValue.Kind() == reflect.Array { + arrayLen := destValue.Len() + if srcLen != arrayLen { + return fmt.Errorf("array length mismatch at %s: expected %d, got %d", stack.buildPath(), arrayLen, srcLen) + } + for i := 0; i < srcLen; i++ { + elemValue := destValue.Index(i) + if srcSlice[i] == nil { + continue + } + + // Push list index onto stack + stack.push(TypeKind_List, "*", i) + + var err error + if wildcardDesc != nil { + err = assignValueToField(wildcardDesc, srcSlice[i], elemValue, opt, stack) + } else { + err = assignLeaf(reflect.ValueOf(srcSlice[i]), elemValue) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return err + } + } + return nil + } + + // Handle slice (dynamic size). Reuse existing slice when possible; only allocate when growing. + elemType := destValue.Type().Elem() + destLen := destValue.Len() + useSlice := destValue + if destLen < srcLen { + useSlice = reflect.MakeSlice(destValue.Type(), srcLen, srcLen) + if destLen > 0 { + reflect.Copy(useSlice, destValue) + } + } + + for i := 0; i < srcLen; i++ { + elemValue := useSlice.Index(i) + + // Skip empty non-leaf sources to avoid overwriting existing elements + if wildcardDesc != nil && isEmptyNonLeaf(wildcardDesc, srcSlice[i]) { + continue + } + + if srcSlice[i] == nil { + // Preserve existing value + continue + } + + // Push list index onto stack + stack.push(TypeKind_List, "*", i) + + var err error + if wildcardDesc != nil { + // Handle pointer element type without dropping existing value + if elemType.Kind() == reflect.Ptr { + targetPtr := elemValue + if elemValue.IsNil() { + targetPtr = reflect.New(elemType.Elem()) + } + err = assignValue(wildcardDesc, srcSlice[i], targetPtr.Elem(), opt, stack) + if err == nil { + elemValue.Set(targetPtr) + } + } else { + err = assignValue(wildcardDesc, srcSlice[i], elemValue, opt, stack) + } + } else { + err = assignLeaf(reflect.ValueOf(srcSlice[i]), elemValue) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return err + } + } + + if useSlice.Pointer() != destValue.Pointer() || destLen != useSlice.Len() { + destValue.Set(useSlice) + } + return nil + } + + // Specific indices requested + // This is a sparse assignment - we need to ensure the slice is large enough + maxIdx := -1 + for _, child := range desc.Children { + idx := child.ID + if idx > maxIdx { + maxIdx = idx + } + } + + if maxIdx < 0 { + // No valid indices + return nil + } + + // Handle array (fixed size) + if destValue.Kind() == reflect.Array { + arrayLen := destValue.Len() + if maxIdx >= arrayLen { + return fmt.Errorf("index %d out of bounds for array of length %d at %s", maxIdx, arrayLen, stack.buildPath()) + } + + // Find corresponding source elements by index + srcMap := make(map[int]interface{}) + for i, elem := range srcSlice { + if i < len(desc.Children) { + idx := desc.Children[i].ID + srcMap[idx] = elem + } + } + + for _, child := range desc.Children { + idx := child.ID + if idx < 0 || idx >= arrayLen { + if opt.DisallowNotDefined { + stack.push(TypeKind_List, "", idx) + path := stack.buildPath() + stack.pop() + return ErrNotFound{Parent: desc, Field: child, Msg: fmt.Sprintf("index %d out of bounds for array at path %s", idx, path)} + } + continue + } + + srcVal, hasSrc := srcMap[idx] + if !hasSrc { + if opt.DisallowNotDefined { + stack.push(TypeKind_List, "", idx) + path := stack.buildPath() + stack.pop() + return ErrNotFound{Parent: desc, Field: child, Msg: fmt.Sprintf("index %d not found in source at path %s", idx, path)} + } + continue + } + + elemValue := destValue.Index(idx) + + // Push list index onto stack + stack.push(TypeKind_List, "", idx) + + var err2 error + if child.Desc != nil { + err2 = assignValueToField(child.Desc, srcVal, elemValue, opt, stack) + } else { + err2 = assignLeaf(reflect.ValueOf(srcVal), elemValue) + } + + // Pop list index from stack + stack.pop() + + if err2 != nil { + return err2 + } + } + return nil + } + + // Handle slice (dynamic size) + // Create a slice large enough to hold all specified indices + requiredLen := maxIdx + 1 + elemType := destValue.Type().Elem() + newSlice := reflect.MakeSlice(destValue.Type(), requiredLen, requiredLen) + + // Copy existing elements if dest slice already has values + if !destValue.IsNil() && destValue.Len() > 0 { + reflect.Copy(newSlice, destValue) + } + + // Find corresponding source elements by index + srcMap := make(map[int]interface{}) + for i, elem := range srcSlice { + if i < len(desc.Children) { + idx := desc.Children[i].ID + srcMap[idx] = elem + } + } + + for _, child := range desc.Children { + idx := child.ID + + srcVal, hasSrc := srcMap[idx] + if !hasSrc { + if opt.DisallowNotDefined { + stack.push(TypeKind_List, "", idx) + path := stack.buildPath() + stack.pop() + return ErrNotFound{Parent: desc, Field: child, Msg: fmt.Sprintf("index %d not found in source at path %s", idx, path)} + } + continue + } + + elemValue := newSlice.Index(idx) + + // Push list index onto stack + stack.push(TypeKind_List, "", idx) + + var err2 error + if child.Desc != nil { + // Handle pointer element type + if elemType.Kind() == reflect.Ptr { + newElem := reflect.New(elemType.Elem()) + err2 = assignValue(child.Desc, srcVal, newElem.Elem(), opt, stack) + if err2 == nil { + elemValue.Set(newElem) + } + } else { + err2 = assignValue(child.Desc, srcVal, elemValue, opt, stack) + } + } else { + err2 = assignLeaf(reflect.ValueOf(srcVal), elemValue) + } + + // Pop list index from stack + stack.pop() + + if err2 != nil { + return err2 + } + } + + destValue.Set(newSlice) + return nil +} + +// jsonStructFieldInfo caches field mapping information for a struct type based on json tags +type jsonStructFieldInfo struct { + // jsonNameToFieldIndex maps json tag name to struct field index + jsonNameToFieldIndex map[string]int + // noUnkeyedLiteralIndex is the index of XXX_NoUnkeyedLiteral field, -1 if not present + noUnkeyedLiteralIndex int +} + +// jsonFieldCache caches the struct field info for each type +var jsonFieldCache sync.Map // map[reflect.Type]*jsonStructFieldInfo + +// getJSONStructFieldInfo returns cached struct field info for the given type based on json tags +func getJSONStructFieldInfo(t reflect.Type) *jsonStructFieldInfo { + if cached, ok := jsonFieldCache.Load(t); ok { + return cached.(*jsonStructFieldInfo) + } + + // Build the field info + numField := t.NumField() + info := &jsonStructFieldInfo{ + jsonNameToFieldIndex: make(map[string]int, numField), + } + + for i := 0; i < numField; i++ { + field := t.Field(i) + + if field.Name == NoUnkeyedLiteralFieldName { + info.noUnkeyedLiteralIndex = i + continue + } + + tag := field.Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + + // Parse json tag: "field_name" or "field_name,omitempty" + jsonName := tag + if idx := strings.IndexByte(tag, ','); idx >= 0 { + jsonName = tag[:idx] + } + if jsonName == "" { + continue + } + + info.jsonNameToFieldIndex[jsonName] = i + } + + // Store in cache (use LoadOrStore to handle concurrent initialization) + actual, _ := jsonFieldCache.LoadOrStore(t, info) + return actual.(*jsonStructFieldInfo) +} + +// AssignValue assigns values from src to dest by matching reflect-type or map-key or json-tag +func (Assigner) AssignValue(src interface{}, dest interface{}) error { + if src == nil || dest == nil { + return nil + } + + destValue := reflect.ValueOf(dest) + + if destValue.Kind() != reflect.Ptr { + return fmt.Errorf("dest must be a pointer") + } + + return assignLeaf(reflect.ValueOf(src), destValue) +} + +// assignStructToStruct assigns a struct to another struct by matching json tags +func assignStructToStruct(srcValue, destValue reflect.Value) error { + srcType := srcValue.Type() + destType := destValue.Type() + + // Get json field info for both types + srcInfo := getJSONStructFieldInfo(srcType) + destInfo := getJSONStructFieldInfo(destType) + + // Iterate through dest fields and find matching src fields by json tag + for jsonName, destIdx := range destInfo.jsonNameToFieldIndex { + srcIdx, found := srcInfo.jsonNameToFieldIndex[jsonName] + if !found { + continue + } + + srcField := srcValue.Field(srcIdx) + destField := destValue.Field(destIdx) + + if !destField.CanSet() { + continue + } + + // nil pointer in source struct: set dest to nil if pointer + if srcField.Kind() == reflect.Ptr && srcField.IsNil() { + if destField.Kind() == reflect.Ptr { + destField.Set(reflect.Zero(destField.Type())) + } + continue + } + + // Recursively assign the field value + if err := assignLeaf(srcField, destField); err != nil { + return err + } + } + + if srcInfo.noUnkeyedLiteralIndex >= 0 && destInfo.noUnkeyedLiteralIndex >= 0 { + // Special handling for XXX_NoUnkeyedLiteral field + // Copy the map if both src and dest have this field + srcField := srcValue.Field(srcInfo.noUnkeyedLiteralIndex) + destField := destValue.Field(destInfo.noUnkeyedLiteralIndex) + if destField.CanSet() && srcField.Kind() == reflect.Map && destField.Kind() == reflect.Map { + if !srcField.IsNil() && srcField.Len() > 0 { + // Initialize dest map if nil + if destField.IsNil() { + destField.Set(reflect.MakeMap(destField.Type())) + } + + // Copy all entries from src to dest + iter := srcField.MapRange() + for iter.Next() { + destField.SetMapIndex(iter.Key(), iter.Value()) + } + } + } + } + + return nil +} + +// assignMapToStruct assigns a map[string]interface{} to a struct by matching json tags +func assignMapToStruct(srcMap map[string]interface{}, destValue reflect.Value) error { + destType := destValue.Type() + + // Get json field info for dest type + destInfo := getJSONStructFieldInfo(destType) + + // Iterate through source map and find matching dest fields by json tag + for jsonName, srcVal := range srcMap { + destIdx, found := destInfo.jsonNameToFieldIndex[jsonName] + if !found { + continue + } + + destField := destValue.Field(destIdx) + if !destField.CanSet() { + continue + } + + if srcVal == nil { + // Set to zero value if source is nil + if destField.Kind() == reflect.Ptr || destField.Kind() == reflect.Slice || destField.Kind() == reflect.Map { + destField.Set(reflect.Zero(destField.Type())) + } + continue + } + + // Recursively assign the field value + if err := assignLeaf(reflect.ValueOf(srcVal), destField); err != nil { + return err + } + } + + return nil +} + +// assignLeaf assigns a reflect.Value to another reflect.Value +func assignLeaf(srcValue, destValue reflect.Value) error { + // Dereference pointer source + for srcValue.Kind() == reflect.Ptr { + if srcValue.IsNil() { + return nil + } + srcValue = srcValue.Elem() + } + + // Handle pointer destination + if destValue.Kind() == reflect.Ptr { + if destValue.IsNil() { + destValue.Set(reflect.New(destValue.Type().Elem())) + } + destValue = destValue.Elem() + } + + // Try direct assignment first + if srcValue.Type().AssignableTo(destValue.Type()) { + destValue.Set(srcValue) + return nil + } + + // Try conversion + if srcValue.Type().ConvertibleTo(destValue.Type()) { + destValue.Set(srcValue.Convert(destValue.Type())) + return nil + } + + // Handle interface{} source - extract the underlying value + if srcValue.Kind() == reflect.Interface { + if srcValue.IsNil() { + return nil + } + // Get the underlying value and use assignScalar instead + // because the underlying value could be map[string]interface{} + return assignLeaf(srcValue.Elem(), destValue) + } + + // Handle struct to struct mapping via json tags + if srcValue.Kind() == reflect.Struct && destValue.Kind() == reflect.Struct { + return assignStructToStruct(srcValue, destValue) + } + + // Handle slice to slice mapping + if srcValue.Kind() == reflect.Slice && destValue.Kind() == reflect.Slice { + return assignSliceToSlice(srcValue, destValue) + } + + // Handle map to map mapping + if srcValue.Kind() == reflect.Map && destValue.Kind() == reflect.Map { + return assignMapToMap(srcValue, destValue) + } + + // Handle map[string]interface{} to struct mapping + if srcValue.Kind() == reflect.Map && destValue.Kind() == reflect.Struct { + srcMapIface, ok := srcValue.Interface().(map[string]interface{}) + if ok { + return assignMapToStruct(srcMapIface, destValue) + } + } + + return fmt.Errorf("cannot assign %v to %v", srcValue.Type(), destValue.Type()) +} + +// assignSliceToSlice assigns a slice to another slice, converting elements as needed +func assignSliceToSlice(srcValue, destValue reflect.Value) error { + if srcValue.IsNil() { + destValue.Set(reflect.Zero(destValue.Type())) + return nil + } + srcLen := srcValue.Len() + destElemType := destValue.Type().Elem() + + // Create a new slice with the same length + newSlice := reflect.MakeSlice(destValue.Type(), srcLen, srcLen) + + for i := 0; i < srcLen; i++ { + srcElem := srcValue.Index(i) + destElem := newSlice.Index(i) + + // Handle pointer element type + if destElemType.Kind() == reflect.Ptr { + // Check if source element is nil pointer + if srcElem.Kind() == reflect.Ptr && srcElem.IsNil() { + // Keep destination as nil (zero value for pointer) + continue + } + // Check if source element is interface{} containing nil + if srcElem.Kind() == reflect.Interface && srcElem.IsNil() { + // Keep destination as nil (zero value for pointer) + continue + } + newElem := reflect.New(destElemType.Elem()) + if err := assignLeaf(srcElem, newElem.Elem()); err != nil { + return err + } + destElem.Set(newElem) + } else { + if err := assignLeaf(srcElem, destElem); err != nil { + return err + } + } + } + + destValue.Set(newSlice) + return nil +} + +// assignMapToMap assigns a map to another map, converting elements as needed +func assignMapToMap(srcValue, destValue reflect.Value) error { + if srcValue.IsNil() { + return nil + } + + destType := destValue.Type() + destKeyType := destType.Key() + destElemType := destType.Elem() + + // Create a new map + newMap := reflect.MakeMap(destType) + + iter := srcValue.MapRange() + for iter.Next() { + srcKey := iter.Key() + srcVal := iter.Value() + + // Convert key + var destKey reflect.Value + if srcKey.Type().AssignableTo(destKeyType) { + destKey = srcKey + } else if srcKey.Type().ConvertibleTo(destKeyType) { + destKey = srcKey.Convert(destKeyType) + } else { + return fmt.Errorf("cannot convert map key %v to %v", srcKey.Type(), destKeyType) + } + + // Convert value + destVal := reflect.New(destElemType).Elem() + if destElemType.Kind() == reflect.Ptr { + // Check if source value is nil pointer + if srcVal.Kind() == reflect.Ptr && srcVal.IsNil() { + // Keep destination as nil (zero value for pointer) + newMap.SetMapIndex(destKey, destVal) + continue + } + // Check if source value is interface{} containing nil + if srcVal.Kind() == reflect.Interface && srcVal.IsNil() { + // Keep destination as nil (zero value for pointer) + newMap.SetMapIndex(destKey, destVal) + continue + } + newElem := reflect.New(destElemType.Elem()) + if err := assignLeaf(srcVal, newElem.Elem()); err != nil { + return err + } + destVal.Set(newElem) + } else { + if err := assignLeaf(srcVal, destVal); err != nil { + return err + } + } + + newMap.SetMapIndex(destKey, destVal) + } + + destValue.Set(newMap) + return nil +} + +// encodeUnknownField encodes a field value to protobuf binary format +// desc: field descriptor for this value, can be nil for basic types +func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{}, desc *Descriptor) error { + if value == nil { + return nil + } + + // Handle nested structures with descriptor + if desc != nil { + switch desc.Kind { + case TypeKind_Struct: + // Encode as embedded message + subBp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(subBp) + + if m, ok := value.(map[string]interface{}); ok { + for key, val := range m { + if childField, ok := desc.names[key]; ok { + if err := encodeUnknownField(subBp, childField.ID, val, childField.Desc); err != nil { + return err + } + } + } + } + + bp.Buf = appendTag(bp.Buf, fieldID, 2) // length-delimited wire type + bp.WriteBytes(subBp.Buf) + return nil + + case TypeKind_List: + // Encode as repeated field + if arr, ok := value.([]interface{}); ok { + // Get the element descriptor (usually wildcard "*") + var elemDesc *Descriptor + if len(desc.Children) > 0 { + if desc.Children[0].Name == "*" { + elemDesc = desc.Children[0].Desc + } + } + + for _, elem := range arr { + if err := encodeUnknownField(bp, fieldID, elem, elemDesc); err != nil { + return err + } + } + } + return nil + + case TypeKind_StrMap: + // For map types, we need to encode each entry as a nested message + // with field 1 = key, field 2 = value + if m, ok := value.(map[string]interface{}); ok { + // Get the value descriptor (usually wildcard "*") + var valueDesc *Descriptor + if len(desc.Children) > 0 { + if desc.Children[0].Name == "*" { + valueDesc = desc.Children[0].Desc + } + } + + for key, val := range m { + // Each map entry is encoded as a nested message + subBp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(subBp) + + // Field 1: key (string) + subBp.Buf = appendTag(subBp.Buf, 1, 2) + subBp.WriteString(key) + + // Field 2: value + if err := encodeUnknownField(subBp, 2, val, valueDesc); err != nil { + return err + } + + // Write the map entry + bp.Buf = appendTag(bp.Buf, fieldID, 2) + bp.WriteBytes(subBp.Buf) + } + } + return nil + } + } + + // Fallback to type-based encoding for basic types or when no descriptor + switch v := value.(type) { + case bool: + // varint type for bool + bp.Buf = appendTag(bp.Buf, fieldID, 0) // varint wire type + bp.WriteBool(v) + + case int: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteInt64(int64(v)) + + case int32: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteInt32(v) + + case int64: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteInt64(v) + + case uint32: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteUint32(v) + + case uint64: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteUint64(v) + + case float32: + bp.Buf = appendTag(bp.Buf, fieldID, 5) // fixed32 wire type + bp.WriteFloat(v) + + case float64: + bp.Buf = appendTag(bp.Buf, fieldID, 1) // fixed64 wire type + bp.WriteDouble(v) + + case string: + bp.Buf = appendTag(bp.Buf, fieldID, 2) // length-delimited wire type + bp.WriteString(v) + + case []byte: + bp.Buf = appendTag(bp.Buf, fieldID, 2) + bp.WriteBytes(v) + + case []interface{}: + // Encode list as repeated field (without descriptor) + for _, elem := range v { + if err := encodeUnknownField(bp, fieldID, elem, nil); err != nil { + return err + } + } + + case map[string]interface{}: + // Encode as embedded message (without descriptor) + subBp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(subBp) + + for key, val := range v { + // For unknown map without descriptor, use hash-based field ID + if err := encodeUnknownField(subBp, hashFieldName(key), val, nil); err != nil { + return err + } + } + + bp.Buf = appendTag(bp.Buf, fieldID, 2) // length-delimited wire type + bp.WriteBytes(subBp.Buf) + + default: + return fmt.Errorf("unsupported type for unknown field encoding: %T", value) + } + + return nil +} + +// appendTag appends a protobuf tag to the buffer +// wireType: 0=varint, 1=fixed64, 2=length-delimited, 5=fixed32 +func appendTag(buf []byte, fieldNumber int, wireType int) []byte { + tag := uint64(fieldNumber)<<3 | uint64(wireType&7) + return protowire.AppendVarint(buf, tag) +} + +// hashFieldName generates a simple field ID from a field name (for unknown maps) +func hashFieldName(name string) int { + // Simple hash function - in practice you'd use a proper mapping + h := 0 + for _, c := range name { + h = h*31 + int(c) + } + if h < 0 { + h = -h + } + // Keep within valid protobuf field number range + return (h % 536870911) + 1 // Max valid field number is 2^29 - 1 +} diff --git a/trim/assign_test.go b/trim/assign_test.go new file mode 100644 index 00000000..6643f7f5 --- /dev/null +++ b/trim/assign_test.go @@ -0,0 +1,4142 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/bytedance/sonic" + "github.com/cloudwego/dynamicgo/proto/binary" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" +) + +func assignAny(desc *Descriptor, src interface{}, dest interface{}) error { + if desc != nil { + desc.Normalize() + } + assigner := &Assigner{} + return assigner.AssignAny(desc, src, dest) +} + +type sampleAssign struct { + XXX_NoUnkeyedLiteral map[string]interface{} `json:"-"` + FieldA int `protobuf:"varint,1,req,name=field_a" json:"field_a,omitempty"` + FieldB []*sampleAssign `protobuf:"bytes,2,opt,name=field_b" json:"field_b,omitempty"` + FieldC map[string]*sampleAssign `protobuf:"bytes,3,opt,name=field_c" json:"field_c,omitempty"` + FieldD *sampleAssign `protobuf:"bytes,4,opt,name=field_d" json:"field_d,omitempty"` + FieldE string `protobuf:"bytes,5,opt,name=field_e" json:"field_e,omitempty"` + FieldList []int `protobuf:"bytes,6,opt,name=field_list" json:"field_list,omitempty"` + FieldMap map[string]int `protobuf:"bytes,7,opt,name=field_map" json:"field_map,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func makeSampleAssign(width, depth int) *sampleAssign { + if width <= 0 || depth <= 0 { + return nil + } + ret := &sampleAssign{ + FieldA: 2, + FieldE: "2", + FieldC: make(map[string]*sampleAssign), + FieldList: []int{4, 5, 6}, + FieldMap: map[string]int{ + "4": 4, + "5": 5, + "6": 6, + }, + } + for i := 0; i < width; i++ { + ret.FieldB = append(ret.FieldB, makeSampleAssign(width, depth-1)) + ret.FieldC[fmt.Sprintf("%d", i)] = makeSampleAssign(width, depth-1) + } + ret.FieldD = makeSampleAssign(width, depth-1) + return ret +} + +// sampleAssignSmall is a struct with fewer fields than SampleAssign +// Used to test XXX_unrecognized field encoding +type sampleAssignSmall struct { + XXX_NoUnkeyedLiteral map[string]interface{} `json:"-"` + FieldA *int `protobuf:"varint,1,req,name=field_a"` + FieldE string `protobuf:"bytes,5,opt,name=field_e"` + XXX_unrecognized []byte `json:"-"` +} + +func intPtr(i int) *int { + return &i +} + +func TestAssignAny_Basic(t *testing.T) { + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + if dest.FieldE != "hello" { + t.Errorf("field_e: expected 'hello', got %v", dest.FieldE) + } +} + +func TestAssignAny_NestedStruct(t *testing.T) { + src := map[string]interface{}{ + "field_a": 1, + "field_d": map[string]interface{}{ + "field_a": 2, + "field_e": "nested", + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.FieldA != 1 { + t.Errorf("field_a: expected 1, got %v", dest.FieldA) + } + if dest.FieldD == nil { + t.Fatalf("field_d: expected non-nil") + } + if dest.FieldD.FieldA != 2 { + t.Errorf("field_d.field_a: expected 2, got %v", dest.FieldD.FieldA) + } + if dest.FieldD.FieldE != "nested" { + t.Errorf("field_d.field_e: expected 'nested', got %v", dest.FieldD.FieldE) + } +} + +func TestAssignAny_List(t *testing.T) { + src := map[string]interface{}{ + "field_list": []int{1, 2, 3}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_Leaf, + Type: "LIST", + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := []int{1, 2, 3} + if !reflect.DeepEqual(dest.FieldList, expected) { + t.Errorf("field_list: expected %v, got %v", expected, dest.FieldList) + } +} + +func TestAssignAny_Map(t *testing.T) { + src := map[string]interface{}{ + "field_map": map[string]interface{}{ + "key1": 100, + "key2": 200, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_map", + ID: 7, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + {Name: "*"}, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := map[string]int{"key1": 100, "key2": 200} + if !reflect.DeepEqual(dest.FieldMap, expected) { + t.Errorf("field_map: expected %v, got %v", expected, dest.FieldMap) + } +} + +func TestAssignAny_OnlyAssignLeafNodes(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{}, + map[string]interface{}{ + "field_a": 10, + "field_b": []interface{}{}, + }, + map[string]interface{}{ + "field_a": 10, + }, + }, + "field_c": map[string]interface{}{ + "empty": map[string]interface{}{}, + "non_empty": map[string]interface{}{ + "field_a": 10, + "field_c": map[string]interface{}{}, + }, + "add": map[string]interface{}{ + "field_a": 10, + }, + }, + "field_d": map[string]interface{}{}, + } + var desc = new(Descriptor) + *desc = Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1, Desc: &Descriptor{Kind: TypeKind_Leaf, Type: "INT32"}}, + {Name: "field_b", ID: 2, Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{{Name: "*", Desc: desc}}, + }}, + {Name: "field_c", ID: 3, Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{{Name: "*", Desc: desc}}, + }}, + { + Name: "field_d", + ID: 4, + Desc: desc, + }, + }, + } + + dest := &sampleAssign{ + FieldB: []*sampleAssign{ + {FieldA: 1, FieldE: "should not be cleared"}, + {FieldA: 1, FieldB: []*sampleAssign{{FieldA: 1}}, FieldE: "should not be cleared"}, + }, + FieldC: map[string]*sampleAssign{ + "empty": {FieldA: 1, FieldE: "should not be cleared"}, + "non_empty": {FieldA: 1, FieldC: map[string]*sampleAssign{"a": {FieldA: 1}}, FieldE: "should not be cleared"}, + }, + FieldD: &sampleAssign{FieldA: 1, FieldE: "should not be cleared"}, + } + + assigner := &Assigner{} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := &sampleAssign{ + FieldB: []*sampleAssign{ + {FieldA: 1, FieldE: "should not be cleared"}, + {FieldA: 10, FieldB: []*sampleAssign{{FieldA: 1}}, FieldE: "should not be cleared"}, + {FieldA: 10}, + }, + FieldC: map[string]*sampleAssign{ + "empty": {FieldA: 1, FieldE: "should not be cleared"}, + "non_empty": {FieldA: 10, FieldC: map[string]*sampleAssign{"a": {FieldA: 1}}, FieldE: "should not be cleared"}, + "add": {FieldA: 10}, + }, + FieldD: &sampleAssign{FieldA: 1, FieldE: "should not be cleared"}, + } + + require.Equal(t, expected, dest) +} + +// TestAssignAny_UnknownFields tests that the converted sample +// with XXX_unrecognized can be correctly serialized and deserialized +func TestAssignAny_UnknownFields(t *testing.T) { + // Step 1: Create a sampleAssignSmall with unknown fields + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + "unknown_int": 100, // Field ID 10 + "unknown_str": "secret", // Field ID 11 + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssignSmall", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + {Name: "unknown_int", ID: 10}, + {Name: "unknown_str", ID: 11}, + }, + } + + dest := &sampleAssignSmall{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Step 2: Serialize sampleAssignSmall to protobuf binary + // We need to manually serialize this since sampleAssignSmall is not a generated proto message + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + // Write field_a (field ID 1, varint) + if dest.FieldA != nil { + bp.AppendTag(1, 0) // 0 = varint wire type + bp.WriteInt32(int32(*dest.FieldA)) + } + + // Write field_e (field ID 5, string/bytes) + if dest.FieldE != "" { + bp.AppendTag(5, 2) // 2 = length-delimited wire type + bp.WriteString(dest.FieldE) + } + + // Write XXX_unrecognized fields (these contain unknown_int and unknown_str) + if len(dest.XXX_unrecognized) > 0 { + bp.Buf = append(bp.Buf, dest.XXX_unrecognized...) + } + + serializedData := bp.Buf + + // Step 3: Create a dynamic proto message descriptor using official protobuf reflect API + // This defines the full structure including all fields (known and unknown) + messageDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("SampleAssignFull"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("field_a"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("field_e"), + Number: proto.Int32(5), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("unknown_int"), + Number: proto.Int32(10), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT64.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("unknown_str"), + Number: proto.Int32(11), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + }, + } + + // Create file descriptor + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: proto.String("test.proto"), + Syntax: proto.String("proto3"), + MessageType: []*descriptorpb.DescriptorProto{messageDesc}, + } + + // Build the descriptor using protodesc + fd, err := protodesc.NewFile(fileDesc, nil) + if err != nil { + t.Fatalf("failed to create file descriptor: %v", err) + } + + msgDesc := fd.Messages().Get(0) + + // Step 4: Create a dynamic message and unmarshal using official proto.Unmarshal + dynamicMsg := dynamicpb.NewMessage(msgDesc) + err = proto.Unmarshal(serializedData, dynamicMsg) + if err != nil { + t.Fatalf("proto.Unmarshal failed: %v", err) + } + + // Step 5: Verify that all fields are correctly deserialized + fields := dynamicMsg.Descriptor().Fields() + + fieldA := dynamicMsg.Get(fields.ByNumber(1)).Int() + if fieldA != 42 { + t.Errorf("field_a: expected 42, got %v", fieldA) + } + + fieldE := dynamicMsg.Get(fields.ByNumber(5)).String() + if fieldE != "hello" { + t.Errorf("field_e: expected 'hello', got %v", fieldE) + } + + unknownInt := dynamicMsg.Get(fields.ByNumber(10)).Int() + if unknownInt != 100 { + t.Errorf("unknown_int: expected 100, got %v", unknownInt) + } + + unknownStr := dynamicMsg.Get(fields.ByNumber(11)).String() + if unknownStr != "secret" { + t.Errorf("unknown_str: expected 'secret', got %v", unknownStr) + } + + t.Logf("Successfully verified protobuf serialization with unknown fields using official proto") + t.Logf(" field_a: %v", fieldA) + t.Logf(" field_e: %v", fieldE) + t.Logf(" unknown_int: %v", unknownInt) + t.Logf(" unknown_str: %v", unknownStr) +} + +// Test struct for nested unknown fields +type sampleNestedUnknown struct { + FieldA int `protobuf:"varint,1,opt,name=field_a" json:"field_a,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func TestEncodeUnknownField_NestedStruct(t *testing.T) { + // Test nested struct encoding + src := map[string]interface{}{ + "field_a": 42, + "nested_struct": map[string]interface{}{ + "inner_field1": "hello", + "inner_field2": int64(123), + }, + } + + // Create descriptor for the struct with nested struct field + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_struct", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "NestedStruct", + Children: []Field{ + {Name: "inner_field1", ID: 1}, + {Name: "inner_field2", ID: 2}, + }, + }, + }, + }, + } + + dest := &sampleNestedUnknown{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify field_a is assigned correctly + if dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + + // Verify XXX_unrecognized contains the nested struct + if len(dest.XXX_unrecognized) == 0 { + t.Fatal("XXX_unrecognized should not be empty") + } + + // Create protobuf descriptor for verification + messageDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("SampleNestedUnknown"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("field_a"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("nested_struct"), + Number: proto.Int32(2), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + TypeName: proto.String(".NestedStruct"), + }, + }, + } + + nestedMessageDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("NestedStruct"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("inner_field1"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("inner_field2"), + Number: proto.Int32(2), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT64.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + }, + } + + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: proto.String("test.proto"), + Syntax: proto.String("proto3"), + MessageType: []*descriptorpb.DescriptorProto{messageDesc, nestedMessageDesc}, + } + + fd, err := protodesc.NewFile(fileDesc, nil) + if err != nil { + t.Fatalf("failed to create file descriptor: %v", err) + } + + msgDesc := fd.Messages().Get(0) + + // Serialize the complete message + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + bp.AppendTag(1, 0) + bp.WriteInt32(int32(dest.FieldA)) + bp.Buf = append(bp.Buf, dest.XXX_unrecognized...) + + // Unmarshal and verify + dynamicMsg := dynamicpb.NewMessage(msgDesc) + err = proto.Unmarshal(bp.Buf, dynamicMsg) + if err != nil { + t.Fatalf("proto.Unmarshal failed: %v", err) + } + + fields := dynamicMsg.Descriptor().Fields() + fieldA := dynamicMsg.Get(fields.ByNumber(1)).Int() + if fieldA != 42 { + t.Errorf("field_a: expected 42, got %v", fieldA) + } + + nestedMsg := dynamicMsg.Get(fields.ByNumber(2)).Message() + nestedFields := nestedMsg.Descriptor().Fields() + innerField1 := nestedMsg.Get(nestedFields.ByNumber(1)).String() + innerField2 := nestedMsg.Get(nestedFields.ByNumber(2)).Int() + + if innerField1 != "hello" { + t.Errorf("nested_struct.inner_field1: expected 'hello', got %v", innerField1) + } + if innerField2 != 123 { + t.Errorf("nested_struct.inner_field2: expected 123, got %v", innerField2) + } + + t.Logf("Successfully verified nested struct encoding") + t.Logf(" field_a: %v", fieldA) + t.Logf(" nested_struct.inner_field1: %v", innerField1) + t.Logf(" nested_struct.inner_field2: %v", innerField2) +} + +func TestEncodeUnknownField_NestedList(t *testing.T) { + // Test nested list encoding + src := map[string]interface{}{ + "field_a": 42, + "nested_list": []interface{}{ + map[string]interface{}{ + "item_field": "item1", + }, + map[string]interface{}{ + "item_field": "item2", + }, + }, + } + + // Create descriptor for the struct with nested list field + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_list", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "NestedList", + Children: []Field{ + { + Name: "*", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "ListItem", + Children: []Field{ + {Name: "item_field", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleNestedUnknown{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify field_a is assigned correctly + if dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + + // Verify XXX_unrecognized contains the nested list + if len(dest.XXX_unrecognized) == 0 { + t.Fatal("XXX_unrecognized should not be empty") + } + + // Create protobuf descriptor for verification + listItemDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("ListItem"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("item_field"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + }, + } + + messageDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("SampleNestedUnknown"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("field_a"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("nested_list"), + Number: proto.Int32(3), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum(), + TypeName: proto.String(".ListItem"), + }, + }, + } + + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: proto.String("test.proto"), + Syntax: proto.String("proto3"), + MessageType: []*descriptorpb.DescriptorProto{messageDesc, listItemDesc}, + } + + fd, err := protodesc.NewFile(fileDesc, nil) + if err != nil { + t.Fatalf("failed to create file descriptor: %v", err) + } + + msgDesc := fd.Messages().Get(0) + + // Serialize the complete message + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + bp.AppendTag(1, 0) + bp.WriteInt32(int32(dest.FieldA)) + bp.Buf = append(bp.Buf, dest.XXX_unrecognized...) + + // Unmarshal and verify + dynamicMsg := dynamicpb.NewMessage(msgDesc) + err = proto.Unmarshal(bp.Buf, dynamicMsg) + if err != nil { + t.Fatalf("proto.Unmarshal failed: %v", err) + } + + fields := dynamicMsg.Descriptor().Fields() + fieldA := dynamicMsg.Get(fields.ByNumber(1)).Int() + if fieldA != 42 { + t.Errorf("field_a: expected 42, got %v", fieldA) + } + + nestedList := dynamicMsg.Get(fields.ByNumber(3)).List() + if nestedList.Len() != 2 { + t.Fatalf("nested_list: expected 2 items, got %v", nestedList.Len()) + } + + for i := 0; i < nestedList.Len(); i++ { + itemMsg := nestedList.Get(i).Message() + itemFields := itemMsg.Descriptor().Fields() + itemField := itemMsg.Get(itemFields.ByNumber(1)).String() + expectedValue := "item" + string(rune('1'+i)) + if itemField != expectedValue { + t.Errorf("nested_list[%d].item_field: expected '%s', got %v", i, expectedValue, itemField) + } + t.Logf(" nested_list[%d].item_field: %v", i, itemField) + } + + t.Logf("Successfully verified nested list encoding") +} + +func TestEncodeUnknownField_NestedMap(t *testing.T) { + // Test nested map encoding + src := map[string]interface{}{ + "field_a": 42, + "nested_map": map[string]interface{}{ + "key1": map[string]interface{}{ + "value_field": "value1", + }, + "key2": map[string]interface{}{ + "value_field": "value2", + }, + }, + } + + // Create descriptor for the struct with nested map field + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_map", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "NestedMap", + Children: []Field{ + { + Name: "*", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "MapValue", + Children: []Field{ + {Name: "value_field", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleNestedUnknown{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify field_a is assigned correctly + if dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + + // Verify XXX_unrecognized contains the nested map + if len(dest.XXX_unrecognized) == 0 { + t.Fatal("XXX_unrecognized should not be empty") + } + + // Create protobuf descriptor for verification + mapValueDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("MapValue"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("value_field"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + }, + } + + // Protobuf maps are represented as repeated message with key/value fields + mapEntryDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("NestedMapEntry"), + Options: &descriptorpb.MessageOptions{ + MapEntry: proto.Bool(true), + }, + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("key"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("value"), + Number: proto.Int32(2), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + TypeName: proto.String(".MapValue"), + }, + }, + } + + messageDesc := &descriptorpb.DescriptorProto{ + Name: proto.String("SampleNestedUnknown"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("field_a"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + }, + { + Name: proto.String("nested_map"), + Number: proto.Int32(4), + Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + Label: descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum(), + TypeName: proto.String(".SampleNestedUnknown.NestedMapEntry"), + }, + }, + } + + // Add the map entry as a nested message within the parent message + messageDesc.NestedType = []*descriptorpb.DescriptorProto{mapEntryDesc} + + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: proto.String("test.proto"), + Syntax: proto.String("proto3"), + MessageType: []*descriptorpb.DescriptorProto{messageDesc, mapValueDesc}, + } + + fd, err := protodesc.NewFile(fileDesc, nil) + if err != nil { + t.Fatalf("failed to create file descriptor: %v", err) + } + + msgDesc := fd.Messages().Get(0) + + // Serialize the complete message + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + bp.AppendTag(1, 0) + bp.WriteInt32(int32(dest.FieldA)) + bp.Buf = append(bp.Buf, dest.XXX_unrecognized...) + + // Unmarshal and verify + dynamicMsg := dynamicpb.NewMessage(msgDesc) + err = proto.Unmarshal(bp.Buf, dynamicMsg) + if err != nil { + t.Fatalf("proto.Unmarshal failed: %v", err) + } + + fields := dynamicMsg.Descriptor().Fields() + fieldA := dynamicMsg.Get(fields.ByNumber(1)).Int() + if fieldA != 42 { + t.Errorf("field_a: expected 42, got %v", fieldA) + } + + nestedMap := dynamicMsg.Get(fields.ByNumber(4)).Map() + if nestedMap.Len() != 2 { + t.Fatalf("nested_map: expected 2 entries, got %v", nestedMap.Len()) + } + + // Collect map entries + mapEntries := make(map[string]string) + nestedMap.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + key := k.String() + valueMsg := v.Message() + valueFields := valueMsg.Descriptor().Fields() + value := valueMsg.Get(valueFields.ByNumber(1)).String() + mapEntries[key] = value + t.Logf(" nested_map[%s].value_field: %v", key, value) + return true + }) + + if mapEntries["key1"] != "value1" { + t.Errorf("nested_map[key1].value_field: expected 'value1', got %v", mapEntries["key1"]) + } + if mapEntries["key2"] != "value2" { + t.Errorf("nested_map[key2].value_field: expected 'value2', got %v", mapEntries["key2"]) + } + + t.Logf("Successfully verified nested map encoding") +} + +// TestAssignAny_NoUnkeyedLiteral tests that unknown fields are also stored in XXX_NoUnkeyedLiteral +func TestAssignAny_NoUnkeyedLiteral(t *testing.T) { + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + "unknown_int": 100, + "unknown_str": "secret", + "unknown_map": map[string]interface{}{ + "nested_key": "nested_value", + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssignSmall", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + {Name: "unknown_int", ID: 10}, + {Name: "unknown_str", ID: 11}, + {Name: "unknown_map", ID: 12}, + }, + } + + dest := &sampleAssignSmall{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify known fields + if dest.FieldA == nil || *dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + if dest.FieldE != "hello" { + t.Errorf("field_e: expected 'hello', got %v", dest.FieldE) + } + + // Verify XXX_unrecognized is set (protobuf binary encoded) + if len(dest.XXX_unrecognized) == 0 { + t.Errorf("XXX_unrecognized: expected non-empty, got empty") + } + + // Verify XXX_NoUnkeyedLiteral contains the raw unknown field values + if dest.XXX_NoUnkeyedLiteral == nil { + t.Fatalf("XXX_NoUnkeyedLiteral: expected non-nil map") + } + + // Check unknown_int + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_int"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_int' key") + } else if intVal, ok := val.(int); !ok || intVal != 100 { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_int']: expected 100, got %v (type %T)", val, val) + } + + // Check unknown_str + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_str"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_str' key") + } else if strVal, ok := val.(string); !ok || strVal != "secret" { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_str']: expected 'secret', got %v (type %T)", val, val) + } + + // Check unknown_map + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_map"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_map' key") + } else if mapVal, ok := val.(map[string]interface{}); !ok { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_map']: expected map[string]interface{}, got %T", val) + } else if nestedVal, ok := mapVal["nested_key"]; !ok || nestedVal != "nested_value" { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_map']['nested_key']: expected 'nested_value', got %v", nestedVal) + } + + t.Logf("Successfully verified XXX_NoUnkeyedLiteral and XXX_unrecognized for unknown fields") + t.Logf(" field_a: %v", *dest.FieldA) + t.Logf(" field_e: %v", dest.FieldE) + t.Logf(" XXX_unrecognized length: %d", len(dest.XXX_unrecognized)) + t.Logf(" XXX_NoUnkeyedLiteral: %+v", dest.XXX_NoUnkeyedLiteral) +} + +func TestAssignAny_ListOfStructs(t *testing.T) { + + src := map[string]interface{}{ + "field_b": []*sampleAssign{ + { + FieldA: 2, + FieldE: "first", + }, + { + FieldA: 2, + FieldE: "second", + }, + }, + } + + // nestedDesc := &Descriptor{ + // Kind: TypeKind_Struct, + // Name: "SampleAssign", + // Children: []Field{ + // {Name: "field_a", ID: 1}, + // {Name: "field_e", ID: 5}, + // }, + // } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Leaf, + Type: "LIST", + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if len(dest.FieldB) != 2 { + t.Fatalf("field_b: expected length 2, got %d", len(dest.FieldB)) + } + + if dest.FieldB[0].FieldA != 2 { + t.Errorf("field_b[0].field_a: expected 1, got %v", dest.FieldB[0].FieldA) + } + if dest.FieldB[0].FieldE != "first" { + t.Errorf("field_b[0].field_e: expected 'first', got %v", dest.FieldB[0].FieldE) + } + if dest.FieldB[1].FieldA != 2 { + t.Errorf("field_b[1].field_a: expected 2, got %v", dest.FieldB[1].FieldA) + } + if dest.FieldB[1].FieldE != "second" { + t.Errorf("field_b[1].field_e: expected 'second', got %v", dest.FieldB[1].FieldE) + } +} + +func TestAssignAny_MapOfStructs(t *testing.T) { + src := map[string]interface{}{ + "field_c": map[string]interface{}{ + "key1": map[string]interface{}{ + "field_a": 10, + "field_e": "value1", + }, + "key2": map[string]interface{}{ + "field_a": 20, + "field_e": "value2", + }, + }, + } + + nestedDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + {Name: "*", Desc: nestedDesc}, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if len(dest.FieldC) != 2 { + t.Fatalf("field_c: expected length 2, got %d", len(dest.FieldC)) + } + + if dest.FieldC["key1"] == nil { + t.Fatalf("field_c['key1']: expected non-nil") + } + if dest.FieldC["key1"].FieldA != 10 { + t.Errorf("field_c['key1'].field_a: expected 10, got %v", dest.FieldC["key1"].FieldA) + } + if dest.FieldC["key1"].FieldE != "value1" { + t.Errorf("field_c['key1'].field_e: expected 'value1', got %v", dest.FieldC["key1"].FieldE) + } +} + +// TestAssignAny_ListWithSpecificIndices tests assigning list elements by specific indices or wildcard +func TestAssignAny_ListWithSpecificIndices(t *testing.T) { + // Test case 1: Wildcard - assign all elements + t.Run("wildcard_all_elements", func(t *testing.T) { + src := map[string]interface{}{ + "field_list": []interface{}{10, 20, 30, 40, 50}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "*"}, // wildcard - all elements + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := []int{10, 20, 30, 40, 50} + if !reflect.DeepEqual(dest.FieldList, expected) { + t.Errorf("field_list: expected %v, got %v", expected, dest.FieldList) + } + }) + + // Test case 2: Specific indices (0, 2, 4) + // The source array's elements are mapped to destination indices specified by Field.ID + // src[0] (10) -> dest[0], src[1] (20) -> dest[2], src[2] (30) -> dest[4] + t.Run("specific_indices", func(t *testing.T) { + src := map[string]interface{}{ + "field_list": []interface{}{10, 20, 30}, // 3 elements + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "0", ID: 0}, // src[0] -> dest[0] + {Name: "2", ID: 2}, // src[1] -> dest[2] + {Name: "4", ID: 4}, // src[2] -> dest[4] + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Should create a slice with length maxIdx+1 = 5 + if len(dest.FieldList) != 5 { + t.Fatalf("field_list: expected length 5, got %d", len(dest.FieldList)) + } + + // Check that specific indices are assigned + if dest.FieldList[0] != 10 { + t.Errorf("field_list[0]: expected 10, got %v", dest.FieldList[0]) + } + if dest.FieldList[2] != 20 { + t.Errorf("field_list[2]: expected 20, got %v", dest.FieldList[2]) + } + if dest.FieldList[4] != 30 { + t.Errorf("field_list[4]: expected 30, got %v", dest.FieldList[4]) + } + + // Indices 1 and 3 should be zero values + if dest.FieldList[1] != 0 { + t.Errorf("field_list[1]: expected 0 (zero value), got %v", dest.FieldList[1]) + } + if dest.FieldList[3] != 0 { + t.Errorf("field_list[3]: expected 0 (zero value), got %v", dest.FieldList[3]) + } + }) + + // Test case 3: Mapping to non-contiguous indices + // src[0] -> dest[0], src[1] -> dest[1], src[2] -> dest[10] + t.Run("non_contiguous_indices", func(t *testing.T) { + src := map[string]interface{}{ + "field_list": []interface{}{10, 20, 30}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "0", ID: 0}, // src[0] -> dest[0] + {Name: "1", ID: 1}, // src[1] -> dest[1] + {Name: "10", ID: 10}, // src[2] -> dest[10] + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Should create a slice with length maxIdx+1 = 11 + if len(dest.FieldList) != 11 { + t.Fatalf("field_list: expected length 11, got %d", len(dest.FieldList)) + } + + // Check assigned values + if dest.FieldList[0] != 10 { + t.Errorf("field_list[0]: expected 10, got %v", dest.FieldList[0]) + } + if dest.FieldList[1] != 20 { + t.Errorf("field_list[1]: expected 20, got %v", dest.FieldList[1]) + } + if dest.FieldList[10] != 30 { + t.Errorf("field_list[10]: expected 30, got %v", dest.FieldList[10]) + } + + // Indices 2-9 should be zero values + for i := 2; i < 10; i++ { + if dest.FieldList[i] != 0 { + t.Errorf("field_list[%d]: expected 0 (zero value), got %v", i, dest.FieldList[i]) + } + } + }) + + // Test case 4: DisallowNotDefined with insufficient source elements + // Descriptor requires 3 elements but source only has 2 + t.Run("disallow_not_defined_insufficient_source", func(t *testing.T) { + src := map[string]interface{}{ + "field_list": []interface{}{10, 20}, // only 2 elements + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "0", ID: 0}, // src[0] -> dest[0] + {Name: "1", ID: 1}, // src[1] -> dest[1] + {Name: "2", ID: 2}, // src[2] doesn't exist! + }, + }, + }, + }, + } + + assigner := Assigner{AssignOptions: AssignOptions{DisallowNotDefined: true}} + dest := &sampleAssign{} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err == nil { + t.Fatalf("expected ErrNotFound, got nil") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T: %v", err, err) + } + if notFoundErr.Parent.Type != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Type) + } + }) + + // Test case 5: List with nested structures + t.Run("list_with_nested_structs_wildcard", func(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{ + "field_a": 1, + "field_e": "first", + }, + map[string]interface{}{ + "field_a": 2, + "field_e": "second", + }, + map[string]interface{}{ + "field_a": 3, + "field_e": "third", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if len(dest.FieldB) != 3 { + t.Fatalf("field_b: expected length 3, got %d", len(dest.FieldB)) + } + + if dest.FieldB[0].FieldA != 1 { + t.Errorf("field_b[0].field_a: expected 1, got %v", dest.FieldB[0].FieldA) + } + if dest.FieldB[0].FieldE != "first" { + t.Errorf("field_b[0].field_e: expected 'first', got %v", dest.FieldB[0].FieldE) + } + + if dest.FieldB[2].FieldA != 3 { + t.Errorf("field_b[2].field_a: expected 3, got %v", dest.FieldB[2].FieldA) + } + if dest.FieldB[2].FieldE != "third" { + t.Errorf("field_b[2].field_e: expected 'third', got %v", dest.FieldB[2].FieldE) + } + }) + + // Test case 6: List with nested structures and specific indices + // src[0] -> dest[0], src[1] -> dest[2] (Note: assign copies ALL available fields from source) + t.Run("list_with_nested_structs_specific_indices", func(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{ + "field_a": 1, + "field_e": "first", + }, + map[string]interface{}{ + "field_a": 2, + "field_e": "second", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "0", + ID: 0, // src[0] -> dest[0] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + { + Name: "2", + ID: 2, // src[1] -> dest[2] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Should create a slice with length maxIdx+1 = 3 + if len(dest.FieldB) != 3 { + t.Fatalf("field_b: expected length 3, got %d", len(dest.FieldB)) + } + + // Check first element (src[0] -> dest[0]) + if dest.FieldB[0] == nil { + t.Fatalf("field_b[0]: expected non-nil") + } + if dest.FieldB[0].FieldA != 1 { + t.Errorf("field_b[0].field_a: expected 1, got %v", dest.FieldB[0].FieldA) + } + if dest.FieldB[0].FieldE != "first" { + t.Errorf("field_b[0].field_e: expected 'first', got %v", dest.FieldB[0].FieldE) + } + + // Index 1 should be nil (not assigned) + if dest.FieldB[1] != nil { + t.Errorf("field_b[1]: expected nil, got %v", dest.FieldB[1]) + } + + // Check element at index 2 (src[1] -> dest[2]) + if dest.FieldB[2] == nil { + t.Fatalf("field_b[2]: expected non-nil") + } + if dest.FieldB[2].FieldA != 2 { + t.Errorf("field_b[2].field_a: expected 2, got %v", dest.FieldB[2].FieldA) + } + if dest.FieldB[2].FieldE != "second" { + t.Errorf("field_b[2].field_e: expected 'second', got %v", dest.FieldB[2].FieldE) + } + }) + + // Test case 7: Preserve existing slice elements not accessed by descriptor + // dest already has [elem0, elem1, elem2, elem3], descriptor only modifies indices 1 and 3 + t.Run("preserve_unmodified_elements", func(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{ + "field_a": 100, + "field_e": "new_first", + }, + map[string]interface{}{ + "field_a": 200, + "field_e": "new_second", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "1", + ID: 1, // src[0] -> dest[1] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + { + Name: "3", + ID: 3, // src[1] -> dest[3] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + // Pre-populate dest with existing elements + dest := &sampleAssign{ + FieldB: []*sampleAssign{ + {FieldA: 1, FieldE: "original_0"}, + {FieldA: 2, FieldE: "original_1"}, + {FieldA: 3, FieldE: "original_2"}, + {FieldA: 4, FieldE: "original_3"}, + }, + } + + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Should have 4 elements (maxIdx+1 = 4) + if len(dest.FieldB) != 4 { + t.Fatalf("field_b: expected length 4, got %d", len(dest.FieldB)) + } + + // Index 0 should be preserved (not modified) + if dest.FieldB[0] == nil { + t.Fatalf("field_b[0]: expected non-nil") + } + if dest.FieldB[0].FieldA != 1 { + t.Errorf("field_b[0].field_a: expected 1 (preserved), got %v", dest.FieldB[0].FieldA) + } + if dest.FieldB[0].FieldE != "original_0" { + t.Errorf("field_b[0].field_e: expected 'original_0' (preserved), got %v", dest.FieldB[0].FieldE) + } + + // Index 1 should be overwritten by src[0] + if dest.FieldB[1] == nil { + t.Fatalf("field_b[1]: expected non-nil") + } + if dest.FieldB[1].FieldA != 100 { + t.Errorf("field_b[1].field_a: expected 100 (overwritten), got %v", dest.FieldB[1].FieldA) + } + if dest.FieldB[1].FieldE != "new_first" { + t.Errorf("field_b[1].field_e: expected 'new_first' (overwritten), got %v", dest.FieldB[1].FieldE) + } + + // Index 2 should be preserved (not modified) + if dest.FieldB[2] == nil { + t.Fatalf("field_b[2]: expected non-nil") + } + if dest.FieldB[2].FieldA != 3 { + t.Errorf("field_b[2].field_a: expected 3 (preserved), got %v", dest.FieldB[2].FieldA) + } + if dest.FieldB[2].FieldE != "original_2" { + t.Errorf("field_b[2].field_e: expected 'original_2' (preserved), got %v", dest.FieldB[2].FieldE) + } + + // Index 3 should be overwritten by src[1] + if dest.FieldB[3] == nil { + t.Fatalf("field_b[3]: expected non-nil") + } + if dest.FieldB[3].FieldA != 200 { + t.Errorf("field_b[3].field_a: expected 200 (overwritten), got %v", dest.FieldB[3].FieldA) + } + if dest.FieldB[3].FieldE != "new_second" { + t.Errorf("field_b[3].field_e: expected 'new_second' (overwritten), got %v", dest.FieldB[3].FieldE) + } + }) + + // Test case 8: Expand existing slice when descriptor requires larger size + t.Run("expand_existing_slice", func(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{ + "field_a": 999, + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "5", + ID: 5, // src[0] -> dest[5] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + // Pre-populate dest with smaller slice + dest := &sampleAssign{ + FieldB: []*sampleAssign{ + {FieldA: 1, FieldE: "keep_0"}, + {FieldA: 2, FieldE: "keep_1"}, + }, + } + + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Should expand to length 6 (maxIdx+1 = 6) + if len(dest.FieldB) != 6 { + t.Fatalf("field_b: expected length 6, got %d", len(dest.FieldB)) + } + + // Original elements should be preserved + if dest.FieldB[0].FieldA != 1 || dest.FieldB[0].FieldE != "keep_0" { + t.Errorf("field_b[0]: expected {1, keep_0}, got {%v, %v}", dest.FieldB[0].FieldA, dest.FieldB[0].FieldE) + } + if dest.FieldB[1].FieldA != 2 || dest.FieldB[1].FieldE != "keep_1" { + t.Errorf("field_b[1]: expected {2, keep_1}, got {%v, %v}", dest.FieldB[1].FieldA, dest.FieldB[1].FieldE) + } + + // Indices 2-4 should be nil (newly created, not assigned) + for i := 2; i <= 4; i++ { + if dest.FieldB[i] != nil { + t.Errorf("field_b[%d]: expected nil, got %v", i, dest.FieldB[i]) + } + } + + // Index 5 should have the new value + if dest.FieldB[5] == nil { + t.Fatalf("field_b[5]: expected non-nil") + } + if dest.FieldB[5].FieldA != 999 { + t.Errorf("field_b[5].field_a: expected 999, got %v", dest.FieldB[5].FieldA) + } + }) + + // Test case 9: Wildcard overwrites all existing elements + t.Run("wildcard_overwrites_all", func(t *testing.T) { + src := map[string]interface{}{ + "field_b": []interface{}{ + map[string]interface{}{"field_a": 10}, + map[string]interface{}{"field_a": 20}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + // Pre-populate dest with different size slice + dest := &sampleAssign{ + FieldB: []*sampleAssign{ + {FieldA: 100, FieldE: "old_0"}, + {FieldA: 200, FieldE: "old_1"}, + {FieldA: 300, FieldE: "old_2"}, + }, + } + + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Wildcard should completely replace with source length + if len(dest.FieldB) != 3 { + t.Fatalf("field_b: expected length 2 (from source), got %d", len(dest.FieldB)) + } + + // All elements should be new values from source + if dest.FieldB[0].FieldA != 10 { + t.Errorf("field_b[0].field_a: expected 10, got %v", dest.FieldB[0].FieldA) + } + if dest.FieldB[1].FieldA != 20 { + t.Errorf("field_b[1].field_a: expected 20, got %v", dest.FieldB[1].FieldA) + } + }) +} + +func TestAssignAny_NilValues(t *testing.T) { + err := assignAny(nil, nil, nil) + if err != nil { + t.Errorf("expected nil error for nil inputs, got %v", err) + } + + desc := &Descriptor{Kind: TypeKind_Struct, Type: "Test"} + dest := &sampleAssign{} + + err = assignAny(desc, nil, dest) + if err != nil { + t.Errorf("expected nil error for nil src, got %v", err) + } +} + +func TestAssignAny_DisallowNotFound(t *testing.T) { + src := map[string]interface{}{ + "field_a": 42, + "nonexistent": 100, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + // nonexistent is not in descriptor + }, + } + + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + desc.Normalize() + err := as.AssignAny(desc, src, dest) + if err == nil { + t.Fatalf("expected error for nonexistent field with DisallowNotFound") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T", err) + } + if notFoundErr.Field.Name != "nonexistent" { + t.Errorf("expected field name 'nonexistent', got %v", notFoundErr.Field.Name) + } +} + +// ===================== Circular Reference Tests ===================== +// These tests verify that AssignAny can handle circular reference type descriptions. +// The key principle is: recursively process data until data is nil (src == nil). + +// circularAssignNode represents a node that can reference itself (like a linked list) +type circularAssignNode struct { + Value int `protobuf:"varint,1,req,name=value" json:"value,omitempty"` + Next *circularAssignNode `protobuf:"bytes,2,opt,name=next" json:"next,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +// circularAssignTree represents a tree node that can reference itself +type circularAssignTree struct { + Value int `protobuf:"varint,1,req,name=value" json:"value,omitempty"` + Left *circularAssignTree `protobuf:"bytes,2,opt,name=left" json:"left,omitempty"` + Right *circularAssignTree `protobuf:"bytes,3,opt,name=right" json:"right,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +// makeCircularAssignDesc creates a descriptor that references itself (circular reference) +func makeCircularAssignDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularNode", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "next", ID: 2}, + }, + } + // Make it circular: next field's Desc points back to the same descriptor + desc.Children[1].Desc = desc + return desc +} + +// makeCircularAssignTreeDesc creates a tree descriptor that references itself +func makeCircularAssignTreeDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularTree", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "left", ID: 2}, + {Name: "right", ID: 3}, + }, + } + // Make it circular: left, right fields' Desc point back to the same descriptor + desc.Children[1].Desc = desc + desc.Children[2].Desc = desc + return desc +} + +func TestAssignAny_CircularDescriptor_LinkedList(t *testing.T) { + // Create source data: linked list 1 -> 2 -> 3 -> nil + src := map[string]interface{}{ + "value": 1, + "next": map[string]interface{}{ + "value": 2, + "next": map[string]interface{}{ + "value": 3, + // next is nil (absent) + }, + }, + } + + desc := makeCircularAssignDesc() + + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify the assigned structure + if dest.Value != 1 { + t.Errorf("value: expected 1, got %v", dest.Value) + } + + if dest.Next == nil { + t.Fatalf("next: expected non-nil") + } + if dest.Next.Value != 2 { + t.Errorf("next.value: expected 2, got %v", dest.Next.Value) + } + + if dest.Next.Next == nil { + t.Fatalf("next.next: expected non-nil") + } + if dest.Next.Next.Value != 3 { + t.Errorf("next.next.value: expected 3, got %v", dest.Next.Next.Value) + } + + // The last node's next should be nil + if dest.Next.Next.Next != nil { + t.Errorf("next.next.next: expected nil, got %v", dest.Next.Next.Next) + } +} + +func TestAssignAny_CircularDescriptor_SingleNode(t *testing.T) { + // Single node with nil next + src := map[string]interface{}{ + "value": 42, + // next is not present (nil) + } + + desc := makeCircularAssignDesc() + + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.Value != 42 { + t.Errorf("value: expected 42, got %v", dest.Value) + } + + if dest.Next != nil { + t.Errorf("next: expected nil, got %v", dest.Next) + } +} + +func TestAssignAny_CircularDescriptor_Tree(t *testing.T) { + // Create source data: binary tree + // 1 + // / \ + // 2 3 + // / + // 4 + src := map[string]interface{}{ + "value": 1, + "left": map[string]interface{}{ + "value": 2, + "left": map[string]interface{}{ + "value": 4, + }, + }, + "right": map[string]interface{}{ + "value": 3, + }, + } + + desc := makeCircularAssignTreeDesc() + + dest := &circularAssignTree{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify root + if dest.Value != 1 { + t.Errorf("value: expected 1, got %v", dest.Value) + } + + // Verify left subtree + if dest.Left == nil { + t.Fatalf("left: expected non-nil") + } + if dest.Left.Value != 2 { + t.Errorf("left.value: expected 2, got %v", dest.Left.Value) + } + + if dest.Left.Left == nil { + t.Fatalf("left.left: expected non-nil") + } + if dest.Left.Left.Value != 4 { + t.Errorf("left.left.value: expected 4, got %v", dest.Left.Left.Value) + } + + // Verify right subtree + if dest.Right == nil { + t.Fatalf("right: expected non-nil") + } + if dest.Right.Value != 3 { + t.Errorf("right.value: expected 3, got %v", dest.Right.Value) + } +} + +func TestAssignAny_CircularDescriptor_NilSrc(t *testing.T) { + desc := makeCircularAssignDesc() + + dest := &circularAssignNode{Value: 999} + + // Assign with nil src should not modify dest + err := assignAny(desc, nil, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Original value should be preserved + if dest.Value != 999 { + t.Errorf("value should be preserved, expected 999, got %v", dest.Value) + } +} + +func TestAssignAny_CircularDescriptor_DeepList(t *testing.T) { + // Create a deep linked list (depth=100) as source + depth := 100 + var src interface{} + for i := depth; i > 0; i-- { + node := map[string]interface{}{ + "value": i, + } + if src != nil { + node["next"] = src + } + src = node + } + + desc := makeCircularAssignDesc() + + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify the structure by traversing + current := dest + for i := 1; i <= depth; i++ { + if current.Value != i { + t.Errorf("at depth %d: expected value %d, got %v", i, i, current.Value) + } + if i < depth { + if current.Next == nil { + t.Fatalf("at depth %d: expected non-nil next", i) + } + current = current.Next + } else { + // Last node should have nil next + if current.Next != nil { + t.Errorf("last node should have nil next") + } + } + } +} + +// circularAssignMapNode represents a node with a map that can contain circular references +type circularAssignMapNode struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Children map[string]*circularAssignMapNode `protobuf:"bytes,2,opt,name=children" json:"children,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func makeCircularAssignMapDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularMapNode", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "children", ID: 2}, + }, + } + // Make children field circular: it's a map with values of the same type + desc.Children[1].Desc = &Descriptor{ + Kind: TypeKind_StrMap, + Type: "ChildrenMap", + Children: []Field{ + {Name: "*", Desc: desc}, // Wildcard with circular reference + }, + } + return desc +} + +func TestAssignAny_CircularDescriptor_MapOfNodes(t *testing.T) { + // Create source data: tree-like structure using maps + // root + // ├── child1 + // │ └── grandchild1 + // └── child2 + src := map[string]interface{}{ + "name": "root", + "children": map[string]interface{}{ + "child1": map[string]interface{}{ + "name": "child1", + "children": map[string]interface{}{ + "grandchild1": map[string]interface{}{ + "name": "grandchild1", + // children is nil + }, + }, + }, + "child2": map[string]interface{}{ + "name": "child2", + // children is nil + }, + }, + } + + desc := makeCircularAssignMapDesc() + + dest := &circularAssignMapNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify root + if dest.Name != "root" { + t.Errorf("name: expected 'root', got %v", dest.Name) + } + + if dest.Children == nil { + t.Fatalf("children: expected non-nil") + } + + child1 := dest.Children["child1"] + if child1 == nil { + t.Fatalf("child1: expected non-nil") + } + if child1.Name != "child1" { + t.Errorf("child1.name: expected 'child1', got %v", child1.Name) + } + + if child1.Children == nil { + t.Fatalf("child1.children: expected non-nil") + } + + grandchild1 := child1.Children["grandchild1"] + if grandchild1 == nil { + t.Fatalf("grandchild1: expected non-nil") + } + if grandchild1.Name != "grandchild1" { + t.Errorf("grandchild1.name: expected 'grandchild1', got %v", grandchild1.Name) + } + + child2 := dest.Children["child2"] + if child2 == nil { + t.Fatalf("child2: expected non-nil") + } + if child2.Name != "child2" { + t.Errorf("child2.name: expected 'child2', got %v", child2.Name) + } +} + +// TestAssignAny_CircularDescriptor_FetchThenAssign tests the full round-trip: +// fetch from a circular structure, then assign to another circular structure +func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { + // This uses types from fetch_test.go + // Create a linked list: 1 -> 2 -> 3 -> nil + type circularFetchNode struct { + Value int `thrift:"Value,1" json:"value,omitempty"` + Next *circularFetchNode `thrift:"Next,2" json:"next,omitempty"` + } + + srcList := &circularFetchNode{ + Value: 1, + Next: &circularFetchNode{ + Value: 2, + Next: &circularFetchNode{ + Value: 3, + Next: nil, + }, + }, + } + + // Create circular descriptor for fetch + fetchDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularNode", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "next", ID: 2}, + }, + } + fetchDesc.Children[1].Desc = fetchDesc + + // Fetch + fetched, err := fetchAny(fetchDesc, srcList) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + // Create circular descriptor for assign (with different IDs if needed) + assignDesc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularNode", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "next", ID: 2}, + }, + } + assignDesc.Children[1].Desc = assignDesc + + // Assign + dest := &circularAssignNode{} + err = assignAny(assignDesc, fetched, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + // Verify + if dest.Value != 1 { + t.Errorf("value: expected 1, got %v", dest.Value) + } + if dest.Next == nil || dest.Next.Value != 2 { + t.Errorf("next.value: expected 2") + } + if dest.Next.Next == nil || dest.Next.Next.Value != 3 { + t.Errorf("next.next.value: expected 3") + } + if dest.Next.Next.Next != nil { + t.Errorf("next.next.next: expected nil") + } +} + +// TestAssignAny_PathTracking tests that error messages include the correct DSL path +func TestAssignAny_PathTracking(t *testing.T) { + tests := []struct { + name string + src interface{} + desc *Descriptor + expectedErr string + }{ + { + name: "field not found in root", + src: map[string]interface{}{ + "unknown_field": 42, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + expectedErr: "not found unknown_field at SampleAssign: field 'unknown_field' not found in struct at path $.unknown_field", + }, + { + name: "field not found in nested struct", + src: map[string]interface{}{ + "field_d": map[string]interface{}{ + "unknown_nested": 123, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + expectedErr: "not found unknown_nested at SampleAssign: field 'unknown_nested' not found in struct at path $.field_d.unknown_nested", + }, + { + name: "field not found in deeply nested struct", + src: map[string]interface{}{ + "field_d": map[string]interface{}{ + "field_a": 1, + "field_d": map[string]interface{}{ + "missing_field": "test", + }, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: "not found missing_field at SampleAssign: field 'missing_field' not found in struct at path $.field_d.field_d.missing_field", + }, + { + name: "field not found in map", + src: map[string]interface{}{ + "field_c": map[string]interface{}{ + "key1": map[string]interface{}{ + "bad_field": 999, + }, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: "not found bad_field at SampleAssign: field 'bad_field' not found in struct at path $.field_c[key1].bad_field", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + tt.desc.Normalize() + err := as.AssignAny(tt.desc, tt.src, dest) + if err == nil { + t.Fatalf("expected error, got nil") + } + if err.Error() != tt.expectedErr { + t.Errorf("expected error:\n%s\ngot:\n%s", tt.expectedErr, err.Error()) + } + }) + } +} + +// TestAssignAny_PathTracking_TypeErrors tests path tracking for type mismatch errors +func TestAssignAny_PathTracking_TypeErrors(t *testing.T) { + tests := []struct { + name string + src interface{} + desc *Descriptor + errorSubstr string // Substring that should be in the error + }{ + { + name: "type error at root", + src: "not a map", // Should be map[string]interface{} + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + errorSubstr: "expected map[string]interface{} for struct at $", + }, + { + name: "type error in nested struct", + src: map[string]interface{}{ + "field_d": "not a map", // Should be map[string]interface{} + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + errorSubstr: "expected map[string]interface{} for struct at $.field_d", + }, + { + name: "type error in map value", + src: map[string]interface{}{ + "field_c": map[string]interface{}{ + "key1": []int{1, 2, 3}, // Should be a struct map + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + errorSubstr: "expected map[string]interface{} for struct at $.field_c[key1]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dest := &sampleAssign{} + err := assignAny(tt.desc, tt.src, dest) + if err == nil { + t.Fatalf("expected error, got nil") + } + if !contains(err.Error(), tt.errorSubstr) { + t.Errorf("expected error to contain:\n%s\ngot:\n%s", tt.errorSubstr, err.Error()) + } + }) + } +} + +// TestPathStack tests the pathStack implementation directly +func TestPathStack(t *testing.T) { + tests := []struct { + name string + ops func(*pathStack) + expected string + }{ + { + name: "empty stack", + ops: func(s *pathStack) { + }, + expected: "$", + }, + { + name: "single field", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "field_a", 1) + }, + expected: "$.field_a", + }, + { + name: "nested fields", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "field_d", 4) + s.push(TypeKind_Struct, "field_a", 1) + }, + expected: "$.field_d.field_a", + }, + { + name: "map key", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "field_c", 3) + s.push(TypeKind_StrMap, "my_key", 0) + }, + expected: "$.field_c[my_key]", + }, + { + name: "array index", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "field_b", 2) + s.push(TypeKind_List, "", 0) + }, + expected: "$.field_b[0]", + }, + { + name: "complex path", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "root_field", 1) + s.push(TypeKind_Struct, "map_field", 3) + s.push(TypeKind_StrMap, "key1", 0) + s.push(TypeKind_Struct, "nested", 4) + s.push(TypeKind_Struct, "array", 2) + s.push(TypeKind_List, "", 2) + }, + expected: "$.root_field.map_field[key1].nested.array[2]", + }, + { + name: "push and pop", + ops: func(s *pathStack) { + s.push(TypeKind_Struct, "field_a", 1) + s.push(TypeKind_Struct, "field_b", 2) + s.pop() + }, + expected: "$.field_a", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stack := getStackFrames() + defer putStackFrames(stack) + + tt.ops(stack) + result := stack.buildPath() + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + +// TestStackFramePool tests that the stack frame pool works correctly +func TestStackFramePool(t *testing.T) { + // Get a stack from the pool + frames1 := getStackFrames() + if len(*frames1) != 0 { + t.Errorf("expected empty frames, got length %d", len(*frames1)) + } + if cap(*frames1) < 16 { + t.Errorf("expected capacity >= 16, got %d", cap(*frames1)) + } + + // Use it and return it + *frames1 = append(*frames1, stackFrame{name: "test", id: 1}) + putStackFrames(frames1) + + // Get another one - should be reused + frames2 := getStackFrames() + if len(*frames2) != 0 { + t.Errorf("expected frames to be reset, got length %d", len(*frames2)) + } + + // Return it + putStackFrames(frames2) +} + +// TestAssignAny_PathTracking_Integration tests path tracking in a complex nested scenario +func TestAssignAny_PathTracking_Integration(t *testing.T) { + // Create a complex nested structure + src := map[string]interface{}{ + "field_a": 1, + "field_c": map[string]interface{}{ + "item1": map[string]interface{}{ + "field_a": 10, + "field_d": map[string]interface{}{ + "field_a": 20, + "bad_field": "should fail", // This will cause error + }, + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + desc.Normalize() + err := as.AssignAny(desc, src, dest) + if err == nil { + t.Fatalf("expected error, got nil") + } + + expectedPath := "$.field_c[item1].field_d.bad_field" + if !contains(err.Error(), expectedPath) { + t.Errorf("expected error to contain path %s, got: %s", expectedPath, err.Error()) + } +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && + (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || + anyIndex(s, substr))) +} + +func anyIndex(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +// Benchmark tests for path tracking overhead + +// BenchmarkAssignAny_NestedStruct tests performance with nested structures +func BenchmarkAssignAny_NestedStruct(b *testing.B) { + src := map[string]interface{}{ + "field_a": 1, + "field_d": map[string]interface{}{ + "field_a": 2, + "field_d": map[string]interface{}{ + "field_a": 3, + "field_e": "nested", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + _ = assignAny(desc, src, dest) + } +} + +func BenchmarkAssignAny_WithUnknownFields(b *testing.B) { + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + "unknown1": 100, + "unknown2": "secret", + "unknown3": 3.14, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssignSmall", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + {Name: "unknown1", ID: 10}, + {Name: "unknown2", ID: 11}, + {Name: "unknown3", ID: 12}, + }, + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + dest := &sampleAssignSmall{} + _ = assignAny(desc, src, dest) + } +} + +// BenchmarkAssignAny_WithMap tests performance with map structures +func BenchmarkAssignAny_WithMap(b *testing.B) { + src := map[string]interface{}{ + "field_a": 1, + "field_c": map[string]interface{}{ + "key1": map[string]interface{}{ + "field_a": 10, + "field_e": "value1", + }, + "key2": map[string]interface{}{ + "field_a": 20, + "field_e": "value2", + }, + "key3": map[string]interface{}{ + "field_a": 30, + "field_e": "value3", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + _ = assignAny(desc, src, dest) + } +} + +// BenchmarkAssignAny_ErrorPath tests performance when error occurs (path building) +func BenchmarkAssignAny_ErrorPath(b *testing.B) { + src := map[string]interface{}{ + "field_a": 1, + "field_d": map[string]interface{}{ + "field_a": 2, + "unknown": 999, // This will cause error + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + desc.Normalize() + _ = as.AssignAny(desc, src, dest) + } +} + +// BenchmarkPathStack_Operations tests the performance of stack operations +func BenchmarkPathStack_Operations(b *testing.B) { + b.Run("push_and_pop", func(b *testing.B) { + for i := 0; i < b.N; i++ { + stack := getStackFrames() + for j := 0; j < 10; j++ { + stack.push(TypeKind_Struct, "field", j) + } + for j := 0; j < 10; j++ { + stack.pop() + } + putStackFrames(stack) + } + }) + + b.Run("build_path_shallow", func(b *testing.B) { + stack := getStackFrames() + stack.push(TypeKind_Struct, "field_a", 1) + stack.push(TypeKind_Struct, "field_b", 2) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = stack.buildPath() + } + }) + + b.Run("build_path_deep", func(b *testing.B) { + stack := getStackFrames() + for i := 0; i < 10; i++ { + stack.push(TypeKind_Struct, "field", i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = stack.buildPath() + } + }) + + b.Run("build_path_mixed", func(b *testing.B) { + stack := getStackFrames() + stack.push(TypeKind_Struct, "root", 1) + stack.push(TypeKind_StrMap, "map_field", 3) + stack.push(TypeKind_StrMap, "key1", 0) + stack.push(TypeKind_Struct, "nested", 4) + stack.push(TypeKind_List, "", 5) + stack.push(TypeKind_Struct, "deep", 6) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = stack.buildPath() + } + }) +} + +// BenchmarkPathTracking_Overhead compares the overhead of path tracking +func BenchmarkPathTracking_Overhead(b *testing.B) { + src := map[string]interface{}{ + "field_a": 1, + "field_e": "test", + "field_c": map[string]interface{}{ + "k1": map[string]interface{}{ + "field_a": 10, + }, + "k2": map[string]interface{}{ + "field_a": 20, + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + } + + b.Run("success_case", func(b *testing.B) { + // Normal case - path tracking is active but never used for errors + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + _ = assignAny(desc, src, dest) + } + }) + + b.Run("error_case", func(b *testing.B) { + // Error case - path is built when error occurs + srcWithError := map[string]interface{}{ + "field_a": 1, + "unknown": 999, + } + b.ResetTimer() + desc.Normalize() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + _ = as.AssignAny(desc, srcWithError, dest) + } + }) +} + +// Test types for AssignValue API +type SimpleStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Score float64 `json:"score"` +} + +type NestedStruct struct { + XXX_NoUnkeyedLiteral map[string]interface{} `json:"-"` + ID int `json:"id"` + Simple SimpleStruct `json:"simple"` + PtrSimple *SimpleStruct `json:"ptr_simple"` +} + +type ComplexStruct struct { + Numbers []int `json:"numbers"` + Strings []string `json:"strings"` + PtrSlice []*SimpleStruct `json:"ptr_slice"` + StrMap map[string]int `json:"str_map"` + StructMap map[string]SimpleStruct `json:"struct_map"` + Nested NestedStruct `json:"nested"` +} + +func TestAssignValue_BasicTypes(t *testing.T) { + assigner := Assigner{} + + t.Run("int_to_int", func(t *testing.T) { + var dest int + err := assigner.AssignValue(42, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 42 { + t.Errorf("expected 42, got %d", dest) + } + }) + + t.Run("int_to_int64", func(t *testing.T) { + var dest int64 + err := assigner.AssignValue(42, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 42 { + t.Errorf("expected 42, got %d", dest) + } + }) + + t.Run("int32_to_int64", func(t *testing.T) { + var dest int64 + err := assigner.AssignValue(int32(100), &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 100 { + t.Errorf("expected 100, got %d", dest) + } + }) + + t.Run("uint_to_int", func(t *testing.T) { + var dest int + err := assigner.AssignValue(uint(50), &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 50 { + t.Errorf("expected 50, got %d", dest) + } + }) + + t.Run("float64_to_float32", func(t *testing.T) { + var dest float32 + err := assigner.AssignValue(3.14, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest < 3.13 || dest > 3.15 { + t.Errorf("expected ~3.14, got %f", dest) + } + }) + + t.Run("int_to_float64", func(t *testing.T) { + var dest float64 + err := assigner.AssignValue(42, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 42.0 { + t.Errorf("expected 42.0, got %f", dest) + } + }) + + t.Run("float64_to_int", func(t *testing.T) { + var dest int + err := assigner.AssignValue(42.9, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != 42 { + t.Errorf("expected 42, got %d", dest) + } + }) + + t.Run("string_to_string", func(t *testing.T) { + var dest string + err := assigner.AssignValue("hello", &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest != "hello" { + t.Errorf("expected 'hello', got %s", dest) + } + }) + + t.Run("bool_to_bool", func(t *testing.T) { + var dest bool + err := assigner.AssignValue(true, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !dest { + t.Errorf("expected true, got false") + } + }) + + t.Run("nil_source", func(t *testing.T) { + var dest int + err := assigner.AssignValue(nil, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("non_pointer_dest", func(t *testing.T) { + var dest int + err := assigner.AssignValue(42, dest) + if err == nil { + t.Fatal("expected error for non-pointer dest") + } + }) +} + +func TestAssignValue_StructToStruct(t *testing.T) { + assigner := Assigner{} + + t.Run("simple_struct_copy", func(t *testing.T) { + src := SimpleStruct{Name: "Alice", Age: 30, Score: 95.5} + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Alice" || dest.Age != 30 || dest.Score != 95.5 { + t.Errorf("struct not copied correctly: %+v", dest) + } + }) + + t.Run("partial_field_match", func(t *testing.T) { + type SourceStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Extra string `json:"extra"` + } + + src := SourceStruct{Name: "Bob", Age: 25, Extra: "ignored"} + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Bob" || dest.Age != 25 { + t.Errorf("fields not matched correctly: %+v", dest) + } + }) + + t.Run("pointer_source", func(t *testing.T) { + src := &SimpleStruct{Name: "Charlie", Age: 35, Score: 88.0} + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Charlie" || dest.Age != 35 { + t.Errorf("pointer struct not copied correctly: %+v", dest) + } + }) + + t.Run("nil_pointer_field", func(t *testing.T) { + type StructWithPtr struct { + Value *int `json:"value"` + } + + src := StructWithPtr{Value: nil} + dest := StructWithPtr{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Value != nil { + t.Errorf("expected nil pointer, got %v", dest.Value) + } + }) + + t.Run("nested_struct_with_XXX_NoUnkeyedLiteral", func(t *testing.T) { + src := NestedStruct{ + XXX_NoUnkeyedLiteral: map[string]interface{}{ + "unknown_field1": 100, + "unknown_field2": "secret_data", + "unknown_nested": map[string]interface{}{ + "inner_key": "inner_value", + }, + }, + ID: 42, + Simple: SimpleStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, + }, + PtrSimple: &SimpleStruct{ + Name: "Bob", + Age: 25, + Score: 88.0, + }, + } + + type NestedStruct2 struct { + XXX_NoUnkeyedLiteral map[string]interface{} `json:"-"` + ID int `json:"id"` + Simple SimpleStruct `json:"simple"` + PtrSimple *SimpleStruct `json:"ptr_simple"` + } + dest := NestedStruct2{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify regular fields + if dest.ID != 42 { + t.Errorf("ID: expected 42, got %d", dest.ID) + } + if dest.Simple.Name != "Alice" { + t.Errorf("Simple.Name: expected 'Alice', got '%s'", dest.Simple.Name) + } + if dest.PtrSimple == nil { + t.Fatalf("PtrSimple should not be nil") + } + if dest.PtrSimple.Name != "Bob" { + t.Errorf("PtrSimple.Name: expected 'Bob', got '%s'", dest.PtrSimple.Name) + } + + // Verify XXX_NoUnkeyedLiteral was copied + if dest.XXX_NoUnkeyedLiteral == nil { + t.Fatalf("XXX_NoUnkeyedLiteral should not be nil") + } + + if len(dest.XXX_NoUnkeyedLiteral) != 3 { + t.Errorf("XXX_NoUnkeyedLiteral: expected 3 entries, got %d", len(dest.XXX_NoUnkeyedLiteral)) + } + + // Verify unknown_field1 + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_field1"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_field1' key") + } else if intVal, ok := val.(int); !ok || intVal != 100 { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_field1']: expected 100, got %v (type %T)", val, val) + } + + // Verify unknown_field2 + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_field2"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_field2' key") + } else if strVal, ok := val.(string); !ok || strVal != "secret_data" { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_field2']: expected 'secret_data', got %v (type %T)", val, val) + } + + // Verify unknown_nested + if val, ok := dest.XXX_NoUnkeyedLiteral["unknown_nested"]; !ok { + t.Errorf("XXX_NoUnkeyedLiteral: expected 'unknown_nested' key") + } else if mapVal, ok := val.(map[string]interface{}); !ok { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_nested']: expected map[string]interface{}, got %T", val) + } else if innerVal, ok := mapVal["inner_key"]; !ok || innerVal != "inner_value" { + t.Errorf("XXX_NoUnkeyedLiteral['unknown_nested']['inner_key']: expected 'inner_value', got %v", innerVal) + } + + t.Logf("Successfully verified XXX_NoUnkeyedLiteral copy in NestedStruct") + t.Logf(" XXX_NoUnkeyedLiteral: %+v", dest.XXX_NoUnkeyedLiteral) + }) + + t.Run("nested_struct_with_nil_XXX_NoUnkeyedLiteral", func(t *testing.T) { + src := NestedStruct{ + XXX_NoUnkeyedLiteral: nil, + ID: 100, + Simple: SimpleStruct{ + Name: "Test", + Age: 40, + Score: 92.0, + }, + } + + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Regular fields should be copied + if dest.ID != 100 { + t.Errorf("ID: expected 100, got %d", dest.ID) + } + + // XXX_NoUnkeyedLiteral should remain nil or empty + if len(dest.XXX_NoUnkeyedLiteral) > 0 { + t.Errorf("XXX_NoUnkeyedLiteral: expected nil or empty, got %v", dest.XXX_NoUnkeyedLiteral) + } + }) + + t.Run("nested_struct_with_empty_XXX_NoUnkeyedLiteral", func(t *testing.T) { + src := NestedStruct{ + XXX_NoUnkeyedLiteral: map[string]interface{}{}, + ID: 200, + Simple: SimpleStruct{ + Name: "Empty", + Age: 50, + Score: 85.0, + }, + } + + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Regular fields should be copied + if dest.ID != 200 { + t.Errorf("ID: expected 200, got %d", dest.ID) + } + + // Empty map should not cause issues + if dest.Simple.Name != "Empty" { + t.Errorf("Simple.Name: expected 'Empty', got '%s'", dest.Simple.Name) + } + }) +} + +func TestAssignValue_Slices(t *testing.T) { + assigner := Assigner{} + + t.Run("int_slice", func(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + var dest []int + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(dest, src) { + t.Errorf("slices not equal: got %v, want %v", dest, src) + } + }) + + t.Run("string_slice", func(t *testing.T) { + src := []string{"a", "b", "c"} + var dest []string + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(dest, src) { + t.Errorf("slices not equal: got %v, want %v", dest, src) + } + }) + + t.Run("int_to_int64_slice", func(t *testing.T) { + src := []int{10, 20, 30} + var dest []int64 + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := []int64{10, 20, 30} + if !reflect.DeepEqual(dest, expected) { + t.Errorf("slices not equal: got %v, want %v", dest, expected) + } + }) + + t.Run("struct_slice", func(t *testing.T) { + src := []SimpleStruct{ + {Name: "Alice", Age: 30}, + {Name: "Bob", Age: 25}, + } + var dest []SimpleStruct + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 2 { + t.Fatalf("expected 2 elements, got %d", len(dest)) + } + if dest[0].Name != "Alice" || dest[1].Name != "Bob" { + t.Errorf("struct slice not copied correctly: %+v", dest) + } + }) + + t.Run("pointer_slice", func(t *testing.T) { + src := []*SimpleStruct{ + {Name: "Alice", Age: 30}, + nil, + {Name: "Bob", Age: 25}, + } + var dest []*SimpleStruct + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 3 { + t.Fatalf("expected 3 elements, got %d", len(dest)) + } + if dest[0] == nil || dest[0].Name != "Alice" { + t.Errorf("first element incorrect: %+v", dest[0]) + } + if dest[1] != nil { + t.Errorf("expected nil at index 1, got %+v", dest[1]) + } + if dest[2] == nil || dest[2].Name != "Bob" { + t.Errorf("third element incorrect: %+v", dest[2]) + } + }) + + t.Run("nil_slice", func(t *testing.T) { + var src []int = nil + var dest []int + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest != nil { + t.Errorf("expected nil slice, got %v", dest) + } + }) + + t.Run("empty_slice", func(t *testing.T) { + src := []int{} + var dest []int + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 0 { + t.Errorf("expected empty slice, got %v", dest) + } + }) +} + +func TestAssignValue_Maps(t *testing.T) { + assigner := Assigner{} + + t.Run("string_int_map", func(t *testing.T) { + src := map[string]int{"a": 1, "b": 2, "c": 3} + dest := make(map[string]int) + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(dest, src) { + t.Errorf("maps not equal: got %v, want %v", dest, src) + } + }) + + t.Run("int_conversion_map", func(t *testing.T) { + src := map[string]int32{"x": 10, "y": 20} + dest := make(map[string]int64) + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest["x"] != 10 || dest["y"] != 20 { + t.Errorf("map values not converted correctly: %v", dest) + } + }) + + t.Run("struct_value_map", func(t *testing.T) { + src := map[string]SimpleStruct{ + "alice": {Name: "Alice", Age: 30}, + "bob": {Name: "Bob", Age: 25}, + } + dest := make(map[string]SimpleStruct) + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 2 { + t.Fatalf("expected 2 entries, got %d", len(dest)) + } + if dest["alice"].Name != "Alice" || dest["bob"].Name != "Bob" { + t.Errorf("struct map not copied correctly: %+v", dest) + } + }) + + t.Run("pointer_value_map", func(t *testing.T) { + src := map[string]*SimpleStruct{ + "alice": {Name: "Alice", Age: 30}, + "nil": nil, + } + dest := make(map[string]*SimpleStruct) + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest["alice"] == nil || dest["alice"].Name != "Alice" { + t.Errorf("map pointer incorrect: %+v", dest["alice"]) + } + if dest["nil"] != nil { + t.Errorf("expected nil pointer in map, got %+v", dest["nil"]) + } + }) + + t.Run("nil_map", func(t *testing.T) { + var src map[string]int = nil + var dest map[string]int + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestAssignValue_MapToStruct(t *testing.T) { + assigner := Assigner{} + + t.Run("basic_map_to_struct", func(t *testing.T) { + src := map[string]interface{}{ + "name": "Alice", + "age": 30, + "score": 95.5, + } + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Alice" || dest.Age != 30 || dest.Score != 95.5 { + t.Errorf("map to struct failed: %+v", dest) + } + }) + + t.Run("partial_fields", func(t *testing.T) { + src := map[string]interface{}{ + "name": "Bob", + } + dest := SimpleStruct{Age: 100, Score: 50.0} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Bob" { + t.Errorf("name not set: %s", dest.Name) + } + // Age and Score should remain unchanged + if dest.Age != 100 || dest.Score != 50.0 { + t.Errorf("other fields were modified: %+v", dest) + } + }) + + t.Run("type_conversion_in_map", func(t *testing.T) { + src := map[string]interface{}{ + "name": "Charlie", + "age": int32(25), + "score": float32(88.5), + } + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Charlie" || dest.Age != 25 { + t.Errorf("type conversion failed: %+v", dest) + } + }) + + t.Run("nil_value_in_map", func(t *testing.T) { + src := map[string]interface{}{ + "name": "David", + "age": nil, + } + dest := SimpleStruct{Age: 50} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "David" { + t.Errorf("name not set: %s", dest.Name) + } + // Age should remain as is since nil won't overwrite + if dest.Age != 50 { + t.Errorf("age was modified: %d", dest.Age) + } + }) + + t.Run("unknown_fields_ignored", func(t *testing.T) { + src := map[string]interface{}{ + "name": "Eve", + "unknown": "should be ignored", + } + dest := SimpleStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Name != "Eve" { + t.Errorf("name not set: %s", dest.Name) + } + }) +} + +func TestAssignValue_NestedStructs(t *testing.T) { + assigner := Assigner{} + + t.Run("nested_struct", func(t *testing.T) { + src := NestedStruct{ + ID: 1, + Simple: SimpleStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, + }, + } + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.ID != 1 { + t.Errorf("ID not set: %d", dest.ID) + } + if dest.Simple.Name != "Alice" || dest.Simple.Age != 30 { + t.Errorf("nested struct not copied: %+v", dest.Simple) + } + }) + + t.Run("nested_pointer_struct", func(t *testing.T) { + src := NestedStruct{ + ID: 2, + PtrSimple: &SimpleStruct{ + Name: "Bob", + Age: 25, + }, + } + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.PtrSimple == nil { + t.Fatal("PtrSimple is nil") + } + if dest.PtrSimple.Name != "Bob" || dest.PtrSimple.Age != 25 { + t.Errorf("nested pointer struct not copied: %+v", dest.PtrSimple) + } + }) + + t.Run("map_to_nested_struct", func(t *testing.T) { + src := map[string]interface{}{ + "id": 3, + "simple": map[string]interface{}{ + "name": "Charlie", + "age": 35, + "score": 88.0, + }, + } + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.ID != 3 { + t.Errorf("ID not set: %d", dest.ID) + } + if dest.Simple.Name != "Charlie" || dest.Simple.Age != 35 { + t.Errorf("nested struct from map failed: %+v", dest.Simple) + } + }) + + t.Run("map_to_nested_pointer_struct", func(t *testing.T) { + src := map[string]interface{}{ + "id": 4, + "ptr_simple": map[string]interface{}{ + "name": "David", + "age": 40, + }, + } + dest := NestedStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.PtrSimple == nil { + t.Fatal("PtrSimple is nil") + } + if dest.PtrSimple.Name != "David" || dest.PtrSimple.Age != 40 { + t.Errorf("nested pointer struct from map failed: %+v", dest.PtrSimple) + } + }) +} + +func TestAssignValue_ComplexNested(t *testing.T) { + assigner := Assigner{} + + t.Run("struct_with_slices_and_maps", func(t *testing.T) { + src := ComplexStruct{ + Numbers: []int{1, 2, 3}, + Strings: []string{"a", "b", "c"}, + StrMap: map[string]int{"x": 10, "y": 20}, + } + dest := ComplexStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(dest.Numbers, src.Numbers) { + t.Errorf("numbers not copied: %v", dest.Numbers) + } + if !reflect.DeepEqual(dest.Strings, src.Strings) { + t.Errorf("strings not copied: %v", dest.Strings) + } + if !reflect.DeepEqual(dest.StrMap, src.StrMap) { + t.Errorf("map not copied: %v", dest.StrMap) + } + }) + + t.Run("pointer_slice_in_struct", func(t *testing.T) { + src := ComplexStruct{ + PtrSlice: []*SimpleStruct{ + {Name: "Alice", Age: 30}, + {Name: "Bob", Age: 25}, + }, + } + dest := ComplexStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest.PtrSlice) != 2 { + t.Fatalf("expected 2 elements, got %d", len(dest.PtrSlice)) + } + if dest.PtrSlice[0].Name != "Alice" || dest.PtrSlice[1].Name != "Bob" { + t.Errorf("pointer slice not copied correctly: %+v", dest.PtrSlice) + } + }) + + t.Run("struct_map_in_struct", func(t *testing.T) { + src := ComplexStruct{ + StructMap: map[string]SimpleStruct{ + "alice": {Name: "Alice", Age: 30}, + "bob": {Name: "Bob", Age: 25}, + }, + } + dest := ComplexStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest.StructMap) != 2 { + t.Fatalf("expected 2 entries, got %d", len(dest.StructMap)) + } + if dest.StructMap["alice"].Name != "Alice" { + t.Errorf("struct map not copied correctly: %+v", dest.StructMap) + } + }) + + t.Run("deeply_nested", func(t *testing.T) { + src := ComplexStruct{ + Nested: NestedStruct{ + ID: 100, + Simple: SimpleStruct{ + Name: "DeepAlice", + Age: 30, + Score: 95.5, + }, + PtrSimple: &SimpleStruct{ + Name: "DeepBob", + Age: 25, + Score: 80.0, + }, + }, + } + dest := ComplexStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest.Nested.ID != 100 { + t.Errorf("nested ID not copied: %d", dest.Nested.ID) + } + if dest.Nested.Simple.Name != "DeepAlice" { + t.Errorf("deeply nested struct not copied: %+v", dest.Nested.Simple) + } + if dest.Nested.PtrSimple == nil || dest.Nested.PtrSimple.Name != "DeepBob" { + t.Errorf("deeply nested pointer struct not copied: %+v", dest.Nested.PtrSimple) + } + }) + + t.Run("map_to_complex_struct", func(t *testing.T) { + src := map[string]interface{}{ + "numbers": []interface{}{1, 2, 3}, + "strings": []interface{}{"x", "y", "z"}, + "str_map": map[string]interface{}{"key1": 100, "key2": 200}, + "ptr_slice": []interface{}{ + map[string]interface{}{"name": "MapAlice", "age": 30}, + map[string]interface{}{"name": "MapBob", "age": 25}, + }, + "nested": map[string]interface{}{ + "id": 500, + "simple": map[string]interface{}{ + "name": "NestedFromMap", + "age": 45, + }, + }, + } + dest := ComplexStruct{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest.Numbers) != 3 || dest.Numbers[0] != 1 { + t.Errorf("numbers not set from map: %v", dest.Numbers) + } + if len(dest.Strings) != 3 || dest.Strings[0] != "x" { + t.Errorf("strings not set from map: %v", dest.Strings) + } + if dest.StrMap["key1"] != 100 { + t.Errorf("str_map not set from map: %v", dest.StrMap) + } + if len(dest.PtrSlice) != 2 || dest.PtrSlice[0].Name != "MapAlice" { + t.Errorf("ptr_slice not set from map: %+v", dest.PtrSlice) + } + if dest.Nested.ID != 500 || dest.Nested.Simple.Name != "NestedFromMap" { + t.Errorf("nested not set from map: %+v", dest.Nested) + } + }) +} + +func TestAssignValue_EdgeCases(t *testing.T) { + assigner := Assigner{} + + t.Run("both_nil", func(t *testing.T) { + err := assigner.AssignValue(nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("interface_wrapping", func(t *testing.T) { + var src interface{} = 42 + var dest int + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest != 42 { + t.Errorf("expected 42, got %d", dest) + } + }) + + t.Run("interface_to_interface", func(t *testing.T) { + var src interface{} = "hello" + var dest interface{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if str, ok := dest.(string); !ok || str != "hello" { + t.Errorf("expected 'hello', got %v", dest) + } + }) + + t.Run("slice_with_interface_elements", func(t *testing.T) { + src := []interface{}{1, "two", 3.0} + var dest []interface{} + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 3 { + t.Fatalf("expected 3 elements, got %d", len(dest)) + } + }) + + t.Run("map_with_interface_values", func(t *testing.T) { + src := map[string]interface{}{ + "int": 42, + "string": "hello", + "float": 3.14, + } + dest := make(map[string]interface{}) + + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(dest) != 3 { + t.Fatalf("expected 3 entries, got %d", len(dest)) + } + if dest["int"] != 42 { + t.Errorf("int value incorrect: %v", dest["int"]) + } + }) + + t.Run("zero_values", func(t *testing.T) { + var srcInt int = 0 + var destInt int = 100 + + err := assigner.AssignValue(srcInt, &destInt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if destInt != 0 { + t.Errorf("expected 0, got %d", destInt) + } + }) +} + +// BenchmarkComplexStruct_Comparison compares AssignValue vs JSON marshal/unmarshal +func BenchmarkAssignValue(b *testing.B) { + // Create a complex source struct with nested data (shared by both sub-benchmarks) + src := ComplexStruct{ + Numbers: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + Strings: []string{"a", "b", "c", "d", "e"}, + PtrSlice: []*SimpleStruct{ + {Name: "Alice", Age: 30, Score: 95.5}, + {Name: "Bob", Age: 25, Score: 88.0}, + {Name: "Charlie", Age: 35, Score: 92.0}, + }, + StrMap: map[string]int{ + "key1": 100, + "key2": 200, + "key3": 300, + }, + StructMap: map[string]SimpleStruct{ + "user1": {Name: "Dave", Age: 40, Score: 87.5}, + "user2": {Name: "Eve", Age: 28, Score: 91.0}, + }, + Nested: NestedStruct{ + XXX_NoUnkeyedLiteral: map[string]interface{}{ + "extra1": 999, + "extra2": "hidden", + }, + ID: 123, + Simple: SimpleStruct{ + Name: "NestedUser", + Age: 45, + Score: 89.5, + }, + PtrSimple: &SimpleStruct{ + Name: "NestedPtr", + Age: 50, + Score: 86.0, + }, + }, + } + + b.Run("AssignValue", func(b *testing.B) { + assigner := Assigner{} + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + dest := ComplexStruct{} + _ = assigner.AssignValue(src, &dest) + } + }) + + b.Run("JSON", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + // Marshal to JSON + jsonData, err := sonic.Marshal(src) + if err != nil { + b.Fatalf("marshal failed: %v", err) + } + + // Unmarshal back to struct + dest := ComplexStruct{} + err = sonic.Unmarshal(jsonData, &dest) + if err != nil { + b.Fatalf("unmarshal failed: %v", err) + } + } + }) +} + +type sampleAssignArray struct { + FieldArray [3]int `protobuf:"varint,1,opt,name=field_array" json:"field_array"` +} + +func TestAssignAny_Array(t *testing.T) { + t.Run("Array_SpecificIndices_Success", func(t *testing.T) { + src := map[string]interface{}{ + "field_array": []interface{}{10, 30}, + } + // Assign 10 to index 0, 30 to index 2 + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleAssignArray", + Children: []Field{ + { + Name: "field_array", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_List, + Children: []Field{ + {ID: 0}, // Maps src[0] (10) to dest[0] + {ID: 2}, // Maps src[1] (30) to dest[2] + }, + }, + }, + }, + } + + dest := &sampleAssignArray{} + assigner := &Assigner{} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dest.FieldArray[0] != 10 { + t.Errorf("expected dest[0] to be 10, got %d", dest.FieldArray[0]) + } + if dest.FieldArray[1] != 0 { + t.Errorf("expected dest[1] to be 0, got %d", dest.FieldArray[1]) + } + if dest.FieldArray[2] != 30 { + t.Errorf("expected dest[2] to be 30, got %d", dest.FieldArray[2]) + } + }) + + t.Run("Array_OutOfBounds", func(t *testing.T) { + src := map[string]interface{}{ + "field_array": []interface{}{10}, + } + desc := &Descriptor{ + Kind: TypeKind_Struct, + Children: []Field{ + { + Name: "field_array", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_List, + Children: []Field{ + {ID: 3}, // Index 3 is out of bounds for [3]int + }, + }, + }, + }, + } + dest := &sampleAssignArray{} + assigner := &Assigner{} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "out of bounds") { + t.Errorf("expected out of bounds error, got: %v", err) + } + }) + + t.Run("Array_MissingSource_DisallowNotDefined", func(t *testing.T) { + src := map[string]interface{}{ + "field_array": []interface{}{10}, + } + desc := &Descriptor{ + Kind: TypeKind_Struct, + Children: []Field{ + { + Name: "field_array", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_List, + Children: []Field{ + {ID: 0}, + {ID: 2}, // Missing in source (src has len 1) + }, + }, + }, + }, + } + dest := &sampleAssignArray{} + assigner := &Assigner{AssignOptions: AssignOptions{DisallowNotDefined: true}} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "not found in source") { + t.Errorf("expected not found error, got: %v", err) + } + }) + + t.Run("Array_NegativeIndex", func(t *testing.T) { + src := map[string]interface{}{ + "field_array": []interface{}{10}, + } + desc := &Descriptor{ + Kind: TypeKind_Struct, + Children: []Field{ + { + Name: "field_array", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_List, + Children: []Field{ + {ID: 0}, + {ID: -1}, + }, + }, + }, + }, + } + dest := &sampleAssignArray{} + assigner := &Assigner{AssignOptions: AssignOptions{DisallowNotDefined: true}} + desc.Normalize() + err := assigner.AssignAny(desc, src, dest) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "out of bounds") { + t.Errorf("expected out of bounds error, got: %v", err) + } + }) +} diff --git a/trim/desc.go b/trim/desc.go new file mode 100644 index 00000000..a0a72042 --- /dev/null +++ b/trim/desc.go @@ -0,0 +1,378 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "sync" +) + +// TypeKind is the kind of type. +type TypeKind int + +const ( + // TypeKind_Leaf indicates Descriptor is a leaf node, its underlying type can be anything (event go struct/map/list) + TypeKind_Leaf TypeKind = iota + // TypeKind_Struct indicates Descriptor.Field is struct field + TypeKind_Struct + // TypeKind_StrMap indicates Descriptor.Field is map key + TypeKind_StrMap + // TypeKind_List indicates Descriptor.Field is list index + TypeKind_List +) + +// Descriptor describes the entire a DSL-pruning scheme for a type. +// base on this, we can fetch the type's object data on demands +// +// WARNING: user must call Normalize() before using this +type Descriptor struct { + // the kind of corresponding type + // Based on this, we can decide how to manipulate the data (e.g. mapKey or strucField) + Kind TypeKind + + // Type of the type + Type string + + // children for TypeKind_Struct|TypeKind_StrMap|TypeKind_List + // - For TypeKind_StrMap, either each Field is a key-value pair or one field with Name "*" + // - For TypeKind_Struct, each Field is a field with both Name and ID + // - For TypeKind_List, either each Field.ID is the element index or one field with Name "*" (means all elements) + Children []Field + + // for speed-up search + // WARNING: user must call Normalize() before using this + normalized bool + ids map[int]Field + names map[string]Field +} + +// Field represents a mapping selection +type Field struct { + // Name of the field path for TypeKind_Struct + // Or the selection key for TypeKind_StrMap + Name string + + // FieldID in IDL + ID int + + // the child of the field + Desc *Descriptor +} + +// Normalize cache all the fields in the descriptor for speeding up search. +// It handles circular references by using an atomic flag to detect re-entry. +func (d *Descriptor) Normalize() { + // Fast-path: already normalized (or in-progress done) + if d.normalized { + return + } + d.normalized = true + + d.ids = make(map[int]Field, len(d.Children)) + d.names = make(map[string]Field, len(d.Children)) + for _, f := range d.Children { + d.ids[f.ID] = f + d.names[f.Name] = f + if f.Desc != nil { + f.Desc.Normalize() + } + } +} + +// String returns the string representation of the descriptor in JSON-like format. +// It handles circular references by tracking visited descriptors. +// Format: {...} for struct/map, "-" for scalar types. +func (d *Descriptor) String() string { + sb := strings.Builder{} + visited := make(map[*Descriptor]bool) + + var printer func(desc *Descriptor, indent string) + printer = func(desc *Descriptor, indent string) { + // Handle circular references + if visited[desc] { + sb.WriteString("<" + desc.Type + ">") + return + } + visited[desc] = true + + // Get type prefix based on Kind + var typePrefix string + switch desc.Kind { + case TypeKind_Leaf: + sb.WriteString("-") + return + case TypeKind_StrMap: + typePrefix = "" + default: // TypeKind_Struct + typePrefix = "<" + desc.Type + ">" + } + + sb.WriteString(typePrefix) + + if len(desc.Children) == 0 { + sb.WriteString("{}") + return + } + + sb.WriteString("{\n") + nextIndent := indent + "\t" + + for i, f := range desc.Children { + sb.WriteString(nextIndent) + + // For MAP with "*" key, just use "*" + if desc.Kind == TypeKind_StrMap && f.Name == "*" { + sb.WriteString("\"*\": ") + } else { + sb.WriteString("\"" + f.Name + "\": ") + } + + if f.Desc == nil { + sb.WriteString("-") + } else { + printer(f.Desc, nextIndent) + } + + if i < len(desc.Children)-1 { + sb.WriteString(",") + } + sb.WriteString("\n") + } + + sb.WriteString(indent + "}") + } + + printer(d, "") + return sb.String() +} + +// descriptorJSON is the JSON representation of Descriptor +type descriptorJSON struct { + Kind TypeKind `json:"kind"` + Name string `json:"name"` + Children []fieldJSON `json:"children,omitempty"` +} + +// fieldJSON is the JSON representation of Field +type fieldJSON struct { + Name string `json:"name"` + ID int `json:"id"` + Desc *descriptorJSON `json:"desc,omitempty"` + Ref string `json:"$ref,omitempty"` // reference to another descriptor by path +} + +// MarshalJSON implements json.Marshaler interface for Descriptor +// It handles circular references by using $ref to reference already visited descriptors +func (d *Descriptor) MarshalJSON() ([]byte, error) { + visited := make(map[*Descriptor]string) // maps pointer to path + result := d.marshalWithPath("$", visited) + return json.Marshal(result) +} + +// marshalWithPath recursively marshals the descriptor, tracking visited nodes +func (d *Descriptor) marshalWithPath(path string, visited map[*Descriptor]string) *descriptorJSON { + if d == nil { + return nil + } + + // Check if we've already visited this descriptor (circular reference) + if existingPath, ok := visited[d]; ok { + // Return a reference placeholder - this will be handled specially + return &descriptorJSON{ + Kind: d.Kind, + Name: fmt.Sprintf("$ref:%s", existingPath), + } + } + + // Mark as visited + visited[d] = path + + result := &descriptorJSON{ + Kind: d.Kind, + Name: d.Type, + Children: make([]fieldJSON, 0, len(d.Children)), + } + + for i, f := range d.Children { + childPath := fmt.Sprintf("%s.children[%d].desc", path, i) + fj := fieldJSON{ + Name: f.Name, + ID: f.ID, + } + + if f.Desc != nil { + // Check if child descriptor was already visited + if existingPath, ok := visited[f.Desc]; ok { + fj.Ref = existingPath + } else { + fj.Desc = f.Desc.marshalWithPath(childPath, visited) + } + } + + result.Children = append(result.Children, fj) + } + + return result +} + +// UnmarshalJSON implements json.Unmarshaler interface for Descriptor +// It handles circular references by resolving $ref references after initial parsing +func (d *Descriptor) UnmarshalJSON(data []byte) error { + var raw descriptorJSON + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // First pass: build all descriptors and collect references + refs := make(map[string]*Descriptor) // path -> descriptor + d.unmarshalFromJSON(&raw, "$", refs) + + // Second pass: resolve references + d.resolveRefs("$", refs) + + return nil +} + +// unmarshalFromJSON populates the descriptor from JSON representation +func (d *Descriptor) unmarshalFromJSON(raw *descriptorJSON, path string, refs map[string]*Descriptor) { + d.Kind = raw.Kind + d.Type = raw.Name + d.Children = make([]Field, 0, len(raw.Children)) + d.ids = nil + d.names = nil + + // Register this descriptor + refs[path] = d + + for i, fj := range raw.Children { + childPath := fmt.Sprintf("%s.children[%d].desc", path, i) + f := Field{ + Name: fj.Name, + ID: fj.ID, + } + + if fj.Ref != "" { + // This is a reference, will be resolved later + // Create a placeholder descriptor with special name + f.Desc = &Descriptor{Type: "$ref:" + fj.Ref} + } else if fj.Desc != nil { + f.Desc = &Descriptor{} + f.Desc.unmarshalFromJSON(fj.Desc, childPath, refs) + } + + d.Children = append(d.Children, f) + } +} + +// resolveRefs resolves all $ref references in the descriptor tree +func (d *Descriptor) resolveRefs(path string, refs map[string]*Descriptor) { + for i := range d.Children { + if d.Children[i].Desc != nil { + childPath := fmt.Sprintf("%s.children[%d].desc", path, i) + + // Check if this is a reference + if strings.HasPrefix(d.Children[i].Desc.Type, "$ref:") { + refPath := strings.TrimPrefix(d.Children[i].Desc.Type, "$ref:") + if target, ok := refs[refPath]; ok { + d.Children[i].Desc = target + } + } else { + // Recursively resolve references + d.Children[i].Desc.resolveRefs(childPath, refs) + } + } + } +} + +// stackFrame represents a single frame in the path tracking stack +type stackFrame struct { + kind TypeKind + name string + id int +} + +// stackFramePool is a pool for stackFrame slices to reduce allocations +var stackFramePool = sync.Pool{ + New: func() interface{} { + ret := make([]stackFrame, 0, 16) // pre-allocate for common depth + return (*pathStack)(&ret) + }, +} + +// getStackFrames gets a stackFrame slice from the pool +func getStackFrames() *pathStack { + return stackFramePool.Get().(*pathStack) +} + +// putStackFrames returns a stackFrame slice to the pool +func putStackFrames(s *pathStack) { + if s == nil { + return + } + *s = (*s)[:0] // reset slice + stackFramePool.Put(s) //nolint:staticcheck // SA6002: storing slice in Pool is intentional +} + +// pathStack tracks the path from root to current node +type pathStack []stackFrame + +// push adds a new frame to the stack +func (s *pathStack) push(kind TypeKind, name string, id int) { + *s = append(*s, stackFrame{ + kind: kind, + name: name, + id: id, + }) +} + +// pop removes the last frame from the stack +func (s *pathStack) pop() { + if len(*s) > 0 { + *s = (*s)[:len(*s)-1] + } +} + +// buildPath constructs a human-readable DSL path from the stack +// This is only called when an error occurs +func (s *pathStack) buildPath() string { + if len(*s) == 0 { + return "$" + } + + var sb strings.Builder + sb.WriteString("$") + + for _, frame := range *s { + if frame.kind == TypeKind_StrMap { + sb.WriteString("[") + sb.WriteString(frame.name) + sb.WriteString("]") + } else if frame.kind == TypeKind_List { + sb.WriteString("[") + sb.WriteString(strconv.Itoa(frame.id)) + sb.WriteString("]") + } else { + sb.WriteString(".") + sb.WriteString(frame.name) + } + } + + return sb.String() +} diff --git a/trim/desc_test.go b/trim/desc_test.go new file mode 100644 index 00000000..24437786 --- /dev/null +++ b/trim/desc_test.go @@ -0,0 +1,225 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDescriptorMarshalJSON(t *testing.T) { + // Create a simple descriptor without circular reference + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Root", + Children: []Field{ + { + Name: "field1", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Leaf, + Type: "Leaf1", + }, + }, + { + Name: "field2", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "Map1", + Children: []Field{ + { + Name: "key1", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Leaf, + Type: "Leaf2", + }, + }, + }, + }, + }, + }, + } + + // Marshal to JSON + data, err := json.Marshal(desc) + require.NoError(t, err) + t.Logf("JSON: %s", string(data)) + + // Unmarshal back + var desc2 Descriptor + err = json.Unmarshal(data, &desc2) + require.NoError(t, err) + + // Verify structure + require.Equal(t, desc.Kind, desc2.Kind) + require.Equal(t, desc.Type, desc2.Type) + require.Len(t, desc2.Children, 2) + require.Equal(t, desc.Children[0].Name, desc2.Children[0].Name) + require.Equal(t, desc.Children[0].ID, desc2.Children[0].ID) + require.NotNil(t, desc2.Children[0].Desc) + require.Equal(t, desc.Children[0].Desc.Type, desc2.Children[0].Desc.Type) +} + +func TestDescriptorMarshalJSONWithCircularReference(t *testing.T) { + // Create a descriptor with circular reference + root := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Root", + } + + child := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Child", + } + + // Root -> Child -> Root (circular reference) + root.Children = []Field{ + { + Name: "child", + ID: 1, + Desc: child, + }, + } + + child.Children = []Field{ + { + Name: "parent", + ID: 1, + Desc: root, // circular reference back to root + }, + } + + // Marshal to JSON - should not panic or infinite loop + data, err := json.Marshal(root) + require.NoError(t, err) + t.Logf("JSON with circular ref: %s", string(data)) + + // Unmarshal back + var root2 Descriptor + err = json.Unmarshal(data, &root2) + require.NoError(t, err) + + // Verify structure + require.Equal(t, root.Kind, root2.Kind) + require.Equal(t, root.Type, root2.Type) + require.Len(t, root2.Children, 1) + require.NotNil(t, root2.Children[0].Desc) + require.Equal(t, "Child", root2.Children[0].Desc.Type) + + // Verify circular reference is resolved + require.NotNil(t, root2.Children[0].Desc.Children) + require.Len(t, root2.Children[0].Desc.Children, 1) + require.NotNil(t, root2.Children[0].Desc.Children[0].Desc) + // The circular reference should point back to root2 + require.Equal(t, &root2, root2.Children[0].Desc.Children[0].Desc) +} + +func TestDescriptorMarshalJSONSelfReference(t *testing.T) { + // Create a descriptor that references itself + self := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Self", + } + + self.Children = []Field{ + { + Name: "self", + ID: 1, + Desc: self, // self reference + }, + } + + // Marshal to JSON + data, err := json.Marshal(self) + require.NoError(t, err) + t.Logf("JSON with self ref: %s", string(data)) + + // Unmarshal back + var self2 Descriptor + err = json.Unmarshal(data, &self2) + require.NoError(t, err) + + // Verify self reference is resolved + require.Equal(t, &self2, self2.Children[0].Desc) +} + +func TestDescriptorMarshalJSONNil(t *testing.T) { + // Descriptor with nil child + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Root", + Children: []Field{ + { + Name: "field1", + ID: 1, + Desc: nil, // nil descriptor + }, + }, + } + + data, err := json.Marshal(desc) + require.NoError(t, err) + t.Logf("JSON with nil: %s", string(data)) + + var desc2 Descriptor + err = json.Unmarshal(data, &desc2) + require.NoError(t, err) + + require.Nil(t, desc2.Children[0].Desc) +} + +func TestDescriptorMarshalJSONMultipleReferences(t *testing.T) { + // Create a shared descriptor referenced by multiple fields + shared := &Descriptor{ + Kind: TypeKind_Leaf, + Type: "Shared", + } + + root := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Root", + Children: []Field{ + { + Name: "ref1", + ID: 1, + Desc: shared, + }, + { + Name: "ref2", + ID: 2, + Desc: shared, // same descriptor referenced again + }, + }, + } + + data, err := json.Marshal(root) + require.NoError(t, err) + t.Logf("JSON with multiple refs: %s", string(data)) + + var root2 Descriptor + err = json.Unmarshal(data, &root2) + require.NoError(t, err) + + // Both refs should point to the same descriptor after unmarshaling + require.NotNil(t, root2.Children[0].Desc) + require.NotNil(t, root2.Children[1].Desc) + require.Equal(t, root2.Children[0].Desc, root2.Children[1].Desc) +} diff --git a/trim/fetch.go b/trim/fetch.go new file mode 100644 index 00000000..df912885 --- /dev/null +++ b/trim/fetch.go @@ -0,0 +1,552 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/cloudwego/dynamicgo/thrift" +) + +type Fetcher struct { + FetchOptions +} + +// FetchOptions contains options for FetchAny +type FetchOptions struct { + // DisallowNotFound if true, returns ErrNotFound when a field/index/key is not found + DisallowNotFound bool +} + +// FetchAny fetches the value of the field described by desc from any based on go reflect. +// +// Warning: desc must be normalized before calling this method. +func (f Fetcher) FetchAny(desc *Descriptor, any interface{}) (interface{}, error) { + if any == nil || desc == nil { + return nil, nil + } + + // desc.Normalize() + + // Initialize path stack from pool + stack := getStackFrames() + defer putStackFrames(stack) + + v := reflect.ValueOf(any) + return fetchValue(desc, v, &f.FetchOptions, stack) +} + +// ErrNotFound is returned when a field/index/key is not found and disallowNotFound is enabled +type ErrNotFound struct { + Parent *Descriptor + Field Field // the field that is not found + Msg string // additional message +} + +func (e ErrNotFound) Error() string { + if e.Msg != "" { + return fmt.Sprintf("not found %v at %v: %s", e.Field.Name, e.Parent.Type, e.Msg) + } + return fmt.Sprintf("not found %v at %v", e.Field.Name, e.Parent.Type) +} + +// structFieldInfo caches field mapping information for a struct type +type structFieldInfo struct { + fieldIndexMap map[int]int // thrift field ID -> struct field index + unknownFieldsIndex int // index of _unknownFields field, -1 if not present +} + +// fieldCache caches the struct field info for each type +var fieldCache sync.Map // map[reflect.Type]*structFieldInfo + +var ( + thriftUnknownFieldName = "_unknownFields" + thriftUnknownFieldNameOnce sync.Once +) + +func SetThriftUnknownFieldName(name string) { + thriftUnknownFieldNameOnce.Do(func() { + thriftUnknownFieldName = name + }) +} + +// getStructFieldInfo returns cached struct field info for the given type +func getStructFieldInfo(t reflect.Type) *structFieldInfo { + if cached, ok := fieldCache.Load(t); ok { + return cached.(*structFieldInfo) + } + + // Build the field info + numField := t.NumField() + info := &structFieldInfo{ + fieldIndexMap: make(map[int]int, numField), + unknownFieldsIndex: -1, + } + + for i := 0; i < numField; i++ { + field := t.Field(i) + + // Check for _unknownFields field + if field.Name == thriftUnknownFieldName { + info.unknownFieldsIndex = i + continue + } + + tag := field.Tag.Get("thrift") + if tag == "" { + continue + } + + // Parse thrift tag: "FieldName,ID" - use IndexByte for better performance + idx := strings.Split(tag, ",") + if len(idx) < 2 { + continue + } + + fieldID, err := strconv.Atoi(idx[1]) + if err != nil { + continue + } + + info.fieldIndexMap[fieldID] = i + } + + // Store in cache (use LoadOrStore to handle concurrent initialization) + actual, _ := fieldCache.LoadOrStore(t, info) + return actual.(*structFieldInfo) +} + +// fetchValue is the internal implementation that works with reflect.Value directly +// to avoid repeated interface{} boxing/unboxing overhead +func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { + // Dereference pointers + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, nil + } + v = v.Elem() + } + + switch desc.Kind { + case TypeKind_Struct: + return fetchStruct(desc, v, opt, stack) + + case TypeKind_StrMap: + return fetchStrMap(desc, v, opt, stack) + + case TypeKind_List: + return fetchList(desc, v, opt, stack) + + default: + return v.Interface(), nil + } +} + +// fetchStruct handles TypeKind_Struct +func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { + if v.Kind() != reflect.Struct { + return nil, nil + } + + result := make(map[string]interface{}, len(desc.Children)) + + // Get cached field info for this type + fieldInfo := getStructFieldInfo(v.Type()) + + // Parse unknownFields if present + var unknownFieldsMap map[thrift.FieldID]interface{} + if fieldInfo.unknownFieldsIndex >= 0 { + unknownFieldsValue := v.Field(fieldInfo.unknownFieldsIndex) + if unknownFieldsValue.Len() > 0 { + // unknownFields is []byte (unknown.Fields) + unknownBytes := unknownFieldsValue.Bytes() + var err error + unknownFieldsMap, err = parseUnknownFields(unknownBytes) + if err != nil { + return nil, err + } + } + } + + // Iterate through descriptor fields + for i := range desc.Children { + field := &desc.Children[i] + + // Find struct field by ID (thrift id) using cached index map + fieldIdx, found := fieldInfo.fieldIndexMap[field.ID] + if found { + fieldValue := v.Field(fieldIdx) + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + if opt.DisallowNotFound { + stack.push(TypeKind_Struct, field.Name, field.ID) + path := stack.buildPath() + stack.pop() + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d is nil at path %s", field.ID, path)} + } + continue + } + + // Push field onto stack + stack.push(TypeKind_Struct, field.Name, field.ID) + + // If field has a child descriptor, recursively fetch + var err error + if field.Desc != nil { + var fetched interface{} + fetched, err = fetchValue(field.Desc, fieldValue, opt, stack) + if err == nil { + result[field.Name] = fetched + } + } else { + // Otherwise, use the value directly + result[field.Name] = fieldValue.Interface() + } + + // Pop field from stack + stack.pop() + + if err != nil { + return nil, err + } + } else if unknownFieldsMap != nil { + // Try to get field from unknownFields + if val, ok := unknownFieldsMap[thrift.FieldID(field.ID)]; ok { + // Convert the value based on the field's Descriptor + // (e.g., map[FieldID]interface{} -> map[string]interface{} for nested structs) + result[field.Name] = fetchUnknownValue(val, field.Desc) + } else if opt.DisallowNotFound { + stack.push(TypeKind_Struct, field.Name, field.ID) + path := stack.buildPath() + stack.pop() + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields at path %s", field.ID, path)} + } + } else if opt.DisallowNotFound { + stack.push(TypeKind_Struct, field.Name, field.ID) + path := stack.buildPath() + stack.pop() + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct at path %s", field.ID, path)} + } + } + return result, nil +} + +// parseUnknownFields parses thrift binary encoded unknown fields and returns a map of field ID to value +func parseUnknownFields(data []byte) (map[thrift.FieldID]interface{}, error) { + if len(data) == 0 { + return nil, nil + } + + result := make(map[thrift.FieldID]interface{}) + p := thrift.BinaryProtocol{Buf: data} + + for p.Read < len(p.Buf) { + // Read field header + _, fieldType, fieldID, err := p.ReadFieldBegin() + if err != nil { + return nil, err + } + if fieldType == thrift.STOP { + break + } + + // Read field value using ReadAny + val, err := p.ReadAny(fieldType, false, false) + if err != nil { + return nil, err + } + + result[fieldID] = val + } + + return result, nil +} + +// fetchUnknownValue converts the value from unknownFields based on the field's Descriptor. +// For struct types, ReadAny returns map[FieldID]interface{}, which needs to be converted +// to map[string]interface{} using the Descriptor's Children field names. +func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { + if desc == nil { + return val + } + + switch desc.Kind { + case TypeKind_Struct: + // ReadAny returns map[FieldID]interface{} for STRUCT type + fieldIDMap, ok := val.(map[thrift.FieldID]interface{}) + if !ok { + return val + } + + // Build a map from field ID to Field for quick lookup + idToField := desc.ids + + // Convert map[FieldID]interface{} to map[string]interface{} + result := make(map[string]interface{}, len(fieldIDMap)) + for fieldID, fieldVal := range fieldIDMap { + if field, ok := idToField[int(fieldID)]; ok { + // Recursively convert nested values + result[field.Name] = fetchUnknownValue(fieldVal, field.Desc) + } + // Fields not in descriptor are ignored + } + return result + + case TypeKind_StrMap: + // ReadAny returns map[string]interface{} for string-keyed MAP type + strMap, ok := val.(map[string]interface{}) + if !ok { + return val + } + + // Find wildcard or keyed descriptors + keyDescMap := desc.names + + result := make(map[string]interface{}, len(strMap)) + for key, elem := range strMap { + if childDesc, ok := keyDescMap[key]; ok { + result[key] = fetchUnknownValue(elem, childDesc.Desc) + } else if len(desc.Children) == 1 && desc.Children[0].Name == "*" { + result[key] = fetchUnknownValue(elem, desc.Children[0].Desc) + } else { + result[key] = elem + } + } + return result + + case TypeKind_List: + // ReadAny returns []interface{} for LIST type + list, ok := val.([]interface{}) + if !ok { + return val + } + + // Check if wildcard descriptor ("*" means all elements) + if len(desc.Children) == 1 && desc.Children[0].Name == "*" { + childDesc := desc.Children[0].Desc + result := make([]interface{}, len(list)) + for i, elem := range list { + result[i] = fetchUnknownValue(elem, childDesc) + } + return result + } + + // Specific indices requested + result := make([]interface{}, 0, len(desc.Children)) + for _, child := range desc.Children { + idx := child.ID + if idx < 0 || idx >= len(list) { + continue + } + result = append(result, fetchUnknownValue(list[idx], child.Desc)) + } + return result + + default: + return val + } +} + +// fetchStrMap handles TypeKind_StrMap +func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { + if v.Kind() != reflect.Map || v.Type().Key().Kind() != reflect.String { + return nil, nil + } + + childrenLen := len(desc.Children) + + // Fast path: only wildcard descriptor + if childrenLen == 1 && desc.Children[0].Name == "*" { + wildcardDesc := desc.Children[0].Desc + result := make(map[string]interface{}, v.Len()) + iter := v.MapRange() + for iter.Next() { + keyStr := iter.Key().String() + elemValue := iter.Value() + + if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { + result[keyStr] = nil + continue + } + + // Push map key onto stack + stack.push(TypeKind_StrMap, keyStr, 0) + + var err error + if wildcardDesc != nil { + var fetched interface{} + fetched, err = fetchValue(wildcardDesc, elemValue, opt, stack) + if err == nil { + result[keyStr] = fetched + } + } else { + result[keyStr] = elemValue.Interface() + } + + // Pop map key from stack + stack.pop() + + if err != nil { + return nil, err + } + } + return result, nil + } + + // range over children + keyDescMap := desc.names + result := make(map[string]interface{}, childrenLen) + for key, child := range keyDescMap { + val := v.MapIndex(reflect.ValueOf(key)) + // Check if specific keys are requested but not available in the map + if !val.IsValid() { + if opt.DisallowNotFound { + stack.push(TypeKind_StrMap, key, 0) + path := stack.buildPath() + stack.pop() + return nil, ErrNotFound{Parent: desc, Field: keyDescMap[key], Msg: fmt.Sprintf("key '%s' not found in map at path %s", key, path)} + } else { + continue + } + } + if val.Kind() == reflect.Ptr && val.IsNil() { + result[key] = nil + continue + } + + // Push map key onto stack + stack.push(TypeKind_StrMap, key, 0) + + var err error + if child.Desc != nil { + var fetched interface{} + fetched, err = fetchValue(child.Desc, val, opt, stack) + if err == nil { + result[key] = fetched + } + } else { + result[key] = val.Interface() + } + + // Pop map key from stack + stack.pop() + + if err != nil { + return nil, err + } + } + + return result, nil +} + +// fetchList handles TypeKind_List +func fetchList(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { + kind := v.Kind() + if kind != reflect.Slice && kind != reflect.Array { + return nil, nil + } + + childrenLen := len(desc.Children) + + // Fast path: only wildcard descriptor ("*" means all elements) + if childrenLen == 1 && desc.Children[0].Name == "*" { + wildcardDesc := desc.Children[0].Desc + listLen := v.Len() + result := make([]interface{}, 0, listLen) + + for i := 0; i < listLen; i++ { + elemValue := v.Index(i) + + if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { + result = append(result, nil) + continue + } + + // Push list index onto stack + stack.push(TypeKind_List, "*", i) + + var err error + if wildcardDesc != nil { + var fetched interface{} + fetched, err = fetchValue(wildcardDesc, elemValue, opt, stack) + if err == nil { + result = append(result, fetched) + } + } else { + result = append(result, elemValue.Interface()) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return nil, err + } + } + return result, nil + } + + // Specific indices requested + result := make([]interface{}, 0, childrenLen) + for _, child := range desc.Children { + // Use Field.ID as the index + idx := child.ID + + // Check if index is out of bounds + if idx < 0 || idx >= v.Len() { + if opt.DisallowNotFound { + stack.push(TypeKind_List, "", idx) + path := stack.buildPath() + stack.pop() + return nil, ErrNotFound{Parent: desc, Field: child, Msg: fmt.Sprintf("index %d out of bounds (len=%d) at path %s", idx, v.Len(), path)} + } + continue + } + + elemValue := v.Index(idx) + if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { + result = append(result, nil) + continue + } + + // Push list index onto stack + stack.push(TypeKind_List, "", idx) + + var err error + if child.Desc != nil { + var fetched interface{} + fetched, err = fetchValue(child.Desc, elemValue, opt, stack) + if err == nil { + result = append(result, fetched) + } + } else { + result = append(result, elemValue.Interface()) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return nil, err + } + } + + return result, nil +} diff --git a/trim/fetch_test.go b/trim/fetch_test.go new file mode 100644 index 00000000..e000ae17 --- /dev/null +++ b/trim/fetch_test.go @@ -0,0 +1,1902 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * 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 + * + * https://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 trim + +import ( + "fmt" + "reflect" + "testing" + + "github.com/cloudwego/dynamicgo/thrift" + "github.com/cloudwego/thriftgo/generator/golang/extension/unknown" +) + +type sampleFetch struct { + FieldA int `thrift:"FieldA,1" json:"field_a,omitempty"` + FieldB []*sampleFetch `thrift:"FieldB,2" json:"field_b,omitempty"` + FieldC map[string]*sampleFetch `thrift:"FieldC,3" json:"field_c,omitempty"` + FieldD *sampleFetch `thrift:"FieldD,4" json:"field_d,omitempty"` + FieldE string `thrift:"FieldE,5" json:"field_e,omitempty"` + FieldList []int `thrift:"FieldList,6" json:"field_list,omitempty"` + FieldMap map[string]int `thrift:"FieldMap,7" json:"field_map,omitempty"` + _unknownFields unknown.Fields `json:"-"` +} + +func makeSampleFetch(width int, depth int) *sampleFetch { + if depth <= 0 { + return nil + } + ret := &sampleFetch{ + FieldA: 1, + FieldE: "1", + FieldC: make(map[string]*sampleFetch), + FieldList: []int{1, 2, 3}, + FieldMap: map[string]int{ + "1": 1, + "2": 2, + "3": 3, + }, + } + for i := 0; i < width; i++ { + ret.FieldB = append(ret.FieldB, makeSampleFetch(width, depth-1)) + ret.FieldC[fmt.Sprintf("%d", i)] = makeSampleFetch(width, depth-1) + } + ret.FieldD = makeSampleFetch(width, depth-1) + return ret +} + +// makeDesc generates a descriptor for fetching SampleFetch struct. +// NOTICE: it ignores FieldE. +func makeDesc(width int, depth int, withE bool) *Descriptor { + if depth <= 0 { + return nil + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + { + Name: "field_a", + ID: 1, + }, + { + Name: "field_b", + ID: 2, + }, + { + Name: "field_c", + ID: 3, + }, + { + Name: "field_d", + ID: 4, + }, + { + Name: "field_list", + ID: 6, + }, + { + Name: "field_map", + ID: 7, + }, + }, + } + + if withE { + desc.Children = append(desc.Children, Field{ + Name: "field_e", + ID: 5, + }) + } + + nd := makeDesc(width, depth-1, withE) + desc.Children[2].Desc = &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: nd, + }, + }, + } + desc.Children[3].Desc = nd + // field_list is TypeKind_List with wildcard (all elements) + desc.Children[4].Desc = &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "*", // all elements + // Desc is nil, meaning list elements are scalar (int) + }, + }, + } + + desc.Normalize() + return desc +} + +// makeSampleAny generates a map[string]interface{} for fetching SampleFetch struct. +// NOTICE: it ignores FieldE and nil field_d at leaf level. +func makeSampleAny(width int, depth int) interface{} { + if depth <= 0 { + return nil + } + ret := map[string]interface{}{ + "field_a": int(1), + "field_b": []*sampleFetch{}, + "field_c": map[string]interface{}{}, + "field_list": []interface{}{int(1), int(2), int(3)}, + "field_map": map[string]int{ + "1": 1, + "2": 2, + "3": 3, + }, + } + for i := 0; i < width; i++ { + ret["field_b"] = append(ret["field_b"].([]*sampleFetch), makeSampleFetch(width, depth-1)) + ret["field_c"].(map[string]interface{})[fmt.Sprintf("%d", i)] = makeSampleAny(width, depth-1) + } + // Only include field_d if it's not nil (depth > 1 means child will not be nil) + childD := makeSampleAny(width, depth-1) + if childD != nil { + ret["field_d"] = childD + } + return ret +} + +func TestFetchAny(t *testing.T) { + width := 2 + depth := 2 + obj := makeSampleFetch(width, depth) + desc := makeDesc(width, depth, false) + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + exp := makeSampleAny(width, depth) + if !reflect.DeepEqual(ret, exp) { + t.Fatalf("FetchAny failed: %v != %v", ret, exp) + } +} + +// TestFetchAny_ListWithSpecificIndices tests fetching list elements by specific indices +func TestFetchAny_ListWithSpecificIndices(t *testing.T) { + // Create a struct with a list field + obj := &sampleFetch{ + FieldA: 42, + FieldList: []int{10, 20, 30, 40, 50}, + } + + // Test case 1: Fetch specific indices (0, 2, 4) + t.Run("specific_indices", func(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + { + Name: "field_a", + ID: 1, + }, + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "0", ID: 0}, // index 0 + {Name: "2", ID: 2}, // index 2 + {Name: "4", ID: 4}, // index 4 + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + listVal, ok := result["field_list"].([]interface{}) + if !ok { + t.Fatalf("field_list: expected []interface{}, got %T", result["field_list"]) + } + + // Should only have 3 elements (indices 0, 2, 4) + if len(listVal) != 3 { + t.Errorf("field_list: expected length 3, got %d", len(listVal)) + } + + expectedValues := []int{10, 30, 50} + for i, expected := range expectedValues { + if listVal[i] != expected { + t.Errorf("field_list[%d]: expected %d, got %v", i, expected, listVal[i]) + } + } + }) + + // Test case 2: Fetch with out of bounds index (should skip) + t.Run("out_of_bounds_index", func(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "1", ID: 1}, // index 1 - valid + {Name: "10", ID: 10}, // index 10 - out of bounds + {Name: "3", ID: 3}, // index 3 - valid + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + listVal, ok := result["field_list"].([]interface{}) + if !ok { + t.Fatalf("field_list: expected []interface{}, got %T", result["field_list"]) + } + + // Should only have 2 elements (indices 1, 3), index 10 is skipped + if len(listVal) != 2 { + t.Errorf("field_list: expected length 2, got %d", len(listVal)) + } + + if listVal[0] != 20 { + t.Errorf("field_list[0]: expected 20, got %v", listVal[0]) + } + if listVal[1] != 40 { + t.Errorf("field_list[1]: expected 40, got %v", listVal[1]) + } + }) + + // Test case 3: DisallowNotFound with out of bounds + t.Run("disallow_not_found_out_of_bounds", func(t *testing.T) { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + {Name: "1", ID: 1}, // index 1 - valid + {Name: "10", ID: 10}, // index 10 - out of bounds + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + _, err := f.FetchAny(desc, obj) + if err == nil { + t.Fatalf("expected ErrNotFound, got nil") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T: %v", err, err) + } + if notFoundErr.Parent.Type != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Type) + } + }) + + // Test case 4: List with nested structures + t.Run("list_with_nested_structs", func(t *testing.T) { + objWithNested := &sampleFetch{ + FieldB: []*sampleFetch{ + {FieldA: 1, FieldE: "first"}, + {FieldA: 2, FieldE: "second"}, + {FieldA: 3, FieldE: "third"}, + {FieldA: 4, FieldE: "fourth"}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Type: "LIST", + Children: []Field{ + { + Name: "0", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + { + Name: "2", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, objWithNested) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + listVal, ok := result["field_b"].([]interface{}) + if !ok { + t.Fatalf("field_b: expected []interface{}, got %T", result["field_b"]) + } + + if len(listVal) != 2 { + t.Fatalf("field_b: expected length 2, got %d", len(listVal)) + } + + // Check first element (index 0, should have field_a) + elem0, ok := listVal[0].(map[string]interface{}) + if !ok { + t.Fatalf("field_b[0]: expected map[string]interface{}, got %T", listVal[0]) + } + if elem0["field_a"] != 1 { + t.Errorf("field_b[0].field_a: expected 1, got %v", elem0["field_a"]) + } + if _, hasE := elem0["field_e"]; hasE { + t.Errorf("field_b[0] should not have field_e") + } + + // Check second element (index 2, should have field_e) + elem2, ok := listVal[1].(map[string]interface{}) + if !ok { + t.Fatalf("field_b[1]: expected map[string]interface{}, got %T", listVal[1]) + } + if elem2["field_e"] != "third" { + t.Errorf("field_b[1].field_e: expected 'third', got %v", elem2["field_e"]) + } + if _, hasA := elem2["field_a"]; hasA { + t.Errorf("field_b[1] should not have field_a") + } + }) +} + +func BenchmarkFetchAny(b *testing.B) { + benchmarks := []struct { + name string + width int + depth int + }{ + {"small_2x2", 2, 2}, + {"medium_3x3", 3, 3}, + {"large_4x4", 4, 4}, + {"wide_5x2", 5, 2}, + {"deep_2x5", 2, 5}, + } + + for _, bm := range benchmarks { + obj := makeSampleFetch(bm.width, bm.depth) + desc := makeDesc(bm.width, bm.depth, false) + + b.Run(bm.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = fetchAny(desc, obj) + } + }) + } +} + +func BenchmarkFetchAny_CacheHit(b *testing.B) { + // This benchmark specifically tests the performance with cache hit + // by running FetchAny once before benchmark to warm up the cache + width := 3 + depth := 3 + obj := makeSampleFetch(width, depth) + desc := makeDesc(width, depth, false) + + // Warm up the cache + _, _ = fetchAny(desc, obj) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = fetchAny(desc, obj) + } +} + +// sampleWithUnknown is a struct that has a subset of fields compared to SampleFetch +// It simulates a scenario where some fields are stored in _unknownFields +type sampleWithUnknown struct { + FieldA int `thrift:"FieldA,1"` + // FieldB, FieldC, FieldD, FieldE are not declared here, they will be in _unknownFields + _unknownFields unknown.Fields +} + +func TestFetchAnyWithUnknownFields(t *testing.T) { + // Create a struct with some fields in _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + // Encode various types into _unknownFields using BinaryProtocol + p := thrift.BinaryProtocol{} + + // Encode FieldE (id=5, string type) + p.WriteFieldBegin("", thrift.STRING, 5) + p.WriteString("hello") + + // Encode custom field (id=6, i32 type) + p.WriteFieldBegin("", thrift.I32, 6) + p.WriteI32(100) + + // Encode list field (id=20) + p.WriteFieldBegin("", thrift.LIST, 20) + p.WriteListBegin(thrift.I32, 3) + p.WriteI32(10) + p.WriteI32(20) + p.WriteI32(30) + p.WriteListEnd() + + // Encode map field (id=21) + p.WriteFieldBegin("", thrift.MAP, 21) + p.WriteMapBegin(thrift.STRING, thrift.I32, 2) + p.WriteString("key1") + p.WriteI32(100) + p.WriteString("key2") + p.WriteI32(200) + p.WriteMapEnd() + + obj._unknownFields = p.Buf + + // Create a descriptor that asks for all fields + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, // Static field + {Name: "field_e", ID: 5}, // From unknownFields (string) + {Name: "custom_field", ID: 6}, // From unknownFields (i32) + {Name: "list_field", ID: 20}, // From unknownFields (list) + {Name: "map_field", ID: 21}, // From unknownFields (map) + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check field_a (static field) + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + // Check field_e (from unknownFields) + if result["field_e"] != "hello" { + t.Errorf("field_e: expected 'hello', got %v", result["field_e"]) + } + + // Check custom_field (from unknownFields) + if result["custom_field"] != int32(100) { + t.Errorf("custom_field: expected 100, got %v", result["custom_field"]) + } + + // Check list_field + listVal, ok := result["list_field"].([]interface{}) + if !ok { + t.Fatalf("list_field: expected []interface{}, got %T", result["list_field"]) + } + if len(listVal) != 3 { + t.Errorf("list_field: expected length 3, got %d", len(listVal)) + } + expectedList := []int32{10, 20, 30} + for i, v := range listVal { + if v != expectedList[i] { + t.Errorf("list_field[%d]: expected %d, got %v", i, expectedList[i], v) + } + } + + // Check map_field + mapVal, ok := result["map_field"].(map[string]interface{}) + if !ok { + t.Fatalf("map_field: expected map[string]interface{}, got %T", result["map_field"]) + } + if mapVal["key1"] != int32(100) { + t.Errorf("map_field['key1']: expected 100, got %v", mapVal["key1"]) + } + if mapVal["key2"] != int32(200) { + t.Errorf("map_field['key2']: expected 200, got %v", mapVal["key2"]) + } +} + +func TestFetchAnyWithEmptyUnknownFields(t *testing.T) { + // Create a struct with empty _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, // Not present in unknownFields + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check field_a (static field) + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + // field_e should not be present since unknownFields is empty + if _, exists := result["field_e"]; exists { + t.Errorf("field_e should not exist when unknownFields is empty") + } +} + +func TestFetchAnyWithDisallowNotFound(t *testing.T) { + t.Run("struct field not found", func(t *testing.T) { + obj := &sampleWithUnknown{FieldA: 42} + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, // Not present + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + _, err := f.FetchAny(desc, obj) + if err == nil { + t.Fatalf("expected ErrNotFound, got nil") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T: %v", err, err) + } + if notFoundErr.Parent.Type != "SampleWithUnknown" { + t.Errorf("expected parent name 'SampleWithUnknown', got '%s'", notFoundErr.Parent.Type) + } + if notFoundErr.Field.Name != "field_e" { + t.Errorf("expected field name 'field_e', got '%s'", notFoundErr.Field.Name) + } + }) + + t.Run("map key not found", func(t *testing.T) { + obj := &struct { + Data map[string]int `thrift:"Data,1"` + }{ + Data: map[string]int{"key1": 100}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Test", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + {Name: "key1"}, // exists + {Name: "key2"}, // not exists + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + _, err := f.FetchAny(desc, obj) + if err == nil { + t.Fatalf("expected ErrNotFound, got nil") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T: %v", err, err) + } + if notFoundErr.Parent.Type != "MAP" { + t.Errorf("expected parent name 'MAP', got '%s'", notFoundErr.Parent.Type) + } + if notFoundErr.Field.Name != "key2" { + t.Errorf("expected field name 'key2', got '%s'", notFoundErr.Field.Name) + } + }) + + t.Run("nested struct field not found", func(t *testing.T) { + obj := &struct { + Inner *struct { + Value int `thrift:"Value,1"` + } `thrift:"Inner,1"` + }{ + Inner: &struct { + Value int `thrift:"Value,1"` + }{Value: 100}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "Outer", + Children: []Field{ + { + Name: "inner", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Inner", + Children: []Field{ + {Name: "value", ID: 1}, // exists + {Name: "missing", ID: 99}, // not exists + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + _, err := f.FetchAny(desc, obj) + if err == nil { + t.Fatalf("expected ErrNotFound, got nil") + } + + notFoundErr, ok := err.(ErrNotFound) + if !ok { + t.Fatalf("expected ErrNotFound, got %T: %v", err, err) + } + if notFoundErr.Parent.Type != "Inner" { + t.Errorf("expected parent name 'Inner', got '%s'", notFoundErr.Parent.Type) + } + if notFoundErr.Field.Name != "missing" { + t.Errorf("expected field name 'missing', got '%s'", notFoundErr.Field.Name) + } + }) + + t.Run("no error when all fields found", func(t *testing.T) { + obj := &sampleWithUnknown{FieldA: 42} + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, // exists + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + ret, err := f.FetchAny(desc, obj) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + }) +} + +// TestFetchAnyWithUnknownFieldsStruct tests fetching struct type from unknownFields +// This covers two cases: +// 1. No further fetch: Descriptor is nil, the struct is returned as-is (map[FieldID]interface{}) +// 2. With further fetch: Descriptor is provided, the struct is converted to map[string]interface{} +// ===================== Circular Reference Tests ===================== +// These tests verify that FetchAny can handle circular reference type descriptions. +// The key principle is: recursively process data until data is nil (any == nil). + +// circularNode represents a node that can reference itself (like a linked list or tree) +type circularNode struct { + Value int `thrift:"Value,1" json:"value,omitempty"` + Next *circularNode `thrift:"Next,2" json:"next,omitempty"` +} + +// circularTree represents a tree node that can reference itself +type circularTree struct { + Value int `thrift:"Value,1" json:"value,omitempty"` + Left *circularTree `thrift:"Left,2" json:"left,omitempty"` + Right *circularTree `thrift:"Right,3" json:"right,omitempty"` + Children []*circularTree `thrift:"Children,4" json:"children,omitempty"` +} + +// makeCircularDesc creates a descriptor that references itself (circular reference) +func makeCircularDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularNode", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "next", ID: 2}, + }, + } + // Make it circular: next field's Desc points back to the same descriptor + desc.Children[1].Desc = desc + return desc +} + +// makeCircularTreeDesc creates a tree descriptor that references itself +func makeCircularTreeDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularTree", + Children: []Field{ + {Name: "value", ID: 1}, + {Name: "left", ID: 2}, + {Name: "right", ID: 3}, + {Name: "children", ID: 4}, + }, + } + // Make it circular: left, right fields' Desc point back to the same descriptor + desc.Children[1].Desc = desc + desc.Children[2].Desc = desc + // children is a list, no further Desc needed (will be handled as raw value) + return desc +} + +func TestFetchAny_CircularDescriptor_LinkedList(t *testing.T) { + // Create a linked list: 1 -> 2 -> 3 -> nil + list := &circularNode{ + Value: 1, + Next: &circularNode{ + Value: 2, + Next: &circularNode{ + Value: 3, + Next: nil, // Termination point + }, + }, + } + + desc := makeCircularDesc() + + // Fetch should work correctly, recursing until Next is nil + fetched, err := fetchAny(desc, list) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + // Verify the fetched structure + result, ok := fetched.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", fetched) + } + + if result["value"] != 1 { + t.Errorf("value: expected 1, got %v", result["value"]) + } + + next1, ok := result["next"].(map[string]interface{}) + if !ok { + t.Fatalf("next: expected map[string]interface{}, got %T", result["next"]) + } + if next1["value"] != 2 { + t.Errorf("next.value: expected 2, got %v", next1["value"]) + } + + next2, ok := next1["next"].(map[string]interface{}) + if !ok { + t.Fatalf("next.next: expected map[string]interface{}, got %T", next1["next"]) + } + if next2["value"] != 3 { + t.Errorf("next.next.value: expected 3, got %v", next2["value"]) + } + + // The last node's next should not be present (nil) + if _, exists := next2["next"]; exists { + t.Errorf("next.next.next should not exist (nil)") + } +} + +func TestFetchAny_CircularDescriptor_SingleNode(t *testing.T) { + // Single node with nil next + node := &circularNode{ + Value: 42, + Next: nil, + } + + desc := makeCircularDesc() + + fetched, err := fetchAny(desc, node) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := fetched.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", fetched) + } + + if result["value"] != 42 { + t.Errorf("value: expected 42, got %v", result["value"]) + } + + // next should not be present + if _, exists := result["next"]; exists { + t.Errorf("next should not exist for nil pointer") + } +} + +func TestFetchAny_CircularDescriptor_Tree(t *testing.T) { + // Create a binary tree: + // 1 + // / \ + // 2 3 + // / + // 4 + tree := &circularTree{ + Value: 1, + Left: &circularTree{ + Value: 2, + Left: &circularTree{ + Value: 4, + }, + }, + Right: &circularTree{ + Value: 3, + }, + } + + desc := makeCircularTreeDesc() + + fetched, err := fetchAny(desc, tree) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := fetched.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", fetched) + } + + // Verify root + if result["value"] != 1 { + t.Errorf("value: expected 1, got %v", result["value"]) + } + + // Verify left subtree + left, ok := result["left"].(map[string]interface{}) + if !ok { + t.Fatalf("left: expected map[string]interface{}, got %T", result["left"]) + } + if left["value"] != 2 { + t.Errorf("left.value: expected 2, got %v", left["value"]) + } + + leftLeft, ok := left["left"].(map[string]interface{}) + if !ok { + t.Fatalf("left.left: expected map[string]interface{}, got %T", left["left"]) + } + if leftLeft["value"] != 4 { + t.Errorf("left.left.value: expected 4, got %v", leftLeft["value"]) + } + + // Verify right subtree + right, ok := result["right"].(map[string]interface{}) + if !ok { + t.Fatalf("right: expected map[string]interface{}, got %T", result["right"]) + } + if right["value"] != 3 { + t.Errorf("right.value: expected 3, got %v", right["value"]) + } +} + +func TestFetchAny_CircularDescriptor_NilRoot(t *testing.T) { + desc := makeCircularDesc() + + // Fetch with nil input should return nil + fetched, err := fetchAny(desc, nil) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + if fetched != nil { + t.Errorf("expected nil, got %v", fetched) + } +} + +func TestFetchAny_CircularDescriptor_DeepList(t *testing.T) { + // Create a deep linked list (depth=100) to stress test + depth := 100 + var head *circularNode + for i := depth; i > 0; i-- { + head = &circularNode{ + Value: i, + Next: head, + } + } + + desc := makeCircularDesc() + + fetched, err := fetchAny(desc, head) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + // Verify the structure by traversing + current := fetched + for i := 1; i <= depth; i++ { + m, ok := current.(map[string]interface{}) + if !ok { + t.Fatalf("at depth %d: expected map[string]interface{}, got %T", i, current) + } + if m["value"] != i { + t.Errorf("at depth %d: expected value %d, got %v", i, i, m["value"]) + } + if i < depth { + current = m["next"] + } else { + // Last node should not have next + if _, exists := m["next"]; exists { + t.Errorf("last node should not have next") + } + } + } +} + +// circularMapNode represents a node with a map that can contain circular references +type circularMapNode struct { + Name string `thrift:"Name,1" json:"name,omitempty"` + Children map[string]*circularMapNode `thrift:"Children,2" json:"children,omitempty"` +} + +func makeCircularMapDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "CircularMapNode", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "children", ID: 2}, + }, + } + // Make children field circular: it's a map with values of the same type + desc.Children[1].Desc = &Descriptor{ + Kind: TypeKind_StrMap, + Type: "ChildrenMap", + Children: []Field{ + {Name: "*", Desc: desc}, // Wildcard with circular reference + }, + } + return desc +} + +func TestFetchAny_CircularDescriptor_MapOfNodes(t *testing.T) { + // Create a tree-like structure using maps: + // root + // ├── child1 + // │ └── grandchild1 + // └── child2 + node := &circularMapNode{ + Name: "root", + Children: map[string]*circularMapNode{ + "child1": { + Name: "child1", + Children: map[string]*circularMapNode{ + "grandchild1": { + Name: "grandchild1", + Children: nil, // Termination + }, + }, + }, + "child2": { + Name: "child2", + Children: nil, // Termination + }, + }, + } + + desc := makeCircularMapDesc() + + fetched, err := fetchAny(desc, node) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := fetched.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", fetched) + } + + if result["name"] != "root" { + t.Errorf("name: expected 'root', got %v", result["name"]) + } + + children, ok := result["children"].(map[string]interface{}) + if !ok { + t.Fatalf("children: expected map[string]interface{}, got %T", result["children"]) + } + + child1, ok := children["child1"].(map[string]interface{}) + if !ok { + t.Fatalf("child1: expected map[string]interface{}, got %T", children["child1"]) + } + if child1["name"] != "child1" { + t.Errorf("child1.name: expected 'child1', got %v", child1["name"]) + } + + child1Children, ok := child1["children"].(map[string]interface{}) + if !ok { + t.Fatalf("child1.children: expected map[string]interface{}, got %T", child1["children"]) + } + + grandchild1, ok := child1Children["grandchild1"].(map[string]interface{}) + if !ok { + t.Fatalf("grandchild1: expected map[string]interface{}, got %T", child1Children["grandchild1"]) + } + if grandchild1["name"] != "grandchild1" { + t.Errorf("grandchild1.name: expected 'grandchild1', got %v", grandchild1["name"]) + } + + child2, ok := children["child2"].(map[string]interface{}) + if !ok { + t.Fatalf("child2: expected map[string]interface{}, got %T", children["child2"]) + } + if child2["name"] != "child2" { + t.Errorf("child2.name: expected 'child2', got %v", child2["name"]) + } +} + +func BenchmarkFetchAny_CircularDescriptor(b *testing.B) { + // Create a linked list of depth 10 + depth := 10 + var head *circularNode + for i := depth; i > 0; i-- { + head = &circularNode{ + Value: i, + Next: head, + } + } + + desc := makeCircularDesc() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = fetchAny(desc, head) + } +} + +func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { + t.Run("nested struct without descriptor (no further fetch)", func(t *testing.T) { + // Create a struct with a nested struct in _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + // Encode a nested struct into _unknownFields (id=10) + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRUCT, 10) + // Write nested struct fields + p.WriteFieldBegin("", thrift.STRING, 1) // field 1: string + p.WriteString("nested_value") + p.WriteFieldBegin("", thrift.I32, 2) // field 2: i32 + p.WriteI32(999) + p.WriteFieldStop() // end of nested struct + + obj._unknownFields = p.Buf + + // Create a descriptor that asks for the nested struct but WITHOUT a Desc + // This means we don't want to further fetch into the struct + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "nested_struct", ID: 10}, // No Desc, so no further fetch + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check field_a + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + // Check nested_struct: without Desc, it should be map[FieldID]interface{} + nestedVal, exists := result["nested_struct"] + if !exists { + t.Fatalf("nested_struct should exist") + } + + nestedMap, ok := nestedVal.(map[thrift.FieldID]interface{}) + if !ok { + t.Fatalf("nested_struct: expected map[thrift.FieldID]interface{}, got %T", nestedVal) + } + + // Verify the nested struct content + if nestedMap[1] != "nested_value" { + t.Errorf("nested_struct[1]: expected 'nested_value', got %v", nestedMap[1]) + } + if nestedMap[2] != int32(999) { + t.Errorf("nested_struct[2]: expected 999, got %v", nestedMap[2]) + } + }) + + t.Run("nested struct with descriptor (with further fetch)", func(t *testing.T) { + // Create a struct with a nested struct in _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + // Encode a nested struct into _unknownFields (id=10) + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRUCT, 10) + // Write nested struct fields + p.WriteFieldBegin("", thrift.STRING, 1) // field 1: string + p.WriteString("nested_value") + p.WriteFieldBegin("", thrift.I32, 2) // field 2: i32 + p.WriteI32(999) + p.WriteFieldBegin("", thrift.STRING, 3) // field 3: string (will be ignored) + p.WriteString("ignored_value") + p.WriteFieldStop() // end of nested struct + + obj._unknownFields = p.Buf + + // Create a descriptor that asks for the nested struct WITH a Desc + // This means we want to further fetch into the struct and convert it + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_struct", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "NestedStruct", + Children: []Field{ + {Name: "name", ID: 1}, // maps to field 1 + {Name: "count", ID: 2}, // maps to field 2 + // field 3 is not in the descriptor, so it will be ignored + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check field_a + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + // Check nested_struct: with Desc, it should be map[string]interface{} + nestedVal, exists := result["nested_struct"] + if !exists { + t.Fatalf("nested_struct should exist") + } + + nestedMap, ok := nestedVal.(map[string]interface{}) + if !ok { + t.Fatalf("nested_struct: expected map[string]interface{}, got %T", nestedVal) + } + + // Verify the nested struct content with field names + if nestedMap["name"] != "nested_value" { + t.Errorf("nested_struct['name']: expected 'nested_value', got %v", nestedMap["name"]) + } + if nestedMap["count"] != int32(999) { + t.Errorf("nested_struct['count']: expected 999, got %v", nestedMap["count"]) + } + + // field 3 should not be present (not in descriptor) + if _, exists := nestedMap["ignored"]; exists { + t.Errorf("nested_struct should not have 'ignored' field") + } + // Also verify that only 2 keys are present + if len(nestedMap) != 2 { + t.Errorf("nested_struct: expected 2 fields, got %d", len(nestedMap)) + } + }) + + t.Run("deeply nested struct with descriptor", func(t *testing.T) { + // Create a struct with a deeply nested struct in _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + // Encode a nested struct with another nested struct inside (id=10) + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.STRUCT, 10) + // Write level 1 nested struct fields + p.WriteFieldBegin("", thrift.STRING, 1) // field 1: string + p.WriteString("level1") + p.WriteFieldBegin("", thrift.STRUCT, 2) // field 2: nested struct + // Write level 2 nested struct fields + p.WriteFieldBegin("", thrift.STRING, 1) // field 1: string + p.WriteString("level2") + p.WriteFieldBegin("", thrift.I64, 2) // field 2: i64 + p.WriteI64(12345) + p.WriteFieldStop() // end of level 2 struct + p.WriteFieldStop() // end of level 1 struct + + obj._unknownFields = p.Buf + + // Create a descriptor for deeply nested fetch + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "level1_struct", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Level1Struct", + Children: []Field{ + {Name: "level1_name", ID: 1}, + { + Name: "level2_struct", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Level2Struct", + Children: []Field{ + {Name: "level2_name", ID: 1}, + {Name: "level2_value", ID: 2}, + }, + }, + }, + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check field_a + if result["field_a"] != 42 { + t.Errorf("field_a: expected 42, got %v", result["field_a"]) + } + + // Check level1_struct + level1, ok := result["level1_struct"].(map[string]interface{}) + if !ok { + t.Fatalf("level1_struct: expected map[string]interface{}, got %T", result["level1_struct"]) + } + + if level1["level1_name"] != "level1" { + t.Errorf("level1_struct['level1_name']: expected 'level1', got %v", level1["level1_name"]) + } + + // Check level2_struct + level2, ok := level1["level2_struct"].(map[string]interface{}) + if !ok { + t.Fatalf("level2_struct: expected map[string]interface{}, got %T", level1["level2_struct"]) + } + + if level2["level2_name"] != "level2" { + t.Errorf("level2_struct['level2_name']: expected 'level2', got %v", level2["level2_name"]) + } + if level2["level2_value"] != int64(12345) { + t.Errorf("level2_struct['level2_value']: expected 12345, got %v", level2["level2_value"]) + } + }) + + t.Run("struct in map in unknownFields", func(t *testing.T) { + // Create a struct with a map in _unknownFields + obj := &sampleWithUnknown{ + FieldA: 42, + } + + // Encode a map into _unknownFields (id=10) + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.MAP, 10) + p.WriteMapBegin(thrift.STRING, thrift.STRUCT, 2) + // First entry + p.WriteString("key1") + p.WriteFieldBegin("", thrift.STRING, 1) + p.WriteString("value1") + p.WriteFieldBegin("", thrift.I32, 2) + p.WriteI32(100) + p.WriteFieldStop() + // Second entry + p.WriteString("key2") + p.WriteFieldBegin("", thrift.STRING, 1) + p.WriteString("value2") + p.WriteFieldBegin("", thrift.I32, 2) + p.WriteI32(200) + p.WriteFieldStop() + p.WriteMapEnd() + + obj._unknownFields = p.Buf + + // Create a descriptor with map containing struct descriptor + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "data_map", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "DataMap", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "Data", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "count", ID: 2}, + }, + }, + }, + }, + }, + }, + }, + } + + ret, err := fetchAny(desc, obj) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + + result, ok := ret.(map[string]interface{}) + if !ok { + t.Fatalf("expected map[string]interface{}, got %T", ret) + } + + // Check data_map + dataMap, ok := result["data_map"].(map[string]interface{}) + if !ok { + t.Fatalf("data_map: expected map[string]interface{}, got %T", result["data_map"]) + } + + if len(dataMap) != 2 { + t.Fatalf("data_map: expected 2 entries, got %d", len(dataMap)) + } + + // Check key1 + data1, ok := dataMap["key1"].(map[string]interface{}) + if !ok { + t.Fatalf("data_map['key1']: expected map[string]interface{}, got %T", dataMap["key1"]) + } + if data1["name"] != "value1" { + t.Errorf("data_map['key1']['name']: expected 'value1', got %v", data1["name"]) + } + if data1["count"] != int32(100) { + t.Errorf("data_map['key1']['count']: expected 100, got %v", data1["count"]) + } + + // Check key2 + data2, ok := dataMap["key2"].(map[string]interface{}) + if !ok { + t.Fatalf("data_map['key2']: expected map[string]interface{}, got %T", dataMap["key2"]) + } + if data2["name"] != "value2" { + t.Errorf("data_map['key2']['name']: expected 'value2', got %v", data2["name"]) + } + if data2["count"] != int32(200) { + t.Errorf("data_map['key2']['count']: expected 200, got %v", data2["count"]) + } + }) +} + +// TestFetchAny_PathTracking tests that error messages include the correct DSL path +func TestFetchAny_PathTracking(t *testing.T) { + tests := []struct { + name string + obj interface{} + desc *Descriptor + expectedErr string + }{ + { + name: "field not found at root", + obj: &sampleFetch{ + FieldA: 42, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "unknown_field", ID: 99}, + }, + }, + expectedErr: "field ID=99 not found in struct at path $.unknown_field", + }, + { + name: "field not found in nested struct", + obj: &sampleFetch{ + FieldA: 1, + FieldD: &sampleFetch{ + FieldA: 2, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "missing_field", ID: 88}, + }, + }, + }, + }, + }, + expectedErr: "field ID=88 not found in struct at path $.field_d.missing_field", + }, + { + name: "field not found in deeply nested struct", + obj: &sampleFetch{ + FieldA: 1, + FieldD: &sampleFetch{ + FieldA: 2, + FieldD: &sampleFetch{ + FieldA: 3, + }, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "bad_field", ID: 77}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: "field ID=77 not found in struct at path $.field_d.field_d.bad_field", + }, + { + name: "map key not found", + obj: &sampleFetch{ + FieldA: 1, + FieldC: map[string]*sampleFetch{ + "key1": {FieldA: 10}, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + {Name: "key1"}, + {Name: "missing_key"}, + }, + }, + }, + }, + }, + expectedErr: "key 'missing_key' not found in map at path $.field_c[missing_key]", + }, + { + name: "field not found in map value", + obj: &sampleFetch{ + FieldA: 1, + FieldC: map[string]*sampleFetch{ + "item1": {FieldA: 10}, + }, + }, + desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "nonexistent", ID: 66}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: "field ID=66 not found in struct at path $.field_c[item1].nonexistent", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + tt.desc.Normalize() + _, err := f.FetchAny(tt.desc, tt.obj) + if err == nil { + t.Fatalf("expected error, got nil") + } + if !contains(err.Error(), tt.expectedErr) { + t.Errorf("expected error to contain:\n%s\ngot:\n%s", tt.expectedErr, err.Error()) + } + }) + } +} + +// TestFetchAny_PathTracking_Integration tests path tracking in complex nested scenarios +func TestFetchAny_PathTracking_Integration(t *testing.T) { + obj := &sampleFetch{ + FieldA: 1, + FieldC: map[string]*sampleFetch{ + "item1": { + FieldA: 10, + FieldD: &sampleFetch{ + FieldA: 20, + }, + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "missing", ID: 55}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + _, err := f.FetchAny(desc, obj) + if err == nil { + t.Fatalf("expected error, got nil") + } + + expectedPath := "$.field_c[item1].field_d.missing" + if !contains(err.Error(), expectedPath) { + t.Errorf("expected error to contain path %s, got: %s", expectedPath, err.Error()) + } +} + +// BenchmarkFetchAny_PathTracking benchmarks the overhead of path tracking in fetch +func BenchmarkFetchAny_PathTracking(b *testing.B) { + b.Run("simple_struct", func(b *testing.B) { + obj := &sampleFetch{ + FieldA: 42, + FieldE: "hello", + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + f := Fetcher{} + desc.Normalize() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = f.FetchAny(desc, obj) + } + }) + + b.Run("nested_struct", func(b *testing.B) { + obj := &sampleFetch{ + FieldA: 1, + FieldD: &sampleFetch{ + FieldA: 2, + FieldD: &sampleFetch{ + FieldA: 3, + FieldE: "nested", + }, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{} + desc.Normalize() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = f.FetchAny(desc, obj) + } + }) + + b.Run("with_map", func(b *testing.B) { + obj := &sampleFetch{ + FieldA: 1, + FieldC: map[string]*sampleFetch{ + "key1": {FieldA: 10, FieldE: "v1"}, + "key2": {FieldA: 20, FieldE: "v2"}, + "key3": {FieldA: 30, FieldE: "v3"}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Type: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{} + desc.Normalize() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = f.FetchAny(desc, obj) + } + }) + + b.Run("error_case", func(b *testing.B) { + obj := &sampleFetch{ + FieldA: 1, + FieldD: &sampleFetch{ + FieldA: 2, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Type: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "missing", ID: 99}, + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = f.FetchAny(desc, obj) + } + }) +}