From 2d52da0790a6c95eb2b66eec4d59af263c13f6be Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Fri, 28 Nov 2025 17:22:10 +0800 Subject: [PATCH 01/17] feat:(trim) support trim thrift data --- trim/desc.go | 80 ++++ trim/fetch.go | 512 ++++++++++++++++++++++ trim/fetch_test.go | 1002 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1594 insertions(+) create mode 100644 trim/desc.go create mode 100644 trim/fetch.go create mode 100644 trim/fetch_test.go diff --git a/trim/desc.go b/trim/desc.go new file mode 100644 index 00000000..5f8910a9 --- /dev/null +++ b/trim/desc.go @@ -0,0 +1,80 @@ +/** + * 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 ( + "strings" +) + +// TypeKind is the kind of type. +type TypeKind int + +const ( + TypeKind_Struct TypeKind = iota + 1 + TypeKind_StrMap + 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 +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 + + // Name of the type + Name string + + // children for TypeKind_Struct|TypeKind_StrMap|TypeKind_List + // - For TypeKind_List, there is either one Field with Name "*" or index as ID + // - 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 + Children []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 + + // IDL-FieldID or Array-Index + ID int + + // the child of the field + Desc *Descriptor +} + +// String returns the string representation of the descriptor. +func (d *Descriptor) String() string { + sb := strings.Builder{} + var printer func(*Descriptor) + printer = func(pbtr *Descriptor) { + sb.WriteString("<" + pbtr.Name + ">") + for _, f := range pbtr.Children { + sb.WriteString("--" + f.Name) + if f.Desc == nil { + sb.WriteString("\n") + continue + } + sb.WriteString("->") + printer(f.Desc) + } + } + printer(d) + return sb.String() +} diff --git a/trim/fetch.go b/trim/fetch.go new file mode 100644 index 00000000..5d9a9f7e --- /dev/null +++ b/trim/fetch.go @@ -0,0 +1,512 @@ +/** + * 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" +) + +// FetchAny fetches the value of the field described by desc from any based on go reflect. +func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOptions) (interface{}, error) { + if any == nil || desc == nil { + return nil, nil + } + + var opt FetchOptions + if len(opts) > 0 { + opt = opts[0] + } + + v := reflect.ValueOf(any) + return fetchValue(desc, v, &opt) +} + +// 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 { + return fmt.Sprintf("not found %v at %v: %s", e.Field.Name, e.Parent.Name, e.Msg) +} + +// FetchOptions contains options for FetchAny +type FetchOptions struct { + // DisallowNotFound if true, returns ErrNotFound when a field/index/key is not found + DisallowNotFound bool +} + +// 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 + +// 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 == "_unknownFields" { + info.unknownFieldsIndex = i + continue + } + + tag := field.Tag.Get("thrift") + if tag == "" { + continue + } + + // Parse thrift tag: "FieldName,ID" - use IndexByte for better performance + idx := strings.IndexByte(tag, ',') + if idx < 0 { + continue + } + + fieldID, err := strconv.Atoi(tag[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) (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) + + case TypeKind_List: + return fetchList(desc, v, opt) + + case TypeKind_StrMap: + return fetchStrMap(desc, v, opt) + + default: + return v.Interface(), nil + } +} + +// fetchStruct handles TypeKind_Struct +func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (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 { + return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d is nil", field.ID)} + } + continue + } + + // If field has a child descriptor, recursively fetch + if field.Desc != nil { + fetched, err := fetchValue(field.Desc, fieldValue, opt) + if err != nil { + return nil, err + } + result[field.Name] = fetched + } else { + // Otherwise, use the value directly + result[field.Name] = fieldValue.Interface() + } + } 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 { + return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields", field.ID)} + } + } else if opt.DisallowNotFound { + return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d not found in struct", field.ID)} + } + } + 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 := make(map[int]*Field, len(desc.Children)) + for i := range desc.Children { + idToField[desc.Children[i].ID] = &desc.Children[i] + } + + // 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_List: + // ReadAny returns []interface{} for LIST type + listVal, ok := val.([]interface{}) + if !ok { + return val + } + + // Find wildcard or indexed descriptors + var wildcardDesc *Descriptor + indexDescMap := make(map[int]*Descriptor, len(desc.Children)) + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child.Desc + } else { + indexDescMap[child.ID] = child.Desc + } + } + + result := make([]interface{}, len(listVal)) + for i, elem := range listVal { + if childDesc, ok := indexDescMap[i]; ok { + result[i] = fetchUnknownValue(elem, childDesc) + } else if wildcardDesc != nil { + result[i] = fetchUnknownValue(elem, wildcardDesc) + } else { + result[i] = elem + } + } + 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 + var wildcardDesc *Descriptor + keyDescMap := make(map[string]*Descriptor, len(desc.Children)) + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child.Desc + } else { + keyDescMap[child.Name] = child.Desc + } + } + + result := make(map[string]interface{}, len(strMap)) + for key, elem := range strMap { + if childDesc, ok := keyDescMap[key]; ok { + result[key] = fetchUnknownValue(elem, childDesc) + } else if wildcardDesc != nil { + result[key] = fetchUnknownValue(elem, wildcardDesc) + } else { + result[key] = elem + } + } + return result + + default: + return val + } +} + +// fetchList handles TypeKind_List +func fetchList(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { + kind := v.Kind() + if kind != reflect.Slice && kind != reflect.Array { + return nil, nil + } + + childrenLen := len(desc.Children) + vLen := v.Len() + + // Fast path: only wildcard descriptor + if childrenLen == 1 && desc.Children[0].Name == "*" { + wildcardDesc := desc.Children[0].Desc + result := make([]interface{}, 0, vLen) + for i := 0; i < vLen; i++ { + elem := v.Index(i) + if elem.Kind() == reflect.Ptr && elem.IsNil() { + result = append(result, nil) + continue + } + if wildcardDesc != nil { + fetched, err := fetchValue(wildcardDesc, elem, opt) + if err != nil { + return nil, err + } + result = append(result, fetched) + } else { + result = append(result, elem.Interface()) + } + } + return result, nil + } + + // Build a map of index -> descriptor for quick lookup + indexDescMap := make(map[int]*Field, childrenLen) + var wildcardDesc *Field + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child + } else { + // Field.ID represents the slice index + indexDescMap[child.ID] = child + } + } + + // Check if specific indices are requested but not available + if opt.DisallowNotFound { + for idx := range indexDescMap { + if idx >= vLen { + return nil, ErrNotFound{Parent: desc, Field: indexDescMap[idx], Msg: fmt.Sprintf("index %d out of range (len=%d)", idx, vLen)} + } + } + } + + result := make([]interface{}, 0, vLen) + for i := 0; i < vLen; i++ { + elem := v.Index(i) + if elem.Kind() == reflect.Ptr && elem.IsNil() { + result = append(result, nil) + continue + } + + // First try to find descriptor by index (Field.ID) + var childDesc *Descriptor + if field, ok := indexDescMap[i]; ok && field.Desc != nil { + childDesc = field.Desc + } else if wildcardDesc != nil && wildcardDesc.Desc != nil { + // Fallback to wildcard descriptor + childDesc = wildcardDesc.Desc + } + + if childDesc != nil { + fetched, err := fetchValue(childDesc, elem, opt) + if err != nil { + return nil, err + } + result = append(result, fetched) + } else { + result = append(result, elem.Interface()) + } + } + return result, nil +} + +// fetchStrMap handles TypeKind_StrMap +func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { + if v.Kind() != reflect.Map || v.Type().Key().Kind() != reflect.String { + return nil, nil + } + + childrenLen := len(desc.Children) + mapLen := v.Len() + + // Fast path: only wildcard descriptor + if childrenLen == 1 && desc.Children[0].Name == "*" { + wildcardDesc := desc.Children[0].Desc + result := make(map[string]interface{}, mapLen) + iter := v.MapRange() + for iter.Next() { + keyStr := iter.Key().String() + elemValue := iter.Value() + + if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { + result[keyStr] = nil + continue + } + if wildcardDesc != nil { + fetched, err := fetchValue(wildcardDesc, elemValue, opt) + if err != nil { + return nil, err + } + result[keyStr] = fetched + } else { + result[keyStr] = elemValue.Interface() + } + } + return result, nil + } + + // Build a map of key -> descriptor for quick lookup + keyDescMap := make(map[string]*Field, childrenLen) + var wildcardDesc *Field + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child + } else { + keyDescMap[child.Name] = child + } + } + + // Check if specific keys are requested but not available in the map + if opt.DisallowNotFound && wildcardDesc == nil { + for key := range keyDescMap { + if !v.MapIndex(reflect.ValueOf(key)).IsValid() { + return nil, ErrNotFound{Parent: desc, Field: keyDescMap[key], Msg: fmt.Sprintf("key '%s' not found in map", key)} + } + } + } + + result := make(map[string]interface{}, mapLen) + iter := v.MapRange() + for iter.Next() { + keyStr := iter.Key().String() + elemValue := iter.Value() + + if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { + result[keyStr] = nil + continue + } + + // First try to find descriptor by key, then fallback to wildcard + var childDesc *Descriptor + if field, ok := keyDescMap[keyStr]; ok && field.Desc != nil { + childDesc = field.Desc + } else if wildcardDesc != nil && wildcardDesc.Desc != nil { + childDesc = wildcardDesc.Desc + } + + if childDesc != nil { + fetched, err := fetchValue(childDesc, elemValue, opt) + if err != nil { + return nil, err + } + result[keyStr] = fetched + } else { + result[keyStr] = elemValue.Interface() + } + } + return result, nil +} diff --git a/trim/fetch_test.go b/trim/fetch_test.go new file mode 100644 index 00000000..d8a24429 --- /dev/null +++ b/trim/fetch_test.go @@ -0,0 +1,1002 @@ +/** + * 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"` + FieldB []*SampleFetch `thrift:"FieldB,2"` + FieldC map[string]*SampleFetch `thrift:"FieldC,3"` + FieldD *SampleFetch `thrift:"FieldD,4"` + FieldE string `thrift:"FieldE,5"` + FieldList []int `thrift:"FieldList,6"` + FieldMap map[string]int `thrift:"FieldMap,7"` + _unknownFields unknown.Fields +} + +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) *Descriptor { + if depth <= 0 { + return nil + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + }, + }, + } + + nd := makeDesc(width, depth-1) + desc.Children[1].Desc = &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "*", + Desc: nd, + }, + }, + } + desc.Children[2].Desc = &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: nd, + }, + }, + } + desc.Children[3].Desc = nd + + 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": []interface{}{}, + "field_c": map[string]interface{}{}, + "field_list": []int{1, 2, 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"].([]interface{}), makeSampleAny(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) + 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) + } +} + +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) + + 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) + + // 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, + Name: "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, + Name: "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, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, // Not present + }, + } + + _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + 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.Name != "SampleWithUnknown" { + t.Errorf("expected parent name 'SampleWithUnknown', got '%s'", notFoundErr.Parent.Name) + } + 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, + Name: "Test", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + {Name: "key1"}, // exists + {Name: "key2"}, // not exists + }, + }, + }, + }, + } + + _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + 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.Name != "MAP" { + t.Errorf("expected parent name 'MAP', got '%s'", notFoundErr.Parent.Name) + } + if notFoundErr.Field.Name != "key2" { + t.Errorf("expected field name 'key2', got '%s'", notFoundErr.Field.Name) + } + }) + + t.Run("list index out of range", func(t *testing.T) { + obj := &struct { + Items []int `thrift:"Items,1"` + }{ + Items: []int{10, 20}, // length is 2 + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Test", + Children: []Field{ + { + Name: "items", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + {Name: "0", ID: 0}, // exists + {Name: "5", ID: 5}, // out of range + }, + }, + }, + }, + } + + _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + 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.Name != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) + } + if notFoundErr.Field.Name != "5" { + t.Errorf("expected field name '5', 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, + Name: "Outer", + Children: []Field{ + { + Name: "inner", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "Inner", + Children: []Field{ + {Name: "value", ID: 1}, // exists + {Name: "missing", ID: 99}, // not exists + }, + }, + }, + }, + } + + _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + 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.Name != "Inner" { + t.Errorf("expected parent name 'Inner', got '%s'", notFoundErr.Parent.Name) + } + 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, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, // exists + }, + } + + ret, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + 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{} +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, + Name: "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, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_struct", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "level1_struct", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "Level1Struct", + Children: []Field{ + {Name: "level1_name", ID: 1}, + { + Name: "level2_struct", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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 list in unknownFields", func(t *testing.T) { + // Create a struct with a list of structs in _unknownFields + obj := &SampleWithUnknown{ + FieldA: 42, + } + + // Encode a list into _unknownFields (id=10) + p := thrift.BinaryProtocol{} + p.WriteFieldBegin("", thrift.LIST, 10) + p.WriteListBegin(thrift.STRUCT, 2) + // First struct + p.WriteFieldBegin("", thrift.STRING, 1) + p.WriteString("item1") + p.WriteFieldBegin("", thrift.I32, 2) + p.WriteI32(100) + p.WriteFieldStop() + // Second struct + p.WriteFieldBegin("", thrift.STRING, 1) + p.WriteString("item2") + p.WriteFieldBegin("", thrift.I32, 2) + p.WriteI32(200) + p.WriteFieldStop() + p.WriteListEnd() + + obj._unknownFields = p.Buf + + // Create a descriptor with list containing struct descriptor + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "items", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "ItemList", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "Item", + Children: []Field{ + {Name: "name", ID: 1}, + {Name: "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 items list + items, ok := result["items"].([]interface{}) + if !ok { + t.Fatalf("items: expected []interface{}, got %T", result["items"]) + } + + if len(items) != 2 { + t.Fatalf("items: expected 2 elements, got %d", len(items)) + } + + // Check first item + item1, ok := items[0].(map[string]interface{}) + if !ok { + t.Fatalf("items[0]: expected map[string]interface{}, got %T", items[0]) + } + if item1["name"] != "item1" { + t.Errorf("items[0]['name']: expected 'item1', got %v", item1["name"]) + } + if item1["value"] != int32(100) { + t.Errorf("items[0]['value']: expected 100, got %v", item1["value"]) + } + + // Check second item + item2, ok := items[1].(map[string]interface{}) + if !ok { + t.Fatalf("items[1]: expected map[string]interface{}, got %T", items[1]) + } + if item2["name"] != "item2" { + t.Errorf("items[1]['name']: expected 'item2', got %v", item2["name"]) + } + if item2["value"] != int32(200) { + t.Errorf("items[1]['value']: expected 200, got %v", item2["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, + Name: "SampleWithUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "data_map", + ID: 10, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "DataMap", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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"]) + } + }) +} From 3d663761dd9a9c1600c1371230725f41ad7052ac Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Fri, 28 Nov 2025 19:07:00 +0800 Subject: [PATCH 02/17] feat: pb assign --- trim/assign.go | 698 ++++++++++++++++++++++++++++++++++++++++++++ trim/assign_test.go | 529 +++++++++++++++++++++++++++++++++ 2 files changed, 1227 insertions(+) create mode 100644 trim/assign.go create mode 100644 trim/assign_test.go diff --git a/trim/assign.go b/trim/assign.go new file mode 100644 index 00000000..0303829e --- /dev/null +++ b/trim/assign.go @@ -0,0 +1,698 @@ +/** + * 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 AssignOption func(*AssignOptions) + +// WithDisallowNotDefined sets the DisallowNotFound option +func WithDisallowNotDefined(disallow bool) AssignOption { + return func(o *AssignOptions) { + o.DisallowNotDefined = disallow + } +} + +// 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 +} + +// pbFieldCache caches the struct field info for each type +var pbFieldCache sync.Map // map[reflect.Type]*pbStructFieldInfo + +// 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, + } + + for i := 0; i < numField; i++ { + field := t.Field(i) + + // Check for XXX_unrecognized field + if field.Name == "XXX_unrecognized" { + info.unrecognizedIndex = 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) +} + +// 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, they will be encoded +// to XXX_unrecognized field using protobuf binary encoding. +func AssignAny(desc *Descriptor, src interface{}, dest interface{}, opts ...AssignOption) error { + if src == nil || dest == nil || desc == nil { + return nil + } + + var opt AssignOptions + for _, o := range opts { + o(&opt) + } + + destValue := reflect.ValueOf(dest) + if destValue.Kind() != reflect.Ptr { + return fmt.Errorf("dest must be a pointer to struct") + } + + return assignValue(desc, src, destValue.Elem(), &opt) +} + +// assignValue is the internal implementation that works with reflect.Value directly +func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) 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) + case TypeKind_List: + return assignList(desc, src, destValue, opt) + case TypeKind_StrMap: + return assignStrMap(desc, src, destValue, opt) + default: + return assignScalar(src, destValue) + } +} + +// assignStruct handles TypeKind_Struct assignment +func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { + srcMap, ok := src.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} for struct, got %T", src) + } + + if destValue.Kind() != reflect.Struct { + return fmt.Errorf("expected struct destination, got %v", destValue.Kind()) + } + + // Get cached field info for this type + fieldInfo := getPBStructFieldInfo(destValue.Type()) + + // Track which fields in srcMap are assigned to struct fields + assignedFields := make(map[string]bool, len(srcMap)) + + // Build a map from desc field name to field info for quick lookup + descFieldMap := make(map[string]*Field, len(desc.Children)) + for i := range desc.Children { + descFieldMap[desc.Children[i].Name] = &desc.Children[i] + } + + // Iterate through srcMap and assign values + for key, value := range srcMap { + if value == nil { + continue + } + + // Find the descriptor field for this key + descField, hasDescField := descFieldMap[key] + + // Find struct field by name using cached index map + fieldIdx, found := fieldInfo.nameToFieldIndex[key] + if found { + assignedFields[key] = true + fieldValue := destValue.Field(fieldIdx) + + // Make sure field is settable + if !fieldValue.CanSet() { + continue + } + + // If field has a child descriptor, recursively assign + if hasDescField && descField.Desc != nil { + if err := assignValueToField(descField.Desc, value, fieldValue, opt); err != nil { + return err + } + } else { + // Otherwise, assign the value directly + if err := assignScalar(value, fieldValue); err != nil { + return err + } + } + } else if hasDescField { + // Field exists in descriptor but not in struct - encode to XXX_unrecognized + // This will be handled below + } else if opt.DisallowNotDefined { + return ErrNotFound{Parent: desc, Field: &Field{Name: key}, Msg: fmt.Sprintf("field '%s' not found in struct", key)} + } + } + + // Encode unassigned fields (from descriptor) to XXX_unrecognized + if fieldInfo.unrecognizedIndex >= 0 { + unrecognizedValue := destValue.Field(fieldInfo.unrecognizedIndex) + if unrecognizedValue.CanSet() { + bp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(bp) + + for _, descField := range desc.Children { + // Skip if already assigned to struct field + if assignedFields[descField.Name] { + continue + } + + value, exists := srcMap[descField.Name] + if !exists || value == nil { + continue + } + + // Encode this field to XXX_unrecognized + if err := encodeUnknownField(bp, descField.ID, value); err != nil { + return fmt.Errorf("failed to encode unknown field '%s': %w", descField.Name, 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) + } + } + } + + return nil +} + +// assignValueToField assigns a value to a field, handling pointer allocation +func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Value, opt *AssignOptions) error { + // Handle pointer fields - allocate if needed + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + fieldValue.Set(reflect.New(fieldValue.Type().Elem())) + } + return assignValue(desc, src, fieldValue.Elem(), opt) + } + return assignValue(desc, src, fieldValue, opt) +} + +// assignList handles TypeKind_List assignment +func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { + srcSlice, ok := src.([]interface{}) + if !ok { + // Try to handle typed slices + srcValue := reflect.ValueOf(src) + if srcValue.Kind() != reflect.Slice && srcValue.Kind() != reflect.Array { + return fmt.Errorf("expected slice for list, got %T", src) + } + // Convert to []interface{} + srcSlice = make([]interface{}, srcValue.Len()) + for i := 0; i < srcValue.Len(); i++ { + srcSlice[i] = srcValue.Index(i).Interface() + } + } + + if destValue.Kind() != reflect.Slice { + return fmt.Errorf("expected slice destination, got %v", destValue.Kind()) + } + + // Find wildcard or indexed descriptors + var wildcardDesc *Descriptor + indexDescMap := make(map[int]*Descriptor, len(desc.Children)) + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child.Desc + } else { + indexDescMap[child.ID] = child.Desc + } + } + + // Create a new slice with the same length as src + newSlice := reflect.MakeSlice(destValue.Type(), len(srcSlice), len(srcSlice)) + + for i, elem := range srcSlice { + if elem == nil { + continue + } + + elemValue := newSlice.Index(i) + + // Find the appropriate descriptor + var childDesc *Descriptor + if d, ok := indexDescMap[i]; ok { + childDesc = d + } else { + childDesc = wildcardDesc + } + + if childDesc != nil { + if err := assignValueToField(childDesc, elem, elemValue, opt); err != nil { + return err + } + } else { + if err := assignScalar(elem, elemValue); err != nil { + return err + } + } + } + + destValue.Set(newSlice) + return nil +} + +// assignStrMap handles TypeKind_StrMap assignment +func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { + srcMap, ok := src.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} for strmap, got %T", src) + } + + if destValue.Kind() != reflect.Map { + return fmt.Errorf("expected map destination, got %v", destValue.Kind()) + } + + // Find wildcard or keyed descriptors + var wildcardDesc *Descriptor + keyDescMap := make(map[string]*Descriptor, len(desc.Children)) + for i := range desc.Children { + child := &desc.Children[i] + if child.Name == "*" { + wildcardDesc = child.Desc + } else { + keyDescMap[child.Name] = child.Desc + } + } + + // Create a new map if nil + if destValue.IsNil() { + destValue.Set(reflect.MakeMap(destValue.Type())) + } + + elemType := destValue.Type().Elem() + + for key, value := range srcMap { + if value == nil { + continue + } + + // Create a new element + elemValue := reflect.New(elemType).Elem() + + // Find the appropriate descriptor + var childDesc *Descriptor + if d, ok := keyDescMap[key]; ok { + childDesc = d + } else { + childDesc = wildcardDesc + } + + if childDesc != nil { + if err := assignValueToField(childDesc, value, elemValue, opt); err != nil { + return err + } + } else { + if err := assignScalar(value, elemValue); err != nil { + return err + } + } + + destValue.SetMapIndex(reflect.ValueOf(key), elemValue) + } + + return nil +} + +// assignScalar assigns a scalar value to destValue +func assignScalar(src interface{}, destValue reflect.Value) error { + if src == nil { + return nil + } + + srcValue := reflect.ValueOf(src) + + // 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 special cases for numeric type conversions + switch destValue.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v, ok := toInt64(src); ok { + destValue.SetInt(v) + return nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if v, ok := toUint64(src); ok { + destValue.SetUint(v) + return nil + } + case reflect.Float32, reflect.Float64: + if v, ok := toFloat64(src); ok { + destValue.SetFloat(v) + return nil + } + case reflect.String: + if v, ok := src.(string); ok { + destValue.SetString(v) + return nil + } + case reflect.Bool: + if v, ok := src.(bool); ok { + destValue.SetBool(v) + return nil + } + } + + return fmt.Errorf("cannot assign %T to %v", src, destValue.Type()) +} + +// toInt64 converts various numeric types to int64 +func toInt64(v interface{}) (int64, bool) { + switch n := v.(type) { + case int: + return int64(n), true + case int8: + return int64(n), true + case int16: + return int64(n), true + case int32: + return int64(n), true + case int64: + return n, true + case uint: + return int64(n), true + case uint8: + return int64(n), true + case uint16: + return int64(n), true + case uint32: + return int64(n), true + case uint64: + return int64(n), true + case float32: + return int64(n), true + case float64: + return int64(n), true + default: + return 0, false + } +} + +// toUint64 converts various numeric types to uint64 +func toUint64(v interface{}) (uint64, bool) { + switch n := v.(type) { + case int: + return uint64(n), true + case int8: + return uint64(n), true + case int16: + return uint64(n), true + case int32: + return uint64(n), true + case int64: + return uint64(n), true + case uint: + return uint64(n), true + case uint8: + return uint64(n), true + case uint16: + return uint64(n), true + case uint32: + return uint64(n), true + case uint64: + return n, true + case float32: + return uint64(n), true + case float64: + return uint64(n), true + default: + return 0, false + } +} + +// toFloat64 converts various numeric types to float64 +func toFloat64(v interface{}) (float64, bool) { + switch n := v.(type) { + case int: + return float64(n), true + case int8: + return float64(n), true + case int16: + return float64(n), true + case int32: + return float64(n), true + case int64: + return float64(n), true + case uint: + return float64(n), true + case uint8: + return float64(n), true + case uint16: + return float64(n), true + case uint32: + return float64(n), true + case uint64: + return float64(n), true + case float32: + return float64(n), true + case float64: + return n, true + default: + return 0, false + } +} + +// encodeUnknownField encodes a field value to protobuf binary format +func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{}) error { + if value == nil { + return nil + } + + 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 + for _, elem := range v { + if err := encodeUnknownField(bp, fieldID, elem); err != nil { + return err + } + } + + case map[string]interface{}: + // Encode as embedded message + // First encode the message content + subBp := binary.NewBinaryProtocolBuffer() + defer binary.FreeBinaryProtocol(subBp) + + for key, val := range v { + // For unknown map, we assume string keys with field ID based on some hash + // This is a simplified approach - in practice, you'd need proper field descriptors + if err := encodeUnknownField(subBp, hashFieldName(key), val); err != nil { + return err + } + } + + bp.Buf = appendTag(bp.Buf, fieldID, 2) // length-delimited wire type + bp.WriteBytes(subBp.Buf) + + default: + // Try to use reflection for other types + rv := reflect.ValueOf(value) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteInt64(rv.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bp.Buf = appendTag(bp.Buf, fieldID, 0) + bp.WriteUint64(rv.Uint()) + case reflect.Float32: + bp.Buf = appendTag(bp.Buf, fieldID, 5) + bp.WriteFloat(float32(rv.Float())) + case reflect.Float64: + bp.Buf = appendTag(bp.Buf, fieldID, 1) + bp.WriteDouble(rv.Float()) + case reflect.String: + bp.Buf = appendTag(bp.Buf, fieldID, 2) + bp.WriteString(rv.String()) + case reflect.Slice: + if rv.Type().Elem().Kind() == reflect.Uint8 { + bp.Buf = appendTag(bp.Buf, fieldID, 2) + bp.WriteBytes(rv.Bytes()) + } else { + // Encode as repeated field + for i := 0; i < rv.Len(); i++ { + if err := encodeUnknownField(bp, fieldID, rv.Index(i).Interface()); err != nil { + return err + } + } + } + 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..6c784cca --- /dev/null +++ b/trim/assign_test.go @@ -0,0 +1,529 @@ +/** + * 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 ( + "reflect" + "testing" + + "github.com/cloudwego/dynamicgo/proto/binary" +) + +type SampleAssign struct { + FieldA *int `protobuf:"varint,1,req,name=field_a"` + FieldB []*SampleAssign `protobuf:"bytes,2,opt,name=field_b"` + FieldC map[string]*SampleAssign `protobuf:"bytes,3,opt,name=field_c"` + FieldD *SampleAssign `protobuf:"bytes,4,opt,name=field_d"` + FieldE string `protobuf:"bytes,5,opt,name=field_e"` + FieldList []int `protobuf:"bytes,6,opt,name=field_list"` + FieldMap map[string]int `protobuf:"bytes,7,opt,name=field_map"` + XXX_unrecognized []byte `json:"-"` +} + +// SampleAssignSmall is a struct with fewer fields than SampleAssign +// Used to test XXX_unrecognized field encoding +type SampleAssignSmall struct { + 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, + Name: "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 == 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) + } +} + +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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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 == nil || *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 == nil || *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": []interface{}{1, 2, 3}, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + {Name: "*"}, + }, + }, + }, + }, + } + + 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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_map", + ID: 7, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "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_UnknownFields(t *testing.T) { + // Source has fields that don't exist in destination struct + src := map[string]interface{}{ + "field_a": 42, + "unknown_int": 100, // Field ID 10 + "unknown_str": "secret", // Field ID 11 + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssignSmall", + Children: []Field{ + {Name: "field_a", ID: 1}, + {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) + } + + // Check known field + if dest.FieldA == nil || *dest.FieldA != 42 { + t.Errorf("field_a: expected 42, got %v", dest.FieldA) + } + + // Check that XXX_unrecognized has data + if len(dest.XXX_unrecognized) == 0 { + t.Fatalf("XXX_unrecognized: expected non-empty bytes") + } + + // Decode the unknown fields + bp := binary.NewBinaryProtol(dest.XXX_unrecognized) + defer binary.FreeBinaryProtocol(bp) + + foundInt := false + foundStr := false + + for bp.Read < len(bp.Buf) { + fieldNum, wireType, _, err := bp.ConsumeTag() + if err != nil { + t.Fatalf("failed to consume tag: %v", err) + } + + switch fieldNum { + case 10: + // unknown_int + val, err := bp.ReadInt64() + if err != nil { + t.Fatalf("failed to read int64: %v", err) + } + if val != 100 { + t.Errorf("unknown_int: expected 100, got %v", val) + } + foundInt = true + case 11: + // unknown_str + if wireType != 2 { // length-delimited + t.Errorf("unknown_str: expected wire type 2, got %v", wireType) + } + val, err := bp.ReadString(true) + if err != nil { + t.Fatalf("failed to read string: %v", err) + } + if val != "secret" { + t.Errorf("unknown_str: expected 'secret', got %v", val) + } + foundStr = true + default: + t.Errorf("unexpected field number: %v", fieldNum) + } + } + + if !foundInt { + t.Errorf("unknown_int field not found in XXX_unrecognized") + } + if !foundStr { + t.Errorf("unknown_str field not found in XXX_unrecognized") + } +} + +func TestAssignAny_ListOfStructs(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", + }, + }, + } + + nestedDesc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + {Name: "*", Desc: nestedDesc}, + }, + }, + }, + }, + } + + 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 == nil || *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[1].FieldA == nil || *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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "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 == nil || *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) + } +} + +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, Name: "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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + // nonexistent is not in descriptor + }, + } + + dest := &SampleAssign{} + err := AssignAny(desc, src, dest, WithDisallowNotDefined(true)) + 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) + } +} + +func BenchmarkAssignAny(b *testing.B) { + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + "field_list": []interface{}{1, 2, 3, 4, 5}, + "field_map": map[string]interface{}{ + "key1": 100, + "key2": 200, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{{Name: "*"}}, + }, + }, + { + Name: "field_map", + ID: 7, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{{Name: "*"}}, + }, + }, + }, + } + + b.ResetTimer() + b.ReportAllocs() + 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, + Name: "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) + } +} From 5f19f2505e0a6bb4d9a37dc77b84c0f9387e02e3 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Fri, 28 Nov 2025 21:31:49 +0800 Subject: [PATCH 03/17] feat: not support list --- trim/assign.go | 126 ++++---------------------- trim/assign_test.go | 48 +++++----- trim/desc.go | 29 +++++- trim/fetch.go | 210 ++++++-------------------------------------- trim/fetch_test.go | 192 ++++------------------------------------ 5 files changed, 113 insertions(+), 492 deletions(-) diff --git a/trim/assign.go b/trim/assign.go index 0303829e..0dde8b9a 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -129,6 +129,8 @@ func AssignAny(desc *Descriptor, src interface{}, dest interface{}, opts ...Assi return nil } + desc.Normalize() + var opt AssignOptions for _, o := range opts { o(&opt) @@ -160,8 +162,6 @@ func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt switch desc.Kind { case TypeKind_Struct: return assignStruct(desc, src, destValue, opt) - case TypeKind_List: - return assignList(desc, src, destValue, opt) case TypeKind_StrMap: return assignStrMap(desc, src, destValue, opt) default: @@ -184,15 +184,10 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op fieldInfo := getPBStructFieldInfo(destValue.Type()) // Track which fields in srcMap are assigned to struct fields - assignedFields := make(map[string]bool, len(srcMap)) - - // Build a map from desc field name to field info for quick lookup - descFieldMap := make(map[string]*Field, len(desc.Children)) - for i := range desc.Children { - descFieldMap[desc.Children[i].Name] = &desc.Children[i] - } + 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 @@ -204,7 +199,6 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op // Find struct field by name using cached index map fieldIdx, found := fieldInfo.nameToFieldIndex[key] if found { - assignedFields[key] = true fieldValue := destValue.Field(fieldIdx) // Make sure field is settable @@ -226,32 +220,23 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op } 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 { - return ErrNotFound{Parent: desc, Field: &Field{Name: key}, Msg: fmt.Sprintf("field '%s' not found in struct", key)} + return ErrNotFound{Parent: desc, Field: Field{Name: key}, Msg: fmt.Sprintf("field '%s' not found in struct", key)} } } // Encode unassigned fields (from descriptor) to XXX_unrecognized - if fieldInfo.unrecognizedIndex >= 0 { + if len(unassignedFields) > 0 && fieldInfo.unrecognizedIndex >= 0 { unrecognizedValue := destValue.Field(fieldInfo.unrecognizedIndex) if unrecognizedValue.CanSet() { bp := binary.NewBinaryProtocolBuffer() defer binary.FreeBinaryProtocol(bp) - for _, descField := range desc.Children { - // Skip if already assigned to struct field - if assignedFields[descField.Name] { - continue - } - - value, exists := srcMap[descField.Name] - if !exists || value == nil { - continue - } - + for key, val := range unassignedFields { // Encode this field to XXX_unrecognized - if err := encodeUnknownField(bp, descField.ID, value); err != nil { - return fmt.Errorf("failed to encode unknown field '%s': %w", descField.Name, err) + if err := encodeUnknownField(bp, descFieldMap[key].ID, val); err != nil { + return fmt.Errorf("failed to encode unknown field '%s': %w", key, err) } } @@ -281,71 +266,6 @@ func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Va return assignValue(desc, src, fieldValue, opt) } -// assignList handles TypeKind_List assignment -func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { - srcSlice, ok := src.([]interface{}) - if !ok { - // Try to handle typed slices - srcValue := reflect.ValueOf(src) - if srcValue.Kind() != reflect.Slice && srcValue.Kind() != reflect.Array { - return fmt.Errorf("expected slice for list, got %T", src) - } - // Convert to []interface{} - srcSlice = make([]interface{}, srcValue.Len()) - for i := 0; i < srcValue.Len(); i++ { - srcSlice[i] = srcValue.Index(i).Interface() - } - } - - if destValue.Kind() != reflect.Slice { - return fmt.Errorf("expected slice destination, got %v", destValue.Kind()) - } - - // Find wildcard or indexed descriptors - var wildcardDesc *Descriptor - indexDescMap := make(map[int]*Descriptor, len(desc.Children)) - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child.Desc - } else { - indexDescMap[child.ID] = child.Desc - } - } - - // Create a new slice with the same length as src - newSlice := reflect.MakeSlice(destValue.Type(), len(srcSlice), len(srcSlice)) - - for i, elem := range srcSlice { - if elem == nil { - continue - } - - elemValue := newSlice.Index(i) - - // Find the appropriate descriptor - var childDesc *Descriptor - if d, ok := indexDescMap[i]; ok { - childDesc = d - } else { - childDesc = wildcardDesc - } - - if childDesc != nil { - if err := assignValueToField(childDesc, elem, elemValue, opt); err != nil { - return err - } - } else { - if err := assignScalar(elem, elemValue); err != nil { - return err - } - } - } - - destValue.Set(newSlice) - return nil -} - // assignStrMap handles TypeKind_StrMap assignment func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { srcMap, ok := src.(map[string]interface{}) @@ -359,15 +279,10 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op // Find wildcard or keyed descriptors var wildcardDesc *Descriptor - keyDescMap := make(map[string]*Descriptor, len(desc.Children)) - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child.Desc - } else { - keyDescMap[child.Name] = child.Desc - } + 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() { @@ -385,15 +300,12 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op elemValue := reflect.New(elemType).Elem() // Find the appropriate descriptor - var childDesc *Descriptor - if d, ok := keyDescMap[key]; ok { - childDesc = d - } else { - childDesc = wildcardDesc - } - - if childDesc != nil { - if err := assignValueToField(childDesc, value, elemValue, opt); err != nil { + if wildcardDesc != nil { + if err := assignValueToField(wildcardDesc, value, elemValue, opt); err != nil { + return err + } + } else if child, ok := keyDescMap[key]; ok && child.Desc != nil { + if err := assignValueToField(child.Desc, value, elemValue, opt); err != nil { return err } } else { diff --git a/trim/assign_test.go b/trim/assign_test.go index 6c784cca..1b7d7ab8 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -126,7 +126,7 @@ func TestAssignAny_NestedStruct(t *testing.T) { func TestAssignAny_List(t *testing.T) { src := map[string]interface{}{ - "field_list": []interface{}{1, 2, 3}, + "field_list": []int{1, 2, 3}, } desc := &Descriptor{ @@ -137,11 +137,8 @@ func TestAssignAny_List(t *testing.T) { Name: "field_list", ID: 6, Desc: &Descriptor{ - Kind: TypeKind_List, + Kind: TypeKind_Scalar, Name: "LIST", - Children: []Field{ - {Name: "*"}, - }, }, }, }, @@ -282,27 +279,28 @@ func TestAssignAny_UnknownFields(t *testing.T) { } func TestAssignAny_ListOfStructs(t *testing.T) { + src := map[string]interface{}{ - "field_b": []interface{}{ - map[string]interface{}{ - "field_a": 1, - "field_e": "first", + "field_b": []*SampleAssign{ + { + FieldA: intPtr(1), + FieldE: "first", }, - map[string]interface{}{ - "field_a": 2, - "field_e": "second", + { + FieldA: intPtr(2), + FieldE: "second", }, }, } - nestedDesc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - }, - } + // nestedDesc := &Descriptor{ + // Kind: TypeKind_Struct, + // Name: "SampleAssign", + // Children: []Field{ + // {Name: "field_a", ID: 1}, + // {Name: "field_e", ID: 5}, + // }, + // } desc := &Descriptor{ Kind: TypeKind_Struct, @@ -312,11 +310,8 @@ func TestAssignAny_ListOfStructs(t *testing.T) { Name: "field_b", ID: 2, Desc: &Descriptor{ - Kind: TypeKind_List, + Kind: TypeKind_Scalar, Name: "LIST", - Children: []Field{ - {Name: "*", Desc: nestedDesc}, - }, }, }, }, @@ -474,9 +469,8 @@ func BenchmarkAssignAny(b *testing.B) { Name: "field_list", ID: 6, Desc: &Descriptor{ - Kind: TypeKind_List, - Name: "LIST", - Children: []Field{{Name: "*"}}, + Kind: TypeKind_Scalar, + Name: "LIST", }, }, { diff --git a/trim/desc.go b/trim/desc.go index 5f8910a9..bcfecd2d 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -18,15 +18,17 @@ package trim import ( "strings" + "sync" ) // TypeKind is the kind of type. type TypeKind int const ( - TypeKind_Struct TypeKind = iota + 1 + TypeKind_Scalar TypeKind = iota + TypeKind_Struct TypeKind_StrMap - TypeKind_List + // TypeKind_List ) // Descriptor describes the entire a DSL-pruning scheme for a type. @@ -40,10 +42,14 @@ type Descriptor struct { Name string // children for TypeKind_Struct|TypeKind_StrMap|TypeKind_List - // - For TypeKind_List, there is either one Field with Name "*" or index as ID // - 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 Children []Field + + // for speed-up search + sync.Once + ids map[int]Field + names map[string]Field } // Field represents a mapping selection @@ -52,13 +58,28 @@ type Field struct { // Or the selection key for TypeKind_StrMap Name string - // IDL-FieldID or Array-Index + // FieldID in IDL ID int // the child of the field Desc *Descriptor } +// Normalize cache all the fields in the descriptor for speeding up search +func (d *Descriptor) Normalize() { + d.Once.Do(func() { + 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. func (d *Descriptor) String() string { sb := strings.Builder{} diff --git a/trim/fetch.go b/trim/fetch.go index 5d9a9f7e..c74f3987 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -32,6 +32,8 @@ func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOptions) (interfac return nil, nil } + desc.Normalize() + var opt FetchOptions if len(opts) > 0 { opt = opts[0] @@ -44,7 +46,7 @@ func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOptions) (interfac // 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 + Field Field // the field that is not found Msg string // additional message } @@ -128,9 +130,6 @@ func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface case TypeKind_Struct: return fetchStruct(desc, v, opt) - case TypeKind_List: - return fetchList(desc, v, opt) - case TypeKind_StrMap: return fetchStrMap(desc, v, opt) @@ -175,7 +174,7 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac fieldValue := v.Field(fieldIdx) if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { if opt.DisallowNotFound { - return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d is nil", field.ID)} + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d is nil", field.ID)} } continue } @@ -198,10 +197,10 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac // (e.g., map[FieldID]interface{} -> map[string]interface{} for nested structs) result[field.Name] = fetchUnknownValue(val, field.Desc) } else if opt.DisallowNotFound { - return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields", field.ID)} + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields", field.ID)} } } else if opt.DisallowNotFound { - return nil, ErrNotFound{Parent: desc, Field: field, Msg: fmt.Sprintf("field ID=%d not found in struct", field.ID)} + return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct", field.ID)} } } return result, nil @@ -255,10 +254,7 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } // Build a map from field ID to Field for quick lookup - idToField := make(map[int]*Field, len(desc.Children)) - for i := range desc.Children { - idToField[desc.Children[i].ID] = &desc.Children[i] - } + idToField := desc.ids // Convert map[FieldID]interface{} to map[string]interface{} result := make(map[string]interface{}, len(fieldIDMap)) @@ -271,37 +267,6 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } return result - case TypeKind_List: - // ReadAny returns []interface{} for LIST type - listVal, ok := val.([]interface{}) - if !ok { - return val - } - - // Find wildcard or indexed descriptors - var wildcardDesc *Descriptor - indexDescMap := make(map[int]*Descriptor, len(desc.Children)) - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child.Desc - } else { - indexDescMap[child.ID] = child.Desc - } - } - - result := make([]interface{}, len(listVal)) - for i, elem := range listVal { - if childDesc, ok := indexDescMap[i]; ok { - result[i] = fetchUnknownValue(elem, childDesc) - } else if wildcardDesc != nil { - result[i] = fetchUnknownValue(elem, wildcardDesc) - } else { - result[i] = elem - } - } - return result - case TypeKind_StrMap: // ReadAny returns map[string]interface{} for string-keyed MAP type strMap, ok := val.(map[string]interface{}) @@ -310,23 +275,14 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } // Find wildcard or keyed descriptors - var wildcardDesc *Descriptor - keyDescMap := make(map[string]*Descriptor, len(desc.Children)) - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child.Desc - } else { - keyDescMap[child.Name] = child.Desc - } - } + 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) - } else if wildcardDesc != nil { - result[key] = fetchUnknownValue(elem, wildcardDesc) + 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 } @@ -338,91 +294,6 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } } -// fetchList handles TypeKind_List -func fetchList(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { - kind := v.Kind() - if kind != reflect.Slice && kind != reflect.Array { - return nil, nil - } - - childrenLen := len(desc.Children) - vLen := v.Len() - - // Fast path: only wildcard descriptor - if childrenLen == 1 && desc.Children[0].Name == "*" { - wildcardDesc := desc.Children[0].Desc - result := make([]interface{}, 0, vLen) - for i := 0; i < vLen; i++ { - elem := v.Index(i) - if elem.Kind() == reflect.Ptr && elem.IsNil() { - result = append(result, nil) - continue - } - if wildcardDesc != nil { - fetched, err := fetchValue(wildcardDesc, elem, opt) - if err != nil { - return nil, err - } - result = append(result, fetched) - } else { - result = append(result, elem.Interface()) - } - } - return result, nil - } - - // Build a map of index -> descriptor for quick lookup - indexDescMap := make(map[int]*Field, childrenLen) - var wildcardDesc *Field - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child - } else { - // Field.ID represents the slice index - indexDescMap[child.ID] = child - } - } - - // Check if specific indices are requested but not available - if opt.DisallowNotFound { - for idx := range indexDescMap { - if idx >= vLen { - return nil, ErrNotFound{Parent: desc, Field: indexDescMap[idx], Msg: fmt.Sprintf("index %d out of range (len=%d)", idx, vLen)} - } - } - } - - result := make([]interface{}, 0, vLen) - for i := 0; i < vLen; i++ { - elem := v.Index(i) - if elem.Kind() == reflect.Ptr && elem.IsNil() { - result = append(result, nil) - continue - } - - // First try to find descriptor by index (Field.ID) - var childDesc *Descriptor - if field, ok := indexDescMap[i]; ok && field.Desc != nil { - childDesc = field.Desc - } else if wildcardDesc != nil && wildcardDesc.Desc != nil { - // Fallback to wildcard descriptor - childDesc = wildcardDesc.Desc - } - - if childDesc != nil { - fetched, err := fetchValue(childDesc, elem, opt) - if err != nil { - return nil, err - } - result = append(result, fetched) - } else { - result = append(result, elem.Interface()) - } - } - return result, nil -} - // fetchStrMap handles TypeKind_StrMap func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { if v.Kind() != reflect.Map || v.Type().Key().Kind() != reflect.String { @@ -430,12 +301,11 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac } childrenLen := len(desc.Children) - mapLen := v.Len() // Fast path: only wildcard descriptor if childrenLen == 1 && desc.Children[0].Name == "*" { wildcardDesc := desc.Children[0].Desc - result := make(map[string]interface{}, mapLen) + result := make(map[string]interface{}, v.Len()) iter := v.MapRange() for iter.Next() { keyStr := iter.Key().String() @@ -458,55 +328,33 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac return result, nil } - // Build a map of key -> descriptor for quick lookup - keyDescMap := make(map[string]*Field, childrenLen) - var wildcardDesc *Field - for i := range desc.Children { - child := &desc.Children[i] - if child.Name == "*" { - wildcardDesc = child - } else { - keyDescMap[child.Name] = child - } - } - - // Check if specific keys are requested but not available in the map - if opt.DisallowNotFound && wildcardDesc == nil { - for key := range keyDescMap { - if !v.MapIndex(reflect.ValueOf(key)).IsValid() { + // 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 { return nil, ErrNotFound{Parent: desc, Field: keyDescMap[key], Msg: fmt.Sprintf("key '%s' not found in map", key)} + } else { + continue } } - } - - result := make(map[string]interface{}, mapLen) - iter := v.MapRange() - for iter.Next() { - keyStr := iter.Key().String() - elemValue := iter.Value() - - if elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { - result[keyStr] = nil + if val.Kind() == reflect.Ptr && val.IsNil() { + result[key] = nil continue } - - // First try to find descriptor by key, then fallback to wildcard - var childDesc *Descriptor - if field, ok := keyDescMap[keyStr]; ok && field.Desc != nil { - childDesc = field.Desc - } else if wildcardDesc != nil && wildcardDesc.Desc != nil { - childDesc = wildcardDesc.Desc - } - - if childDesc != nil { - fetched, err := fetchValue(childDesc, elemValue, opt) + if child.Desc != nil { + fetched, err := fetchValue(child.Desc, val, opt) if err != nil { return nil, err } - result[keyStr] = fetched + result[key] = fetched } else { - result[keyStr] = elemValue.Interface() + result[key] = val.Interface() } } + return result, nil } diff --git a/trim/fetch_test.go b/trim/fetch_test.go index d8a24429..7c28f1d5 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -25,25 +25,25 @@ import ( "github.com/cloudwego/thriftgo/generator/golang/extension/unknown" ) -type SampleFetch struct { +type sampleFetch struct { FieldA int `thrift:"FieldA,1"` - FieldB []*SampleFetch `thrift:"FieldB,2"` - FieldC map[string]*SampleFetch `thrift:"FieldC,3"` - FieldD *SampleFetch `thrift:"FieldD,4"` + FieldB []*sampleFetch `thrift:"FieldB,2"` + FieldC map[string]*sampleFetch `thrift:"FieldC,3"` + FieldD *sampleFetch `thrift:"FieldD,4"` FieldE string `thrift:"FieldE,5"` FieldList []int `thrift:"FieldList,6"` FieldMap map[string]int `thrift:"FieldMap,7"` _unknownFields unknown.Fields } -func makeSampleFetch(width int, depth int) *SampleFetch { +func makeSampleFetch(width int, depth int) *sampleFetch { if depth <= 0 { return nil } - ret := &SampleFetch{ + ret := &sampleFetch{ FieldA: 1, FieldE: "1", - FieldC: make(map[string]*SampleFetch), + FieldC: make(map[string]*sampleFetch), FieldList: []int{1, 2, 3}, FieldMap: map[string]int{ "1": 1, @@ -98,16 +98,6 @@ func makeDesc(width int, depth int) *Descriptor { } nd := makeDesc(width, depth-1) - desc.Children[1].Desc = &Descriptor{ - Kind: TypeKind_List, - Name: "LIST", - Children: []Field{ - { - Name: "*", - Desc: nd, - }, - }, - } desc.Children[2].Desc = &Descriptor{ Kind: TypeKind_StrMap, Name: "MAP", @@ -131,7 +121,7 @@ func makeSampleAny(width int, depth int) interface{} { } ret := map[string]interface{}{ "field_a": int(1), - "field_b": []interface{}{}, + "field_b": []*sampleFetch{}, "field_c": map[string]interface{}{}, "field_list": []int{1, 2, 3}, "field_map": map[string]int{ @@ -141,7 +131,7 @@ func makeSampleAny(width int, depth int) interface{} { }, } for i := 0; i < width; i++ { - ret["field_b"] = append(ret["field_b"].([]interface{}), makeSampleAny(width, depth-1)) + 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) @@ -211,9 +201,9 @@ func BenchmarkFetchAny_CacheHit(b *testing.B) { } } -// SampleWithUnknown is a struct that has a subset of fields compared to SampleFetch +// 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 { +type sampleWithUnknown struct { FieldA int `thrift:"FieldA,1"` // FieldB, FieldC, FieldD, FieldE are not declared here, they will be in _unknownFields _unknownFields unknown.Fields @@ -221,7 +211,7 @@ type SampleWithUnknown struct { func TestFetchAnyWithUnknownFields(t *testing.T) { // Create a struct with some fields in _unknownFields - obj := &SampleWithUnknown{ + obj := &sampleWithUnknown{ FieldA: 42, } @@ -323,7 +313,7 @@ func TestFetchAnyWithUnknownFields(t *testing.T) { func TestFetchAnyWithEmptyUnknownFields(t *testing.T) { // Create a struct with empty _unknownFields - obj := &SampleWithUnknown{ + obj := &sampleWithUnknown{ FieldA: 42, } @@ -359,7 +349,7 @@ func TestFetchAnyWithEmptyUnknownFields(t *testing.T) { func TestFetchAnyWithDisallowNotFound(t *testing.T) { t.Run("struct field not found", func(t *testing.T) { - obj := &SampleWithUnknown{FieldA: 42} + obj := &sampleWithUnknown{FieldA: 42} desc := &Descriptor{ Kind: TypeKind_Struct, @@ -430,49 +420,6 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { } }) - t.Run("list index out of range", func(t *testing.T) { - obj := &struct { - Items []int `thrift:"Items,1"` - }{ - Items: []int{10, 20}, // length is 2 - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Test", - Children: []Field{ - { - Name: "items", - ID: 1, - Desc: &Descriptor{ - Kind: TypeKind_List, - Name: "LIST", - Children: []Field{ - {Name: "0", ID: 0}, // exists - {Name: "5", ID: 5}, // out of range - }, - }, - }, - }, - } - - _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) - 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.Name != "LIST" { - t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) - } - if notFoundErr.Field.Name != "5" { - t.Errorf("expected field name '5', got '%s'", notFoundErr.Field.Name) - } - }) - t.Run("nested struct field not found", func(t *testing.T) { obj := &struct { Inner *struct { @@ -521,7 +468,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }) t.Run("no error when all fields found", func(t *testing.T) { - obj := &SampleWithUnknown{FieldA: 42} + obj := &sampleWithUnknown{FieldA: 42} desc := &Descriptor{ Kind: TypeKind_Struct, @@ -553,7 +500,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { 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{ + obj := &sampleWithUnknown{ FieldA: 42, } @@ -617,7 +564,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { t.Run("nested struct with descriptor (with further fetch)", func(t *testing.T) { // Create a struct with a nested struct in _unknownFields - obj := &SampleWithUnknown{ + obj := &sampleWithUnknown{ FieldA: 42, } @@ -704,7 +651,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { t.Run("deeply nested struct with descriptor", func(t *testing.T) { // Create a struct with a deeply nested struct in _unknownFields - obj := &SampleWithUnknown{ + obj := &sampleWithUnknown{ FieldA: 42, } @@ -796,110 +743,9 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { } }) - t.Run("struct in list in unknownFields", func(t *testing.T) { - // Create a struct with a list of structs in _unknownFields - obj := &SampleWithUnknown{ - FieldA: 42, - } - - // Encode a list into _unknownFields (id=10) - p := thrift.BinaryProtocol{} - p.WriteFieldBegin("", thrift.LIST, 10) - p.WriteListBegin(thrift.STRUCT, 2) - // First struct - p.WriteFieldBegin("", thrift.STRING, 1) - p.WriteString("item1") - p.WriteFieldBegin("", thrift.I32, 2) - p.WriteI32(100) - p.WriteFieldStop() - // Second struct - p.WriteFieldBegin("", thrift.STRING, 1) - p.WriteString("item2") - p.WriteFieldBegin("", thrift.I32, 2) - p.WriteI32(200) - p.WriteFieldStop() - p.WriteListEnd() - - obj._unknownFields = p.Buf - - // Create a descriptor with list containing struct descriptor - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleWithUnknown", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "items", - ID: 10, - Desc: &Descriptor{ - Kind: TypeKind_List, - Name: "ItemList", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "Item", - Children: []Field{ - {Name: "name", ID: 1}, - {Name: "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 items list - items, ok := result["items"].([]interface{}) - if !ok { - t.Fatalf("items: expected []interface{}, got %T", result["items"]) - } - - if len(items) != 2 { - t.Fatalf("items: expected 2 elements, got %d", len(items)) - } - - // Check first item - item1, ok := items[0].(map[string]interface{}) - if !ok { - t.Fatalf("items[0]: expected map[string]interface{}, got %T", items[0]) - } - if item1["name"] != "item1" { - t.Errorf("items[0]['name']: expected 'item1', got %v", item1["name"]) - } - if item1["value"] != int32(100) { - t.Errorf("items[0]['value']: expected 100, got %v", item1["value"]) - } - - // Check second item - item2, ok := items[1].(map[string]interface{}) - if !ok { - t.Fatalf("items[1]: expected map[string]interface{}, got %T", items[1]) - } - if item2["name"] != "item2" { - t.Errorf("items[1]['name']: expected 'item2', got %v", item2["name"]) - } - if item2["value"] != int32(200) { - t.Errorf("items[1]['value']: expected 200, got %v", item2["value"]) - } - }) - t.Run("struct in map in unknownFields", func(t *testing.T) { // Create a struct with a map in _unknownFields - obj := &SampleWithUnknown{ + obj := &sampleWithUnknown{ FieldA: 42, } From 43cc68b5c1bf86feed719ea67e9bf78c13e4e28e Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Sun, 30 Nov 2025 12:10:40 +0800 Subject: [PATCH 04/17] feat: support struct2struct --- trim/all_test.go | 17 ++ trim/assign.go | 292 +++++++++++++++++++++++ trim/assign_test.go | 563 +++++++++++++++++++++++++++++++++++++++++++- trim/desc.go | 4 +- trim/fetch_test.go | 14 +- 5 files changed, 875 insertions(+), 15 deletions(-) create mode 100644 trim/all_test.go diff --git a/trim/all_test.go b/trim/all_test.go new file mode 100644 index 00000000..7ff17cab --- /dev/null +++ b/trim/all_test.go @@ -0,0 +1,17 @@ +package trim + +import "testing" + +func TestFetchAndAssign(t *testing.T) { + src := makeSampleFetch(3, 3) + desc := makeDesc(3, 3) + m, err := FetchAny(desc, src) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) + } + dest := makeSampleAssign(3, 3) + err = AssignAny(desc, m, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } +} diff --git a/trim/assign.go b/trim/assign.go index 0dde8b9a..9b35d418 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -320,6 +320,51 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op 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 +} + +// 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) + 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) +} + // assignScalar assigns a scalar value to destValue func assignScalar(src interface{}, destValue reflect.Value) error { if src == nil { @@ -336,6 +381,14 @@ func assignScalar(src interface{}, destValue reflect.Value) error { destValue = destValue.Elem() } + // Dereference pointer source + for srcValue.Kind() == reflect.Ptr { + if srcValue.IsNil() { + return nil + } + srcValue = srcValue.Elem() + } + // Try direct assignment first if srcValue.Type().AssignableTo(destValue.Type()) { destValue.Set(srcValue) @@ -348,6 +401,21 @@ func assignScalar(src interface{}, destValue reflect.Value) error { return nil } + // 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 special cases for numeric type conversions switch destValue.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -380,6 +448,230 @@ func assignScalar(src interface{}, destValue reflect.Value) error { return fmt.Errorf("cannot assign %T to %v", src, destValue.Type()) } +// 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 := assignScalarValue(srcField, destField); err != nil { + return err + } + } + + return nil +} + +// assignScalarValue assigns a reflect.Value to another reflect.Value +func assignScalarValue(srcValue, destValue reflect.Value) error { + // Handle pointer destination + if destValue.Kind() == reflect.Ptr { + if destValue.IsNil() { + destValue.Set(reflect.New(destValue.Type().Elem())) + } + destValue = destValue.Elem() + } + + // Dereference pointer source + for srcValue.Kind() == reflect.Ptr { + if srcValue.IsNil() { + return nil + } + srcValue = srcValue.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 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 numeric conversions + switch destValue.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if srcValue.CanInt() { + destValue.SetInt(srcValue.Int()) + return nil + } + if srcValue.CanUint() { + destValue.SetInt(int64(srcValue.Uint())) + return nil + } + if srcValue.CanFloat() { + destValue.SetInt(int64(srcValue.Float())) + return nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if srcValue.CanUint() { + destValue.SetUint(srcValue.Uint()) + return nil + } + if srcValue.CanInt() { + destValue.SetUint(uint64(srcValue.Int())) + return nil + } + if srcValue.CanFloat() { + destValue.SetUint(uint64(srcValue.Float())) + return nil + } + case reflect.Float32, reflect.Float64: + if srcValue.CanFloat() { + destValue.SetFloat(srcValue.Float()) + return nil + } + if srcValue.CanInt() { + destValue.SetFloat(float64(srcValue.Int())) + return nil + } + if srcValue.CanUint() { + destValue.SetFloat(float64(srcValue.Uint())) + return nil + } + case reflect.String: + if srcValue.Kind() == reflect.String { + destValue.SetString(srcValue.String()) + return nil + } + case reflect.Bool: + if srcValue.Kind() == reflect.Bool { + destValue.SetBool(srcValue.Bool()) + return nil + } + } + + 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 { + newElem := reflect.New(destElemType.Elem()) + if err := assignScalarValue(srcElem, newElem.Elem()); err != nil { + return err + } + destElem.Set(newElem) + } else { + if err := assignScalarValue(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 { + newElem := reflect.New(destElemType.Elem()) + if err := assignScalarValue(srcVal, newElem.Elem()); err != nil { + return err + } + destVal.Set(newElem) + } else { + if err := assignScalarValue(srcVal, destVal); err != nil { + return err + } + } + + newMap.SetMapIndex(destKey, destVal) + } + + destValue.Set(newMap) + return nil +} + // toInt64 converts various numeric types to int64 func toInt64(v interface{}) (int64, bool) { switch n := v.(type) { diff --git a/trim/assign_test.go b/trim/assign_test.go index 1b7d7ab8..9ca76050 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -17,6 +17,7 @@ package trim import ( + "fmt" "reflect" "testing" @@ -24,16 +25,39 @@ import ( ) type SampleAssign struct { - FieldA *int `protobuf:"varint,1,req,name=field_a"` - FieldB []*SampleAssign `protobuf:"bytes,2,opt,name=field_b"` - FieldC map[string]*SampleAssign `protobuf:"bytes,3,opt,name=field_c"` - FieldD *SampleAssign `protobuf:"bytes,4,opt,name=field_d"` - FieldE string `protobuf:"bytes,5,opt,name=field_e"` - FieldList []int `protobuf:"bytes,6,opt,name=field_list"` - FieldMap map[string]int `protobuf:"bytes,7,opt,name=field_map"` + FieldA *int `protobuf:"varint,1,req,name=field_a" json:"field_a"` + FieldB []*SampleAssign `protobuf:"bytes,2,opt,name=field_b" json:"field_b"` + FieldC map[string]*SampleAssign `protobuf:"bytes,3,opt,name=field_c" json:"field_c"` + FieldD *SampleAssign `protobuf:"bytes,4,opt,name=field_d" json:"field_d"` + FieldE string `protobuf:"bytes,5,opt,name=field_e" json:"field_e"` + FieldList []int `protobuf:"bytes,6,opt,name=field_list" json:"field_list"` + FieldMap map[string]int `protobuf:"bytes,7,opt,name=field_map" json:"field_map"` XXX_unrecognized []byte `json:"-"` } +func makeSampleAssign(width, depth int) *SampleAssign { + if width <= 0 || depth <= 0 { + return nil + } + ret := &SampleAssign{ + FieldA: intPtr(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 { @@ -521,3 +545,528 @@ func BenchmarkAssignAny_WithUnknownFields(b *testing.B) { _ = AssignAny(desc, src, dest) } } + +// SourceStruct is used for struct-to-struct assignment tests via json tag matching +type SourceStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Score float64 `json:"score"` + Active bool `json:"active"` +} + +// DestStruct has same json tags but different Go field names +type DestStruct struct { + UserName string `json:"name"` + UserAge int `json:"age"` + UserScore float64 `json:"score"` + IsActive bool `json:"active"` +} + +// NestedSourceStruct contains nested struct +type NestedSourceStruct struct { + ID int `json:"id"` + Data *SourceStruct `json:"data"` +} + +// NestedDestStruct contains nested struct with different types +type NestedDestStruct struct { + ID int `json:"id"` + Data *DestStruct `json:"data"` +} + +// ListSourceStruct contains a list of structs +type ListSourceStruct struct { + Items []*SourceStruct `json:"items"` +} + +// ListDestStruct contains a list of different struct types +type ListDestStruct struct { + Items []*DestStruct `json:"items"` +} + +// MapSourceStruct contains a map of structs +type MapSourceStruct struct { + Data map[string]*SourceStruct `json:"data"` +} + +// MapDestStruct contains a map of different struct types +type MapDestStruct struct { + Data map[string]*DestStruct `json:"data"` +} + +func TestAssignScalar_StructToStruct(t *testing.T) { + t.Run("basic struct to struct via json tag", func(t *testing.T) { + src := &SourceStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, + Active: true, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "data", ID: 1}, + }, + } + + type Wrapper struct { + Data *DestStruct `protobuf:"bytes,1,opt,name=data" json:"data"` + } + + srcMap := map[string]interface{}{ + "data": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.Data == nil { + t.Fatalf("dest.Data should not be nil") + } + if dest.Data.UserName != "Alice" { + t.Errorf("UserName: expected 'Alice', got '%s'", dest.Data.UserName) + } + if dest.Data.UserAge != 30 { + t.Errorf("UserAge: expected 30, got %d", dest.Data.UserAge) + } + if dest.Data.UserScore != 95.5 { + t.Errorf("UserScore: expected 95.5, got %f", dest.Data.UserScore) + } + if dest.Data.IsActive != true { + t.Errorf("IsActive: expected true, got %v", dest.Data.IsActive) + } + }) + + t.Run("nested struct to struct via json tag", func(t *testing.T) { + src := &NestedSourceStruct{ + ID: 100, + Data: &SourceStruct{ + Name: "Bob", + Age: 25, + Score: 88.0, + Active: false, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "nested", ID: 1}, + }, + } + + type Wrapper struct { + Nested *NestedDestStruct `protobuf:"bytes,1,opt,name=nested" json:"nested"` + } + + srcMap := map[string]interface{}{ + "nested": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.Nested == nil { + t.Fatalf("dest.Nested should not be nil") + } + if dest.Nested.ID != 100 { + t.Errorf("ID: expected 100, got %d", dest.Nested.ID) + } + if dest.Nested.Data == nil { + t.Fatalf("dest.Nested.Data should not be nil") + } + if dest.Nested.Data.UserName != "Bob" { + t.Errorf("UserName: expected 'Bob', got '%s'", dest.Nested.Data.UserName) + } + if dest.Nested.Data.UserAge != 25 { + t.Errorf("UserAge: expected 25, got %d", dest.Nested.Data.UserAge) + } + }) +} + +func TestAssignScalar_SliceToSlice(t *testing.T) { + t.Run("slice of structs via json tag", func(t *testing.T) { + src := &ListSourceStruct{ + Items: []*SourceStruct{ + {Name: "Alice", Age: 30, Score: 95.5, Active: true}, + {Name: "Bob", Age: 25, Score: 88.0, Active: false}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "list", ID: 1}, + }, + } + + type Wrapper struct { + List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` + } + + srcMap := map[string]interface{}{ + "list": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.List == nil { + t.Fatalf("dest.List should not be nil") + } + if len(dest.List.Items) != 2 { + t.Fatalf("Items length: expected 2, got %d", len(dest.List.Items)) + } + if dest.List.Items[0].UserName != "Alice" { + t.Errorf("Items[0].UserName: expected 'Alice', got '%s'", dest.List.Items[0].UserName) + } + if dest.List.Items[1].UserName != "Bob" { + t.Errorf("Items[1].UserName: expected 'Bob', got '%s'", dest.List.Items[1].UserName) + } + }) + + t.Run("slice of primitives", func(t *testing.T) { + src := []int32{1, 2, 3, 4, 5} + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "nums", ID: 1}, + }, + } + + type Wrapper struct { + Nums []int64 `protobuf:"bytes,1,opt,name=nums" json:"nums"` + } + + srcMap := map[string]interface{}{ + "nums": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := []int64{1, 2, 3, 4, 5} + if !reflect.DeepEqual(dest.Nums, expected) { + t.Errorf("Nums: expected %v, got %v", expected, dest.Nums) + } + }) +} + +func TestAssignScalar_MapToMap(t *testing.T) { + t.Run("map of structs via json tag", func(t *testing.T) { + src := &MapSourceStruct{ + Data: map[string]*SourceStruct{ + "user1": {Name: "Alice", Age: 30, Score: 95.5, Active: true}, + "user2": {Name: "Bob", Age: 25, Score: 88.0, Active: false}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "map_data", ID: 1}, + }, + } + + type Wrapper struct { + MapData *MapDestStruct `protobuf:"bytes,1,opt,name=map_data" json:"map_data"` + } + + srcMap := map[string]interface{}{ + "map_data": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.MapData == nil { + t.Fatalf("dest.MapData should not be nil") + } + if len(dest.MapData.Data) != 2 { + t.Fatalf("Data length: expected 2, got %d", len(dest.MapData.Data)) + } + if dest.MapData.Data["user1"].UserName != "Alice" { + t.Errorf("Data['user1'].UserName: expected 'Alice', got '%s'", dest.MapData.Data["user1"].UserName) + } + if dest.MapData.Data["user2"].UserName != "Bob" { + t.Errorf("Data['user2'].UserName: expected 'Bob', got '%s'", dest.MapData.Data["user2"].UserName) + } + }) + + t.Run("map of primitives with type conversion", func(t *testing.T) { + src := map[string]int32{"a": 1, "b": 2, "c": 3} + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "data", ID: 1}, + }, + } + + type Wrapper struct { + Data map[string]int64 `protobuf:"bytes,1,opt,name=data" json:"data"` + } + + srcMap := map[string]interface{}{ + "data": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + expected := map[string]int64{"a": 1, "b": 2, "c": 3} + if !reflect.DeepEqual(dest.Data, expected) { + t.Errorf("Data: expected %v, got %v", expected, dest.Data) + } + }) +} + +func TestAssignScalar_ComplexNested(t *testing.T) { + // Test complex nested structure similar to TestFetchAndAssign scenario + t.Run("complex nested with lists and maps", func(t *testing.T) { + type InnerSource struct { + Value int `json:"value"` + Label string `json:"label"` + } + + type OuterSource struct { + ID int `json:"id"` + Children []*InnerSource `json:"children"` + Mapping map[string]*InnerSource `json:"mapping"` + } + + type InnerDest struct { + Value int `json:"value"` + Label string `json:"label"` + } + + type OuterDest struct { + ID int `json:"id"` + Children []*InnerDest `json:"children"` + Mapping map[string]*InnerDest `json:"mapping"` + } + + src := &OuterSource{ + ID: 1, + Children: []*InnerSource{ + {Value: 10, Label: "first"}, + {Value: 20, Label: "second"}, + }, + Mapping: map[string]*InnerSource{ + "key1": {Value: 100, Label: "mapped1"}, + "key2": {Value: 200, Label: "mapped2"}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "data", ID: 1}, + }, + } + + type Wrapper struct { + Data *OuterDest `protobuf:"bytes,1,opt,name=data" json:"data"` + } + + srcMap := map[string]interface{}{ + "data": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.Data == nil { + t.Fatalf("dest.Data should not be nil") + } + if dest.Data.ID != 1 { + t.Errorf("ID: expected 1, got %d", dest.Data.ID) + } + if len(dest.Data.Children) != 2 { + t.Fatalf("Children length: expected 2, got %d", len(dest.Data.Children)) + } + if dest.Data.Children[0].Value != 10 { + t.Errorf("Children[0].Value: expected 10, got %d", dest.Data.Children[0].Value) + } + if dest.Data.Children[0].Label != "first" { + t.Errorf("Children[0].Label: expected 'first', got '%s'", dest.Data.Children[0].Label) + } + if len(dest.Data.Mapping) != 2 { + t.Fatalf("Mapping length: expected 2, got %d", len(dest.Data.Mapping)) + } + if dest.Data.Mapping["key1"].Value != 100 { + t.Errorf("Mapping['key1'].Value: expected 100, got %d", dest.Data.Mapping["key1"].Value) + } + }) +} + +func TestAssignScalar_NilHandling(t *testing.T) { + t.Run("nil pointer in source struct", func(t *testing.T) { + src := &NestedSourceStruct{ + ID: 100, + Data: nil, // nil pointer + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "nested", ID: 1}, + }, + } + + type Wrapper struct { + Nested *NestedDestStruct `protobuf:"bytes,1,opt,name=nested" json:"nested"` + } + + srcMap := map[string]interface{}{ + "nested": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.Nested == nil { + t.Fatalf("dest.Nested should not be nil") + } + if dest.Nested.ID != 100 { + t.Errorf("ID: expected 100, got %d", dest.Nested.ID) + } + if dest.Nested.Data != nil { + t.Errorf("Data: expected nil, got %v", dest.Nested.Data) + } + }) + + t.Run("nil slice in source struct", func(t *testing.T) { + src := &ListSourceStruct{ + Items: nil, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "list", ID: 1}, + }, + } + + type Wrapper struct { + List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` + } + + srcMap := map[string]interface{}{ + "list": src, + } + + dest := &Wrapper{} + err := AssignAny(desc, srcMap, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } + + if dest.List == nil { + t.Fatalf("dest.List should not be nil") + } + if dest.List.Items != nil { + t.Errorf("Items: expected nil, got %v", dest.List.Items) + } + }) +} + +func BenchmarkAssignScalar_StructToStruct(b *testing.B) { + src := &SourceStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, + Active: true, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "data", ID: 1}, + }, + } + + type Wrapper struct { + Data *DestStruct `protobuf:"bytes,1,opt,name=data" json:"data"` + } + + srcMap := map[string]interface{}{ + "data": src, + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + dest := &Wrapper{} + _ = AssignAny(desc, srcMap, dest) + } +} + +func BenchmarkAssignScalar_SliceOfStructs(b *testing.B) { + src := &ListSourceStruct{ + Items: []*SourceStruct{ + {Name: "Alice", Age: 30, Score: 95.5, Active: true}, + {Name: "Bob", Age: 25, Score: 88.0, Active: false}, + {Name: "Charlie", Age: 35, Score: 92.0, Active: true}, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Wrapper", + Children: []Field{ + {Name: "list", ID: 1}, + }, + } + + type Wrapper struct { + List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` + } + + srcMap := map[string]interface{}{ + "list": src, + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + dest := &Wrapper{} + _ = AssignAny(desc, srcMap, dest) + } +} diff --git a/trim/desc.go b/trim/desc.go index bcfecd2d..8c621911 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -25,10 +25,12 @@ import ( type TypeKind int const ( + // TypeKind_Scalar indicates Descriptor is a leaf node, its underlying type can be anything (event go struct/map/list) TypeKind_Scalar TypeKind = iota + // TypeKind_Struct indicates Descriptor.Field is struct field TypeKind_Struct + // TypeKind_StrMap indicates Descriptor.Field is map key TypeKind_StrMap - // TypeKind_List ) // Descriptor describes the entire a DSL-pruning scheme for a type. diff --git a/trim/fetch_test.go b/trim/fetch_test.go index 7c28f1d5..8d2e304a 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -26,13 +26,13 @@ import ( ) type sampleFetch struct { - FieldA int `thrift:"FieldA,1"` - FieldB []*sampleFetch `thrift:"FieldB,2"` - FieldC map[string]*sampleFetch `thrift:"FieldC,3"` - FieldD *sampleFetch `thrift:"FieldD,4"` - FieldE string `thrift:"FieldE,5"` - FieldList []int `thrift:"FieldList,6"` - FieldMap map[string]int `thrift:"FieldMap,7"` + FieldA int `thrift:"FieldA,1" json:"field_a"` + FieldB []*sampleFetch `thrift:"FieldB,2" json:"field_b"` + FieldC map[string]*sampleFetch `thrift:"FieldC,3" json:"field_c"` + FieldD *sampleFetch `thrift:"FieldD,4" json:"field_d"` + FieldE string `thrift:"FieldE,5" json:"field_e"` + FieldList []int `thrift:"FieldList,6" json:"field_list"` + FieldMap map[string]int `thrift:"FieldMap,7" json:"field_map"` _unknownFields unknown.Fields } From 5ba6107932b4907282aecbe69830ae34ba06c925 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Sun, 30 Nov 2025 20:47:54 +0800 Subject: [PATCH 05/17] test all --- trim/all_test.go | 1036 ++++++++++++++++++++++++++++++++++++++++++- trim/assign.go | 84 +++- trim/assign_test.go | 68 +-- trim/fetch_test.go | 33 +- 4 files changed, 1169 insertions(+), 52 deletions(-) diff --git a/trim/all_test.go b/trim/all_test.go index 7ff17cab..58be8be3 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -1,17 +1,1049 @@ package trim -import "testing" +import ( + "encoding/json" + "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 TestFetchAndAssign(t *testing.T) { src := makeSampleFetch(3, 3) - desc := makeDesc(3, 3) + srcjson, err := json.Marshal(src) + if err != nil { + t.Fatalf("json.Marshal failed: %v", err) + } + desc := makeDesc(3, 3, true) 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) +} + +// ===================== 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, + Name: "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, + Name: "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, + Name: "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, + Name: "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, + Name: "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, + Name: "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, + Name: "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, + Name: "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, + Name: "Outer", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "Outer", + Children: []Field{ + {Name: "id", ID: 10}, + { + Name: "inner", + ID: 20, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "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, + Name: "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, + Name: "Outer", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "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, + Name: "Container", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "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, + Name: "Container", + Children: []Field{ + { + Name: "data", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "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, + Name: "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) } diff --git a/trim/assign.go b/trim/assign.go index 9b35d418..756876d6 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -292,13 +292,15 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op elemType := destValue.Type().Elem() for key, value := range srcMap { + // Create a new element + elemValue := reflect.New(elemType).Elem() + if value == nil { + // Set nil value in map (zero value for the element type, e.g., nil pointer) + destValue.SetMapIndex(reflect.ValueOf(key), elemValue) continue } - // Create a new element - elemValue := reflect.New(elemType).Elem() - // Find the appropriate descriptor if wildcardDesc != nil { if err := assignValueToField(wildcardDesc, value, elemValue, opt); err != nil { @@ -416,6 +418,14 @@ func assignScalar(src interface{}, destValue reflect.Value) error { return assignMapToMap(srcValue, destValue) } + // Handle map[string]interface{} to struct mapping + if srcValue.Kind() == reflect.Map && destValue.Kind() == reflect.Struct { + srcMapIface, ok := src.(map[string]interface{}) + if ok { + return assignMapToStruct(srcMapIface, destValue) + } + } + // Handle special cases for numeric type conversions switch destValue.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -488,6 +498,42 @@ func assignStructToStruct(srcValue, destValue reflect.Value) error { 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 := assignScalar(srcVal, destField); err != nil { + return err + } + } + + return nil +} + // assignScalarValue assigns a reflect.Value to another reflect.Value func assignScalarValue(srcValue, destValue reflect.Value) error { // Handle pointer destination @@ -506,6 +552,16 @@ func assignScalarValue(srcValue, destValue reflect.Value) error { srcValue = srcValue.Elem() } + // 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 assignScalar(srcValue.Interface(), destValue) + } + // Try direct assignment first if srcValue.Type().AssignableTo(destValue.Type()) { destValue.Set(srcValue) @@ -607,6 +663,16 @@ func assignSliceToSlice(srcValue, destValue reflect.Value) error { // 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 := assignScalarValue(srcElem, newElem.Elem()); err != nil { return err @@ -654,6 +720,18 @@ func assignMapToMap(srcValue, destValue reflect.Value) error { // 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 := assignScalarValue(srcVal, newElem.Elem()); err != nil { return err diff --git a/trim/assign_test.go b/trim/assign_test.go index 9ca76050..0957908a 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -24,25 +24,25 @@ import ( "github.com/cloudwego/dynamicgo/proto/binary" ) -type SampleAssign struct { - FieldA *int `protobuf:"varint,1,req,name=field_a" json:"field_a"` - FieldB []*SampleAssign `protobuf:"bytes,2,opt,name=field_b" json:"field_b"` - FieldC map[string]*SampleAssign `protobuf:"bytes,3,opt,name=field_c" json:"field_c"` - FieldD *SampleAssign `protobuf:"bytes,4,opt,name=field_d" json:"field_d"` - FieldE string `protobuf:"bytes,5,opt,name=field_e" json:"field_e"` - FieldList []int `protobuf:"bytes,6,opt,name=field_list" json:"field_list"` - FieldMap map[string]int `protobuf:"bytes,7,opt,name=field_map" json:"field_map"` +type sampleAssign struct { + 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 { +func makeSampleAssign(width, depth int) *sampleAssign { if width <= 0 || depth <= 0 { return nil } - ret := &SampleAssign{ - FieldA: intPtr(2), + ret := &sampleAssign{ + FieldA: 2, FieldE: "2", - FieldC: make(map[string]*SampleAssign), + FieldC: make(map[string]*sampleAssign), FieldList: []int{4, 5, 6}, FieldMap: map[string]int{ "4": 4, @@ -58,9 +58,9 @@ func makeSampleAssign(width, depth int) *SampleAssign { return ret } -// SampleAssignSmall is a struct with fewer fields than SampleAssign +// sampleAssignSmall is a struct with fewer fields than SampleAssign // Used to test XXX_unrecognized field encoding -type SampleAssignSmall struct { +type sampleAssignSmall struct { FieldA *int `protobuf:"varint,1,req,name=field_a"` FieldE string `protobuf:"bytes,5,opt,name=field_e"` XXX_unrecognized []byte `json:"-"` @@ -85,13 +85,13 @@ func TestAssignAny_Basic(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } - if dest.FieldA == nil || *dest.FieldA != 42 { + if dest.FieldA != 42 { t.Errorf("field_a: expected 42, got %v", dest.FieldA) } if dest.FieldE != "hello" { @@ -128,19 +128,19 @@ func TestAssignAny_NestedStruct(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } - if dest.FieldA == nil || *dest.FieldA != 1 { + 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 == nil || *dest.FieldD.FieldA != 2 { + if dest.FieldD.FieldA != 2 { t.Errorf("field_d.field_a: expected 2, got %v", dest.FieldD.FieldA) } if dest.FieldD.FieldE != "nested" { @@ -168,7 +168,7 @@ func TestAssignAny_List(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -206,7 +206,7 @@ func TestAssignAny_Map(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -236,7 +236,7 @@ func TestAssignAny_UnknownFields(t *testing.T) { }, } - dest := &SampleAssignSmall{} + dest := &sampleAssignSmall{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -305,13 +305,13 @@ func TestAssignAny_UnknownFields(t *testing.T) { func TestAssignAny_ListOfStructs(t *testing.T) { src := map[string]interface{}{ - "field_b": []*SampleAssign{ + "field_b": []*sampleAssign{ { - FieldA: intPtr(1), + FieldA: 2, FieldE: "first", }, { - FieldA: intPtr(2), + FieldA: 2, FieldE: "second", }, }, @@ -341,7 +341,7 @@ func TestAssignAny_ListOfStructs(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -351,13 +351,13 @@ func TestAssignAny_ListOfStructs(t *testing.T) { t.Fatalf("field_b: expected length 2, got %d", len(dest.FieldB)) } - if dest.FieldB[0].FieldA == nil || *dest.FieldB[0].FieldA != 1 { + 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 == nil || *dest.FieldB[1].FieldA != 2 { + 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" { @@ -406,7 +406,7 @@ func TestAssignAny_MapOfStructs(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -419,7 +419,7 @@ func TestAssignAny_MapOfStructs(t *testing.T) { if dest.FieldC["key1"] == nil { t.Fatalf("field_c['key1']: expected non-nil") } - if dest.FieldC["key1"].FieldA == nil || *dest.FieldC["key1"].FieldA != 10 { + 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" { @@ -434,7 +434,7 @@ func TestAssignAny_NilValues(t *testing.T) { } desc := &Descriptor{Kind: TypeKind_Struct, Name: "Test"} - dest := &SampleAssign{} + dest := &sampleAssign{} err = AssignAny(desc, nil, dest) if err != nil { @@ -457,7 +457,7 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { }, } - dest := &SampleAssign{} + dest := &sampleAssign{} err := AssignAny(desc, src, dest, WithDisallowNotDefined(true)) if err == nil { t.Fatalf("expected error for nonexistent field with DisallowNotFound") @@ -512,7 +512,7 @@ func BenchmarkAssignAny(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - dest := &SampleAssign{} + dest := &sampleAssign{} _ = AssignAny(desc, src, dest) } } @@ -541,7 +541,7 @@ func BenchmarkAssignAny_WithUnknownFields(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - dest := &SampleAssignSmall{} + dest := &sampleAssignSmall{} _ = AssignAny(desc, src, dest) } } diff --git a/trim/fetch_test.go b/trim/fetch_test.go index 8d2e304a..21001209 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -26,14 +26,14 @@ import ( ) type sampleFetch struct { - FieldA int `thrift:"FieldA,1" json:"field_a"` - FieldB []*sampleFetch `thrift:"FieldB,2" json:"field_b"` - FieldC map[string]*sampleFetch `thrift:"FieldC,3" json:"field_c"` - FieldD *sampleFetch `thrift:"FieldD,4" json:"field_d"` - FieldE string `thrift:"FieldE,5" json:"field_e"` - FieldList []int `thrift:"FieldList,6" json:"field_list"` - FieldMap map[string]int `thrift:"FieldMap,7" json:"field_map"` - _unknownFields unknown.Fields + 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 { @@ -61,7 +61,7 @@ func makeSampleFetch(width int, depth int) *sampleFetch { // makeDesc generates a descriptor for fetching SampleFetch struct. // NOTICE: it ignores FieldE. -func makeDesc(width int, depth int) *Descriptor { +func makeDesc(width int, depth int, withE bool) *Descriptor { if depth <= 0 { return nil } @@ -97,7 +97,14 @@ func makeDesc(width int, depth int) *Descriptor { }, } - nd := makeDesc(width, depth-1) + 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, Name: "MAP", @@ -146,7 +153,7 @@ func TestFetchAny(t *testing.T) { width := 2 depth := 2 obj := makeSampleFetch(width, depth) - desc := makeDesc(width, depth) + desc := makeDesc(width, depth, false) ret, err := FetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) @@ -172,7 +179,7 @@ func BenchmarkFetchAny(b *testing.B) { for _, bm := range benchmarks { obj := makeSampleFetch(bm.width, bm.depth) - desc := makeDesc(bm.width, bm.depth) + desc := makeDesc(bm.width, bm.depth, false) b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() @@ -189,7 +196,7 @@ func BenchmarkFetchAny_CacheHit(b *testing.B) { width := 3 depth := 3 obj := makeSampleFetch(width, depth) - desc := makeDesc(width, depth) + desc := makeDesc(width, depth, false) // Warm up the cache _, _ = FetchAny(desc, obj) From d4d60c09d2699879c75cfc094ca81f02cac851a3 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Mon, 1 Dec 2025 13:21:55 +0800 Subject: [PATCH 06/17] opt: pretty print desc --- trim/all_test.go | 270 ++++++++++++++++++++++++++++ trim/assign_test.go | 430 ++++++++++++++++++++++++++++++++++++++++++++ trim/desc.go | 105 ++++++++--- trim/fetch_test.go | 373 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1152 insertions(+), 26 deletions(-) diff --git a/trim/all_test.go b/trim/all_test.go index 58be8be3..a856fb7a 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -1047,3 +1047,273 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { 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_Scalar, + Name: "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, + Name: "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, + Name: "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, + Name: "OuterStruct", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "inner", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "ContainerStruct", + Children: []Field{ + {Name: "id", ID: 1}, + { + Name: "data", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "DataMap", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SpecificKeyMap", + Children: []Field{ + {Name: "key1"}, + {Name: "key2"}, + { + Name: "key3", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "Level1", + Children: []Field{ + { + Name: "level2", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "Level2", + Children: []Field{ + { + Name: "level3", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "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, + Name: "MixedStruct", + Children: []Field{ + {Name: "scalar_field", ID: 1}, + { + Name: "struct_field", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "NestedStruct", + Children: []Field{ + {Name: "a", ID: 1}, + }, + }, + }, + { + Name: "map_field", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "NestedMap", + Children: []Field{ + {Name: "*"}, + }, + }, + }, + { + Name: "scalar_desc", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Scalar, + Name: "ScalarType", + }, + }, + }, + } + + result := desc.String() + expected := `{ + "scalar_field": -, + "struct_field": { + "a": - + }, + "map_field": { + "*": - + }, + "scalar_desc": - +}` + require.Equal(t, expected, result) +} diff --git a/trim/assign_test.go b/trim/assign_test.go index 0957908a..d0a39eeb 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -1070,3 +1070,433 @@ func BenchmarkAssignScalar_SliceOfStructs(b *testing.B) { _ = AssignAny(desc, srcMap, dest) } } + +// ===================== 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, + Name: "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, + Name: "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, + Name: "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, + Name: "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) + } +} + +func BenchmarkAssignAny_CircularDescriptor(b *testing.B) { + // Create a linked list of depth 10 as source + depth := 10 + 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() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + dest := &circularAssignNode{} + _ = AssignAny(desc, src, dest) + } +} + +// 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, + Name: "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, + Name: "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") + } +} diff --git a/trim/desc.go b/trim/desc.go index 8c621911..03b06ccb 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -18,7 +18,7 @@ package trim import ( "strings" - "sync" + "sync/atomic" ) // TypeKind is the kind of type. @@ -49,9 +49,9 @@ type Descriptor struct { Children []Field // for speed-up search - sync.Once - ids map[int]Field - names map[string]Field + normalized int32 // atomic flag: 0=not started, 1=in progress/done + ids map[int]Field + names map[string]Field } // Field represents a mapping selection @@ -67,37 +67,90 @@ type Field struct { Desc *Descriptor } -// Normalize cache all the fields in the descriptor for speeding up search +// 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() { - d.Once.Do(func() { - 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() - } + // Use atomic to detect if we're already normalizing this descriptor + // This prevents infinite recursion in circular references + if !atomic.CompareAndSwapInt32(&d.normalized, 0, 1) { + // Already being normalized or already done + return + } + + 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. +// 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{} - var printer func(*Descriptor) - printer = func(pbtr *Descriptor) { - sb.WriteString("<" + pbtr.Name + ">") - for _, f := range pbtr.Children { - sb.WriteString("--" + f.Name) + 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.Name + ">") + return + } + visited[desc] = true + + // Get type prefix based on Kind + var typePrefix string + switch desc.Kind { + case TypeKind_Scalar: + sb.WriteString("-") + return + case TypeKind_StrMap: + typePrefix = "" + default: // TypeKind_Struct + typePrefix = "<" + desc.Name + ">" + } + + 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("\n") - continue + sb.WriteString("-") + } else { + printer(f.Desc, nextIndent) + } + + if i < len(desc.Children)-1 { + sb.WriteString(",") } - sb.WriteString("->") - printer(f.Desc) + sb.WriteString("\n") } + + sb.WriteString(indent + "}") } - printer(d) + + printer(d, "") return sb.String() } diff --git a/trim/fetch_test.go b/trim/fetch_test.go index 21001209..d2f0ee83 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -504,6 +504,379 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { // 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, + Name: "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, + Name: "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, + Name: "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, + Name: "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 From 8d4888babd08c7f09de6b4d2bb0df38781c966e8 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Wed, 3 Dec 2025 13:52:00 +0800 Subject: [PATCH 07/17] feat: `Descriptor` support marshal json --- trim/desc.go | 140 +++++++++++++++++++++++++++++ trim/desc_test.go | 225 ++++++++++++++++++++++++++++++++++++++++++++++ trim/fetch.go | 6 +- 3 files changed, 368 insertions(+), 3 deletions(-) create mode 100644 trim/desc_test.go diff --git a/trim/desc.go b/trim/desc.go index 03b06ccb..8a523768 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -17,6 +17,8 @@ package trim import ( + "encoding/json" + "fmt" "strings" "sync/atomic" ) @@ -154,3 +156,141 @@ func (d *Descriptor) String() string { 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.Name, + 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.Name = 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{Name: "$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.Name, "$ref:") { + refPath := strings.TrimPrefix(d.Children[i].Desc.Name, "$ref:") + if target, ok := refs[refPath]; ok { + d.Children[i].Desc = target + } + } else { + // Recursively resolve references + d.Children[i].Desc.resolveRefs(childPath, refs) + } + } + } +} diff --git a/trim/desc_test.go b/trim/desc_test.go new file mode 100644 index 00000000..1a30eb21 --- /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, + Name: "Root", + Children: []Field{ + { + Name: "field1", + ID: 1, + Desc: &Descriptor{ + Kind: TypeKind_Scalar, + Name: "Leaf1", + }, + }, + { + Name: "field2", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "Map1", + Children: []Field{ + { + Name: "key1", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Scalar, + Name: "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.Name, desc2.Name) + 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.Name, desc2.Children[0].Desc.Name) +} + +func TestDescriptorMarshalJSONWithCircularReference(t *testing.T) { + // Create a descriptor with circular reference + root := &Descriptor{ + Kind: TypeKind_Struct, + Name: "Root", + } + + child := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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.Name, root2.Name) + require.Len(t, root2.Children, 1) + require.NotNil(t, root2.Children[0].Desc) + require.Equal(t, "Child", root2.Children[0].Desc.Name) + + // 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, + Name: "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, + Name: "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_Scalar, + Name: "Shared", + } + + root := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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 index c74f3987..14084aa3 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -97,12 +97,12 @@ func getStructFieldInfo(t reflect.Type) *structFieldInfo { } // Parse thrift tag: "FieldName,ID" - use IndexByte for better performance - idx := strings.IndexByte(tag, ',') - if idx < 0 { + idx := strings.Split(tag, ",") + if len(idx) < 2 { continue } - fieldID, err := strconv.Atoi(tag[idx+1:]) + fieldID, err := strconv.Atoi(idx[1]) if err != nil { continue } From 0a2f7112fe7660dd2776d15017f046ef1d9bd8d1 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Sat, 6 Dec 2025 11:44:19 +0800 Subject: [PATCH 08/17] chore --- trim/fetch.go | 26 +++++++++++++++++--------- trim/fetch_test.go | 8 ++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/trim/fetch.go b/trim/fetch.go index 14084aa3..9092e897 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -27,7 +27,7 @@ import ( ) // FetchAny fetches the value of the field described by desc from any based on go reflect. -func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOptions) (interface{}, error) { +func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOption) (interface{}, error) { if any == nil || desc == nil { return nil, nil } @@ -35,15 +35,15 @@ func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOptions) (interfac desc.Normalize() var opt FetchOptions - if len(opts) > 0 { - opt = opts[0] + for _, op := range opts { + op(&opt) } v := reflect.ValueOf(any) return fetchValue(desc, v, &opt) } -// ErrNotFound is returned when a field/index/key is not found and DisallowNotFound is enabled +// 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 @@ -57,7 +57,15 @@ func (e ErrNotFound) Error() string { // FetchOptions contains options for FetchAny type FetchOptions struct { // DisallowNotFound if true, returns ErrNotFound when a field/index/key is not found - DisallowNotFound bool + disallowNotFound bool +} + +type FetchOption func(*FetchOptions) + +func WithDisallowNotFound(b bool) FetchOption { + return func(opt *FetchOptions) { + opt.disallowNotFound = b + } } // structFieldInfo caches field mapping information for a struct type @@ -173,7 +181,7 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac if found { fieldValue := v.Field(fieldIdx) if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { - if opt.DisallowNotFound { + if opt.disallowNotFound { return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d is nil", field.ID)} } continue @@ -196,10 +204,10 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac // 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 { + } else if opt.disallowNotFound { return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields", field.ID)} } - } else if opt.DisallowNotFound { + } else if opt.disallowNotFound { return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct", field.ID)} } } @@ -335,7 +343,7 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac val := v.MapIndex(reflect.ValueOf(key)) // Check if specific keys are requested but not available in the map if !val.IsValid() { - if opt.DisallowNotFound { + if opt.disallowNotFound { return nil, ErrNotFound{Parent: desc, Field: keyDescMap[key], Msg: fmt.Sprintf("key '%s' not found in map", key)} } else { continue diff --git a/trim/fetch_test.go b/trim/fetch_test.go index d2f0ee83..514ce243 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -367,7 +367,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -410,7 +410,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -457,7 +457,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -485,7 +485,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - ret, err := FetchAny(desc, obj, FetchOptions{DisallowNotFound: true}) + ret, err := FetchAny(desc, obj, WithDisallowNotFound(true)) if err != nil { t.Fatalf("unexpected error: %v", err) } From 492a07b17ffde4081ca9952d1f971dd2a29567e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Mon, 15 Dec 2025 13:44:16 +0800 Subject: [PATCH 09/17] feat: error shows stack-trace --- trim/all_test.go | 57 +-- trim/assign.go | 186 ++++++--- trim/assign_test.go | 957 ++++++++++++++++++++++++++++++++++++++++++-- trim/fetch.go | 135 ++++--- trim/fetch_test.go | 446 ++++++++++++++++++++- 5 files changed, 1609 insertions(+), 172 deletions(-) diff --git a/trim/all_test.go b/trim/all_test.go index a856fb7a..41f1ee2c 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -11,6 +11,11 @@ import ( "github.com/stretchr/testify/require" ) +func fetchAny(desc *Descriptor, any interface{}) (interface{}, error) { + fetcher := &Fetcher{} + return fetcher.FetchAny(desc, any) +} + func TestFetchAndAssign(t *testing.T) { src := makeSampleFetch(3, 3) srcjson, err := json.Marshal(src) @@ -18,7 +23,7 @@ func TestFetchAndAssign(t *testing.T) { t.Fatalf("json.Marshal failed: %v", err) } desc := makeDesc(3, 3, true) - m, err := FetchAny(desc, src) + m, err := fetchAny(desc, src) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -29,7 +34,7 @@ func TestFetchAndAssign(t *testing.T) { require.Equal(t, string(srcjson), string(mjson)) dest := makeSampleAssign(3, 3) - err = AssignAny(desc, m, dest) + err = assignAny(desc, m, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -111,7 +116,7 @@ func TestFetchAndAssign_UnknownToUnrecognized(t *testing.T) { } // Fetch from thrift struct - fetched, err := FetchAny(desc, src) + fetched, err := fetchAny(desc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -136,7 +141,7 @@ func TestFetchAndAssign_UnknownToUnrecognized(t *testing.T) { }, } - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) require.NoError(t, err) // Verify field_a is assigned correctly @@ -204,7 +209,7 @@ func TestFetchAndAssign_UnknownToKnown(t *testing.T) { } // Fetch from thrift struct - fetched, err := FetchAny(fetchDesc, src) + fetched, err := fetchAny(fetchDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -225,7 +230,7 @@ func TestFetchAndAssign_UnknownToKnown(t *testing.T) { }, } - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) require.NoError(t, err) // Verify both fields are assigned correctly to known fields @@ -267,7 +272,7 @@ func TestFetchAndAssign_KnownToUnrecognized(t *testing.T) { } // Fetch from thrift struct - fetched, err := FetchAny(fetchDesc, src) + fetched, err := fetchAny(fetchDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -292,7 +297,7 @@ func TestFetchAndAssign_KnownToUnrecognized(t *testing.T) { }, } - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) require.NoError(t, err) // Verify field_a is assigned correctly @@ -380,7 +385,7 @@ func TestFetchAndAssign_MixedScenario(t *testing.T) { } // Fetch from thrift struct - fetched, err := FetchAny(fetchDesc, src) + fetched, err := fetchAny(fetchDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -403,7 +408,7 @@ func TestFetchAndAssign_MixedScenario(t *testing.T) { } dest := &protoMixedStruct{} - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) require.NoError(t, err) // Verify known fields @@ -501,7 +506,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { } // Fetch from thrift struct - fetched, err := FetchAny(fetchDesc, src) + fetched, err := fetchAny(fetchDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -535,7 +540,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { } dest := &protoOuter{} - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) require.NoError(t, err) // Verify @@ -570,7 +575,7 @@ func TestFetchAndAssign_ShallowDescriptor(t *testing.T) { } // Fetch with shallow descriptor - fetched, err := FetchAny(shallowDesc, src) + fetched, err := fetchAny(shallowDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -592,7 +597,7 @@ func TestFetchAndAssign_ShallowDescriptor(t *testing.T) { // Now assign with the same shallow descriptor dest := makeSampleAssign(2, 3) - err = AssignAny(shallowDesc, fetched, dest) + err = assignAny(shallowDesc, fetched, dest) require.NoError(t, err) // Verify scalar fields are correctly assigned @@ -646,7 +651,7 @@ func TestFetchAndAssign_MissingFieldsInDescriptor(t *testing.T) { } // Fetch with partial descriptor - fetched, err := FetchAny(partialDesc, src) + fetched, err := fetchAny(partialDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -669,7 +674,7 @@ func TestFetchAndAssign_MissingFieldsInDescriptor(t *testing.T) { FieldD: "original_d", } - err = AssignAny(partialDesc, fetched, dest) + err = assignAny(partialDesc, fetched, dest) require.NoError(t, err) // Verify only specified fields are assigned @@ -742,7 +747,7 @@ func TestFetchAndAssign_NestedMissingFields(t *testing.T) { } // Fetch with partial descriptor - fetched, err := FetchAny(partialDesc, src) + fetched, err := fetchAny(partialDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -772,7 +777,7 @@ func TestFetchAndAssign_NestedMissingFields(t *testing.T) { }, } - err = AssignAny(partialDesc, fetched, dest) + err = assignAny(partialDesc, fetched, dest) require.NoError(t, err) // Verify assigned fields @@ -825,7 +830,7 @@ func TestFetchAndAssign_DescShallowerThanNestedList(t *testing.T) { } // Fetch with shallow descriptor - fetched, err := FetchAny(shallowDesc, src) + fetched, err := fetchAny(shallowDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -838,7 +843,7 @@ func TestFetchAndAssign_DescShallowerThanNestedList(t *testing.T) { // Assign with shallow descriptor dest := &protoContainer{} - err = AssignAny(shallowDesc, fetched, dest) + err = assignAny(shallowDesc, fetched, dest) require.NoError(t, err) // Verify items are assigned @@ -899,7 +904,7 @@ func TestFetchAndAssign_DescShallowerThanNestedMap(t *testing.T) { } // Fetch with shallow descriptor - fetched, err := FetchAny(shallowDesc, src) + fetched, err := fetchAny(shallowDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -912,7 +917,7 @@ func TestFetchAndAssign_DescShallowerThanNestedMap(t *testing.T) { // Assign with shallow descriptor dest := &protoContainer{} - err = AssignAny(shallowDesc, fetched, dest) + err = assignAny(shallowDesc, fetched, dest) require.NoError(t, err) // Verify data is assigned @@ -967,7 +972,7 @@ func TestFetchAndAssign_PartialMapKeys(t *testing.T) { } // Fetch with partial descriptor - fetched, err := FetchAny(partialDesc, src) + fetched, err := fetchAny(partialDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -991,7 +996,7 @@ func TestFetchAndAssign_PartialMapKeys(t *testing.T) { "key2": 999, // pre-existing key }, } - err = AssignAny(partialDesc, fetched, dest) + err = assignAny(partialDesc, fetched, dest) require.NoError(t, err) // Verify only specified keys are assigned, pre-existing keys might be overwritten depending on implementation @@ -1026,7 +1031,7 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { } // Fetch with empty descriptor - fetched, err := FetchAny(emptyDesc, src) + fetched, err := fetchAny(emptyDesc, src) require.NoError(t, err) fetchedMap, ok := fetched.(map[string]interface{}) @@ -1040,7 +1045,7 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { FieldA: 999, FieldB: "original", } - err = AssignAny(emptyDesc, fetched, dest) + err = assignAny(emptyDesc, fetched, dest) require.NoError(t, err) // Original values should be preserved diff --git a/trim/assign.go b/trim/assign.go index 756876d6..f064ba38 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -33,13 +33,30 @@ type AssignOptions struct { DisallowNotDefined bool } -type AssignOption func(*AssignOptions) +type Assigner struct { + AssignOptions +} -// WithDisallowNotDefined sets the DisallowNotFound option -func WithDisallowNotDefined(disallow bool) AssignOption { - return func(o *AssignOptions) { - o.DisallowNotDefined = disallow +// 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, they will be encoded +// to XXX_unrecognized field using protobuf binary encoding. +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 @@ -121,31 +138,86 @@ func getPBStructFieldInfo(t reflect.Type) *pbStructFieldInfo { return actual.(*pbStructFieldInfo) } -// 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, they will be encoded -// to XXX_unrecognized field using protobuf binary encoding. -func AssignAny(desc *Descriptor, src interface{}, dest interface{}, opts ...AssignOption) error { - if src == nil || dest == nil || desc == nil { - return nil +// stackFrame represents a single frame in the path tracking stack +type stackFrame struct { + fieldName string + fieldID int + isMapKey bool // true if this is a map key + index int // for array elements, -1 if not array +} + +// 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 +} - desc.Normalize() +// pathStack tracks the path from root to current node +type pathStack []stackFrame + +// push adds a new frame to the stack +func (s *pathStack) push(name string, id int, isMapKey bool, index int) { + *s = append(*s, stackFrame{ + fieldName: name, + fieldID: id, + isMapKey: isMapKey, + index: index, + }) +} - var opt AssignOptions - for _, o := range opts { - o(&opt) +// pop removes the last frame from the stack +func (s *pathStack) pop() { + if len(*s) > 0 { + *s = (*s)[:len(*s)-1] } +} - destValue := reflect.ValueOf(dest) - if destValue.Kind() != reflect.Ptr { - return fmt.Errorf("dest must be a pointer to struct") +// 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.isMapKey { + sb.WriteString("[") + sb.WriteString(frame.fieldName) + sb.WriteString("]") + } else if frame.index >= 0 { + sb.WriteString("[") + sb.WriteString(strconv.Itoa(frame.index)) + sb.WriteString("]") + } else { + sb.WriteString(".") + sb.WriteString(frame.fieldName) + } } - return assignValue(desc, src, destValue.Elem(), &opt) + return sb.String() } // assignValue is the internal implementation that works with reflect.Value directly -func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { +func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions, stack *pathStack) error { if src == nil { return nil } @@ -161,23 +233,23 @@ func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt switch desc.Kind { case TypeKind_Struct: - return assignStruct(desc, src, destValue, opt) + return assignStruct(desc, src, destValue, opt, stack) case TypeKind_StrMap: - return assignStrMap(desc, src, destValue, opt) + return assignStrMap(desc, src, destValue, opt, stack) default: return assignScalar(src, destValue) } } // assignStruct handles TypeKind_Struct assignment -func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { +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, got %T", src) + 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, got %v", destValue.Kind()) + return fmt.Errorf("expected struct destination at %s, got %v", stack.buildPath(), destValue.Kind()) } // Get cached field info for this type @@ -195,6 +267,10 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op // 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] @@ -206,23 +282,34 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op continue } + // Push field onto stack + stack.push(key, fieldID, false, -1) + // If field has a child descriptor, recursively assign + var err error if hasDescField && descField.Desc != nil { - if err := assignValueToField(descField.Desc, value, fieldValue, opt); err != nil { - return err - } + err = assignValueToField(descField.Desc, value, fieldValue, opt, stack) } else { // Otherwise, assign the value directly - if err := assignScalar(value, fieldValue); err != nil { - return err - } + err = assignScalar(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 { - return ErrNotFound{Parent: desc, Field: Field{Name: key}, Msg: fmt.Sprintf("field '%s' not found in struct", key)} + // Include path in error message + stack.push(key, fieldID, false, -1) + 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)} } } @@ -255,26 +342,26 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op } // assignValueToField assigns a value to a field, handling pointer allocation -func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Value, opt *AssignOptions) error { +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() { fieldValue.Set(reflect.New(fieldValue.Type().Elem())) } - return assignValue(desc, src, fieldValue.Elem(), opt) + return assignValue(desc, src, fieldValue.Elem(), opt, stack) } - return assignValue(desc, src, fieldValue, opt) + return assignValue(desc, src, fieldValue, opt, stack) } // assignStrMap handles TypeKind_StrMap assignment -func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, opt *AssignOptions) error { +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, got %T", src) + 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, got %v", destValue.Kind()) + return fmt.Errorf("expected map destination at %s, got %v", stack.buildPath(), destValue.Kind()) } // Find wildcard or keyed descriptors @@ -301,19 +388,24 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op continue } + // Push map key onto stack + stack.push(key, 0, true, -1) + // Find the appropriate descriptor + var err error if wildcardDesc != nil { - if err := assignValueToField(wildcardDesc, value, elemValue, opt); err != nil { - return err - } + err = assignValueToField(wildcardDesc, value, elemValue, opt, stack) } else if child, ok := keyDescMap[key]; ok && child.Desc != nil { - if err := assignValueToField(child.Desc, value, elemValue, opt); err != nil { - return err - } + err = assignValueToField(child.Desc, value, elemValue, opt, stack) } else { - if err := assignScalar(value, elemValue); err != nil { - return err - } + err = assignScalar(value, elemValue) + } + + // Pop map key from stack + stack.pop() + + if err != nil { + return err } destValue.SetMapIndex(reflect.ValueOf(key), elemValue) diff --git a/trim/assign_test.go b/trim/assign_test.go index d0a39eeb..3848c1e3 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -24,6 +24,11 @@ import ( "github.com/cloudwego/dynamicgo/proto/binary" ) +func assignAny(desc *Descriptor, src interface{}, dest interface{}) error { + assigner := &Assigner{} + return assigner.AssignAny(desc, src, dest) +} + type sampleAssign struct { 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"` @@ -86,7 +91,7 @@ func TestAssignAny_Basic(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -129,7 +134,7 @@ func TestAssignAny_NestedStruct(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -169,7 +174,7 @@ func TestAssignAny_List(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -207,7 +212,7 @@ func TestAssignAny_Map(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -237,7 +242,7 @@ func TestAssignAny_UnknownFields(t *testing.T) { } dest := &sampleAssignSmall{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -342,7 +347,7 @@ func TestAssignAny_ListOfStructs(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -407,7 +412,7 @@ func TestAssignAny_MapOfStructs(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -428,7 +433,7 @@ func TestAssignAny_MapOfStructs(t *testing.T) { } func TestAssignAny_NilValues(t *testing.T) { - err := AssignAny(nil, nil, nil) + err := assignAny(nil, nil, nil) if err != nil { t.Errorf("expected nil error for nil inputs, got %v", err) } @@ -436,7 +441,7 @@ func TestAssignAny_NilValues(t *testing.T) { desc := &Descriptor{Kind: TypeKind_Struct, Name: "Test"} dest := &sampleAssign{} - err = AssignAny(desc, nil, dest) + err = assignAny(desc, nil, dest) if err != nil { t.Errorf("expected nil error for nil src, got %v", err) } @@ -458,7 +463,8 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { } dest := &sampleAssign{} - err := AssignAny(desc, src, dest, WithDisallowNotDefined(true)) + as := Assigner{AssignOptions{DisallowNotDefined: true}} + err := as.AssignAny(desc, src, dest) if err == nil { t.Fatalf("expected error for nonexistent field with DisallowNotFound") } @@ -472,7 +478,7 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { } } -func BenchmarkAssignAny(b *testing.B) { +func BenchmarkassignAny(b *testing.B) { src := map[string]interface{}{ "field_a": 42, "field_e": "hello", @@ -513,7 +519,7 @@ func BenchmarkAssignAny(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { dest := &sampleAssign{} - _ = AssignAny(desc, src, dest) + _ = assignAny(desc, src, dest) } } @@ -542,7 +548,7 @@ func BenchmarkAssignAny_WithUnknownFields(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { dest := &sampleAssignSmall{} - _ = AssignAny(desc, src, dest) + _ = assignAny(desc, src, dest) } } @@ -620,7 +626,7 @@ func TestAssignScalar_StructToStruct(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -670,7 +676,7 @@ func TestAssignScalar_StructToStruct(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -719,7 +725,7 @@ func TestAssignScalar_SliceToSlice(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -758,7 +764,7 @@ func TestAssignScalar_SliceToSlice(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -796,7 +802,7 @@ func TestAssignScalar_MapToMap(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -835,7 +841,7 @@ func TestAssignScalar_MapToMap(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -901,7 +907,7 @@ func TestAssignScalar_ComplexNested(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -954,7 +960,7 @@ func TestAssignScalar_NilHandling(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -992,7 +998,7 @@ func TestAssignScalar_NilHandling(t *testing.T) { } dest := &Wrapper{} - err := AssignAny(desc, srcMap, dest) + err := assignAny(desc, srcMap, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1034,7 +1040,7 @@ func BenchmarkAssignScalar_StructToStruct(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { dest := &Wrapper{} - _ = AssignAny(desc, srcMap, dest) + _ = assignAny(desc, srcMap, dest) } } @@ -1067,7 +1073,7 @@ func BenchmarkAssignScalar_SliceOfStructs(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { dest := &Wrapper{} - _ = AssignAny(desc, srcMap, dest) + _ = assignAny(desc, srcMap, dest) } } @@ -1138,7 +1144,7 @@ func TestAssignAny_CircularDescriptor_LinkedList(t *testing.T) { desc := makeCircularAssignDesc() dest := &circularAssignNode{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1178,7 +1184,7 @@ func TestAssignAny_CircularDescriptor_SingleNode(t *testing.T) { desc := makeCircularAssignDesc() dest := &circularAssignNode{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1215,7 +1221,7 @@ func TestAssignAny_CircularDescriptor_Tree(t *testing.T) { desc := makeCircularAssignTreeDesc() dest := &circularAssignTree{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1255,7 +1261,7 @@ func TestAssignAny_CircularDescriptor_NilSrc(t *testing.T) { dest := &circularAssignNode{Value: 999} // Assign with nil src should not modify dest - err := AssignAny(desc, nil, dest) + err := assignAny(desc, nil, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1283,7 +1289,7 @@ func TestAssignAny_CircularDescriptor_DeepList(t *testing.T) { desc := makeCircularAssignDesc() dest := &circularAssignNode{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1363,7 +1369,7 @@ func TestAssignAny_CircularDescriptor_MapOfNodes(t *testing.T) { desc := makeCircularAssignMapDesc() dest := &circularAssignMapNode{} - err := AssignAny(desc, src, dest) + err := assignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1426,7 +1432,7 @@ func BenchmarkAssignAny_CircularDescriptor(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { dest := &circularAssignNode{} - _ = AssignAny(desc, src, dest) + _ = assignAny(desc, src, dest) } } @@ -1463,7 +1469,7 @@ func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { fetchDesc.Children[1].Desc = fetchDesc // Fetch - fetched, err := FetchAny(fetchDesc, srcList) + fetched, err := fetchAny(fetchDesc, srcList) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -1481,7 +1487,7 @@ func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { // Assign dest := &circularAssignNode{} - err = AssignAny(assignDesc, fetched, dest) + err = assignAny(assignDesc, fetched, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } @@ -1500,3 +1506,888 @@ func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { 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, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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}} + 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, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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("field_a", 1, false, -1) + }, + expected: "$.field_a", + }, + { + name: "nested fields", + ops: func(s *pathStack) { + s.push("field_d", 4, false, -1) + s.push("field_a", 1, false, -1) + }, + expected: "$.field_d.field_a", + }, + { + name: "map key", + ops: func(s *pathStack) { + s.push("field_c", 3, false, -1) + s.push("my_key", 0, true, -1) + }, + expected: "$.field_c[my_key]", + }, + { + name: "array index", + ops: func(s *pathStack) { + s.push("field_b", 2, false, -1) + s.push("", 0, false, 0) + }, + expected: "$.field_b[0]", + }, + { + name: "complex path", + ops: func(s *pathStack) { + s.push("root_field", 1, false, -1) + s.push("map_field", 3, false, -1) + s.push("key1", 0, true, -1) + s.push("nested", 4, false, -1) + s.push("array", 2, false, -1) + s.push("", 0, false, 2) + }, + expected: "$.root_field.map_field[key1].nested.array[2]", + }, + { + name: "push and pop", + ops: func(s *pathStack) { + s.push("field_a", 1, false, -1) + s.push("field_b", 2, false, -1) + 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{fieldName: "test", fieldID: 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + 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_SimpleStruct tests baseline performance on simple struct +func BenchmarkAssignAny_SimpleStruct(b *testing.B) { + src := map[string]interface{}{ + "field_a": 42, + "field_e": "hello", + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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_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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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_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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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_DeepNesting tests performance with deeply nested structures +func BenchmarkAssignAny_DeepNesting(b *testing.B) { + // Create a deeply nested structure + src := map[string]interface{}{ + "field_a": 1, + } + current := src + for i := 0; i < 10; i++ { + nested := map[string]interface{}{ + "field_a": i + 2, + } + current["field_d"] = nested + current = nested + } + + // Create corresponding descriptor + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + } + currentDesc := desc + for i := 0; i < 10; i++ { + childDesc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + } + currentDesc.Children = append(currentDesc.Children, Field{ + Name: "field_d", + ID: 4, + Desc: childDesc, + }) + currentDesc = childDesc + } + + 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + _ = as.AssignAny(desc, src, dest) + } +} + +// BenchmarkAssignAny_ComplexMixed tests performance with complex mixed structure +func BenchmarkAssignAny_ComplexMixed(b *testing.B) { + src := map[string]interface{}{ + "field_a": 1, + "field_e": "root", + "field_b": []interface{}{ + map[string]interface{}{ + "field_a": 10, + "field_e": "list1", + }, + map[string]interface{}{ + "field_a": 20, + "field_e": "list2", + }, + }, + "field_c": map[string]interface{}{ + "key1": map[string]interface{}{ + "field_a": 100, + "field_d": map[string]interface{}{ + "field_a": 200, + "field_e": "deep", + }, + }, + "key2": map[string]interface{}{ + "field_a": 300, + }, + }, + "field_map": map[string]interface{}{ + "mk1": 1000, + "mk2": 2000, + }, + } + + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Scalar, + Name: "LIST", + }, + }, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "field_map", + ID: 7, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + }, + }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + _ = 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("field", j, false, -1) + } + for j := 0; j < 10; j++ { + stack.pop() + } + putStackFrames(stack) + } + }) + + b.Run("build_path_shallow", func(b *testing.B) { + stack := getStackFrames() + stack.push("field_a", 1, false, -1) + stack.push("field_b", 2, false, -1) + + 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("field", i, false, -1) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = stack.buildPath() + } + }) + + b.Run("build_path_mixed", func(b *testing.B) { + stack := getStackFrames() + stack.push("root", 1, false, -1) + stack.push("map_field", 3, false, -1) + stack.push("key1", 0, true, -1) + stack.push("nested", 4, false, -1) + stack.push("", 0, false, 5) + stack.push("deep", 6, false, -1) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = stack.buildPath() + } + }) +} + +// BenchmarkStackFramePool tests the performance of memory pool +func BenchmarkStackFramePool(b *testing.B) { + b.Run("with_pool", func(b *testing.B) { + for i := 0; i < b.N; i++ { + frames := getStackFrames() + for j := 0; j < 16; j++ { + *frames = append(*frames, stackFrame{fieldName: "test", fieldID: j}) + } + putStackFrames(frames) + } + }) + + b.Run("without_pool", func(b *testing.B) { + for i := 0; i < b.N; i++ { + frames := make([]stackFrame, 0, 16) + for j := 0; j < 16; j++ { + frames = append(frames, stackFrame{fieldName: "test", fieldID: j}) + } + _ = frames + } + }) +} + +// 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + _ = as.AssignAny(desc, srcWithError, dest) + } + }) +} diff --git a/trim/fetch.go b/trim/fetch.go index 9092e897..93bed039 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -26,21 +26,30 @@ import ( "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. -func FetchAny(desc *Descriptor, any interface{}, opts ...FetchOption) (interface{}, error) { +func (f Fetcher) FetchAny(desc *Descriptor, any interface{}) (interface{}, error) { if any == nil || desc == nil { return nil, nil } desc.Normalize() - var opt FetchOptions - for _, op := range opts { - op(&opt) - } + // Initialize path stack from pool + stack := getStackFrames() + defer putStackFrames(stack) v := reflect.ValueOf(any) - return fetchValue(desc, v, &opt) + return fetchValue(desc, v, &f.FetchOptions, stack) } // ErrNotFound is returned when a field/index/key is not found and disallowNotFound is enabled @@ -51,21 +60,10 @@ type ErrNotFound struct { } func (e ErrNotFound) Error() string { - return fmt.Sprintf("not found %v at %v: %s", e.Field.Name, e.Parent.Name, e.Msg) -} - -// FetchOptions contains options for FetchAny -type FetchOptions struct { - // DisallowNotFound if true, returns ErrNotFound when a field/index/key is not found - disallowNotFound bool -} - -type FetchOption func(*FetchOptions) - -func WithDisallowNotFound(b bool) FetchOption { - return func(opt *FetchOptions) { - opt.disallowNotFound = b + if e.Msg != "" { + return fmt.Sprintf("not found %v at %v: %s", e.Field.Name, e.Parent.Name, e.Msg) } + return fmt.Sprintf("not found %v at %v", e.Field.Name, e.Parent.Name) } // structFieldInfo caches field mapping information for a struct type @@ -125,7 +123,7 @@ func getStructFieldInfo(t reflect.Type) *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) (interface{}, error) { +func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { // Dereference pointers for v.Kind() == reflect.Ptr { if v.IsNil() { @@ -136,10 +134,10 @@ func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface switch desc.Kind { case TypeKind_Struct: - return fetchStruct(desc, v, opt) + return fetchStruct(desc, v, opt, stack) case TypeKind_StrMap: - return fetchStrMap(desc, v, opt) + return fetchStrMap(desc, v, opt, stack) default: return v.Interface(), nil @@ -147,7 +145,7 @@ func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface } // fetchStruct handles TypeKind_Struct -func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { +func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pathStack) (interface{}, error) { if v.Kind() != reflect.Struct { return nil, nil } @@ -181,34 +179,54 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac if found { fieldValue := v.Field(fieldIdx) if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { - if opt.disallowNotFound { - return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d is nil", field.ID)} + if opt.DisallowNotFound { + stack.push(field.Name, field.ID, false, -1) + 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(field.Name, field.ID, false, -1) + // If field has a child descriptor, recursively fetch + var err error if field.Desc != nil { - fetched, err := fetchValue(field.Desc, fieldValue, opt) - if err != nil { - return nil, err + var fetched interface{} + fetched, err = fetchValue(field.Desc, fieldValue, opt, stack) + if err == nil { + result[field.Name] = fetched } - 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 { - return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct or unknownFields", field.ID)} + } else if opt.DisallowNotFound { + stack.push(field.Name, field.ID, false, -1) + 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 { - return nil, ErrNotFound{Parent: desc, Field: *field, Msg: fmt.Sprintf("field ID=%d not found in struct", field.ID)} + } else if opt.DisallowNotFound { + stack.push(field.Name, field.ID, false, -1) + 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 @@ -303,7 +321,7 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } // fetchStrMap handles TypeKind_StrMap -func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interface{}, error) { +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 } @@ -323,15 +341,27 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac result[keyStr] = nil continue } + + // Push map key onto stack + stack.push(keyStr, 0, true, -1) + + var err error if wildcardDesc != nil { - fetched, err := fetchValue(wildcardDesc, elemValue, opt) - if err != nil { - return nil, err + var fetched interface{} + fetched, err = fetchValue(wildcardDesc, elemValue, opt, stack) + if err == nil { + result[keyStr] = fetched } - result[keyStr] = fetched } else { result[keyStr] = elemValue.Interface() } + + // Pop map key from stack + stack.pop() + + if err != nil { + return nil, err + } } return result, nil } @@ -343,8 +373,11 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac val := v.MapIndex(reflect.ValueOf(key)) // Check if specific keys are requested but not available in the map if !val.IsValid() { - if opt.disallowNotFound { - return nil, ErrNotFound{Parent: desc, Field: keyDescMap[key], Msg: fmt.Sprintf("key '%s' not found in map", key)} + if opt.DisallowNotFound { + stack.push(key, 0, true, -1) + 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 } @@ -353,15 +386,27 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions) (interfac result[key] = nil continue } + + // Push map key onto stack + stack.push(key, 0, true, -1) + + var err error if child.Desc != nil { - fetched, err := fetchValue(child.Desc, val, opt) - if err != nil { - return nil, err + var fetched interface{} + fetched, err = fetchValue(child.Desc, val, opt, stack) + if err == nil { + result[key] = fetched } - result[key] = fetched } else { result[key] = val.Interface() } + + // Pop map key 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 index 514ce243..9f0861ee 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -154,7 +154,7 @@ func TestFetchAny(t *testing.T) { depth := 2 obj := makeSampleFetch(width, depth) desc := makeDesc(width, depth, false) - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -184,7 +184,7 @@ func BenchmarkFetchAny(b *testing.B) { b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = FetchAny(desc, obj) + _, _ = fetchAny(desc, obj) } }) } @@ -199,12 +199,12 @@ func BenchmarkFetchAny_CacheHit(b *testing.B) { desc := makeDesc(width, depth, false) // Warm up the cache - _, _ = FetchAny(desc, obj) + _, _ = fetchAny(desc, obj) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = FetchAny(desc, obj) + _, _ = fetchAny(desc, obj) } } @@ -265,7 +265,7 @@ func TestFetchAnyWithUnknownFields(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -333,7 +333,7 @@ func TestFetchAnyWithEmptyUnknownFields(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -367,7 +367,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -410,7 +411,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -457,7 +459,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - _, err := FetchAny(desc, obj, WithDisallowNotFound(true)) + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") } @@ -485,7 +488,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { }, } - ret, err := FetchAny(desc, obj, WithDisallowNotFound(true)) + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + ret, err := f.FetchAny(desc, obj) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -572,7 +576,7 @@ func TestFetchAny_CircularDescriptor_LinkedList(t *testing.T) { desc := makeCircularDesc() // Fetch should work correctly, recursing until Next is nil - fetched, err := FetchAny(desc, list) + fetched, err := fetchAny(desc, list) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -618,7 +622,7 @@ func TestFetchAny_CircularDescriptor_SingleNode(t *testing.T) { desc := makeCircularDesc() - fetched, err := FetchAny(desc, node) + fetched, err := fetchAny(desc, node) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -660,7 +664,7 @@ func TestFetchAny_CircularDescriptor_Tree(t *testing.T) { desc := makeCircularTreeDesc() - fetched, err := FetchAny(desc, tree) + fetched, err := fetchAny(desc, tree) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -706,7 +710,7 @@ func TestFetchAny_CircularDescriptor_NilRoot(t *testing.T) { desc := makeCircularDesc() // Fetch with nil input should return nil - fetched, err := FetchAny(desc, nil) + fetched, err := fetchAny(desc, nil) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -729,7 +733,7 @@ func TestFetchAny_CircularDescriptor_DeepList(t *testing.T) { desc := makeCircularDesc() - fetched, err := FetchAny(desc, head) + fetched, err := fetchAny(desc, head) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -808,7 +812,7 @@ func TestFetchAny_CircularDescriptor_MapOfNodes(t *testing.T) { desc := makeCircularMapDesc() - fetched, err := FetchAny(desc, node) + fetched, err := fetchAny(desc, node) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -873,7 +877,7 @@ func BenchmarkFetchAny_CircularDescriptor(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = FetchAny(desc, head) + _, _ = fetchAny(desc, head) } } @@ -907,7 +911,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -985,7 +989,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -1084,7 +1088,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -1181,7 +1185,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { }, } - ret, err := FetchAny(desc, obj) + ret, err := fetchAny(desc, obj) if err != nil { t.Fatalf("FetchAny failed: %v", err) } @@ -1226,3 +1230,403 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { } }) } + +// 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, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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}} + _, 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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "missing", ID: 55}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + _, 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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + } + + f := Fetcher{} + 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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{} + 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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, + }, + } + + f := Fetcher{} + 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, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "missing", ID: 99}, + }, + }, + }, + }, + } + + f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = f.FetchAny(desc, obj) + } + }) +} From 3ce399d20391dac1a2061102feb75c88d7fd62a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Mon, 15 Dec 2025 16:25:02 +0800 Subject: [PATCH 10/17] feat: support `TypeKind_List` --- trim/assign.go | 321 +++++++++++++++------ trim/assign_test.go | 688 ++++++++++++++++++++++++++++++++++++++++++-- trim/desc.go | 81 ++++++ trim/fetch.go | 140 ++++++++- trim/fetch_test.go | 260 ++++++++++++++++- 5 files changed, 1373 insertions(+), 117 deletions(-) diff --git a/trim/assign.go b/trim/assign.go index f064ba38..d16128ae 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -138,84 +138,6 @@ func getPBStructFieldInfo(t reflect.Type) *pbStructFieldInfo { return actual.(*pbStructFieldInfo) } -// stackFrame represents a single frame in the path tracking stack -type stackFrame struct { - fieldName string - fieldID int - isMapKey bool // true if this is a map key - index int // for array elements, -1 if not array -} - -// 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(name string, id int, isMapKey bool, index int) { - *s = append(*s, stackFrame{ - fieldName: name, - fieldID: id, - isMapKey: isMapKey, - index: index, - }) -} - -// 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.isMapKey { - sb.WriteString("[") - sb.WriteString(frame.fieldName) - sb.WriteString("]") - } else if frame.index >= 0 { - sb.WriteString("[") - sb.WriteString(strconv.Itoa(frame.index)) - sb.WriteString("]") - } else { - sb.WriteString(".") - sb.WriteString(frame.fieldName) - } - } - - return sb.String() -} - // 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 { @@ -236,6 +158,8 @@ func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt 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 assignScalar(src, destValue) } @@ -283,7 +207,7 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op } // Push field onto stack - stack.push(key, fieldID, false, -1) + stack.push(TypeKind_Struct, key, fieldID) // If field has a child descriptor, recursively assign var err error @@ -306,7 +230,7 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op unassignedFields[key] = value } else if opt.DisallowNotDefined { // Include path in error message - stack.push(key, fieldID, false, -1) + 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)} @@ -389,7 +313,7 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op } // Push map key onto stack - stack.push(key, 0, true, -1) + stack.push(TypeKind_StrMap, key, 0) // Find the appropriate descriptor var err error @@ -414,6 +338,241 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op 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 = assignScalar(srcSlice[i], elemValue) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return err + } + } + return nil + } + + // Handle slice (dynamic size) + elemType := destValue.Type().Elem() + newSlice := reflect.MakeSlice(destValue.Type(), srcLen, srcLen) + + for i := 0; i < srcLen; i++ { + elemValue := newSlice.Index(i) + if srcSlice[i] == nil { + // Keep as zero value + continue + } + + // Push list index onto stack + stack.push(TypeKind_List, "*", i) + + var err error + if wildcardDesc != nil { + // Handle pointer element type + if elemType.Kind() == reflect.Ptr { + newElem := reflect.New(elemType.Elem()) + err = assignValue(wildcardDesc, srcSlice[i], newElem.Elem(), opt, stack) + if err == nil { + elemValue.Set(newElem) + } + } else { + err = assignValue(wildcardDesc, srcSlice[i], elemValue, opt, stack) + } + } else { + err = assignScalar(srcSlice[i], elemValue) + } + + // Pop list index from stack + stack.pop() + + if err != nil { + return err + } + } + + destValue.Set(newSlice) + 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 = assignScalar(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 = assignScalar(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 diff --git a/trim/assign_test.go b/trim/assign_test.go index 3848c1e3..71d25d6f 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -432,6 +432,638 @@ func TestAssignAny_MapOfStructs(t *testing.T) { } } +// 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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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{} + 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.Name != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) + } + }) + + // 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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "0", + ID: 0, // src[0] -> dest[0] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "1", + ID: 1, // src[0] -> dest[1] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "5", + ID: 5, // src[0] -> dest[5] + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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) != 2 { + 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 { @@ -1760,51 +2392,51 @@ func TestPathStack(t *testing.T) { { name: "single field", ops: func(s *pathStack) { - s.push("field_a", 1, false, -1) + s.push(TypeKind_Struct, "field_a", 1) }, expected: "$.field_a", }, { name: "nested fields", ops: func(s *pathStack) { - s.push("field_d", 4, false, -1) - s.push("field_a", 1, false, -1) + 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("field_c", 3, false, -1) - s.push("my_key", 0, true, -1) + 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("field_b", 2, false, -1) - s.push("", 0, false, 0) + 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("root_field", 1, false, -1) - s.push("map_field", 3, false, -1) - s.push("key1", 0, true, -1) - s.push("nested", 4, false, -1) - s.push("array", 2, false, -1) - s.push("", 0, false, 2) + 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("field_a", 1, false, -1) - s.push("field_b", 2, false, -1) + s.push(TypeKind_Struct, "field_a", 1) + s.push(TypeKind_Struct, "field_b", 2) s.pop() }, expected: "$.field_a", @@ -1837,7 +2469,7 @@ func TestStackFramePool(t *testing.T) { } // Use it and return it - *frames1 = append(*frames1, stackFrame{fieldName: "test", fieldID: 1}) + *frames1 = append(*frames1, stackFrame{name: "test", id: 1}) putStackFrames(frames1) // Get another one - should be reused @@ -2253,7 +2885,7 @@ func BenchmarkPathStack_Operations(b *testing.B) { for i := 0; i < b.N; i++ { stack := getStackFrames() for j := 0; j < 10; j++ { - stack.push("field", j, false, -1) + stack.push(TypeKind_Struct, "field", j) } for j := 0; j < 10; j++ { stack.pop() @@ -2264,8 +2896,8 @@ func BenchmarkPathStack_Operations(b *testing.B) { b.Run("build_path_shallow", func(b *testing.B) { stack := getStackFrames() - stack.push("field_a", 1, false, -1) - stack.push("field_b", 2, false, -1) + stack.push(TypeKind_Struct, "field_a", 1) + stack.push(TypeKind_Struct, "field_b", 2) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -2276,7 +2908,7 @@ func BenchmarkPathStack_Operations(b *testing.B) { b.Run("build_path_deep", func(b *testing.B) { stack := getStackFrames() for i := 0; i < 10; i++ { - stack.push("field", i, false, -1) + stack.push(TypeKind_Struct, "field", i) } b.ResetTimer() @@ -2287,12 +2919,12 @@ func BenchmarkPathStack_Operations(b *testing.B) { b.Run("build_path_mixed", func(b *testing.B) { stack := getStackFrames() - stack.push("root", 1, false, -1) - stack.push("map_field", 3, false, -1) - stack.push("key1", 0, true, -1) - stack.push("nested", 4, false, -1) - stack.push("", 0, false, 5) - stack.push("deep", 6, false, -1) + 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++ { @@ -2307,7 +2939,7 @@ func BenchmarkStackFramePool(b *testing.B) { for i := 0; i < b.N; i++ { frames := getStackFrames() for j := 0; j < 16; j++ { - *frames = append(*frames, stackFrame{fieldName: "test", fieldID: j}) + *frames = append(*frames, stackFrame{name: "test", id: j}) } putStackFrames(frames) } @@ -2317,7 +2949,7 @@ func BenchmarkStackFramePool(b *testing.B) { for i := 0; i < b.N; i++ { frames := make([]stackFrame, 0, 16) for j := 0; j < 16; j++ { - frames = append(frames, stackFrame{fieldName: "test", fieldID: j}) + frames = append(frames, stackFrame{name: "test", id: j}) } _ = frames } diff --git a/trim/desc.go b/trim/desc.go index 8a523768..734a7e44 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -19,7 +19,9 @@ package trim import ( "encoding/json" "fmt" + "strconv" "strings" + "sync" "sync/atomic" ) @@ -33,6 +35,8 @@ const ( 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. @@ -48,6 +52,7 @@ type Descriptor struct { // 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 @@ -294,3 +299,79 @@ func (d *Descriptor) resolveRefs(path string, refs map[string]*Descriptor) { } } } + +// 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/fetch.go b/trim/fetch.go index 93bed039..f8276969 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -139,6 +139,9 @@ func fetchValue(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pat case TypeKind_StrMap: return fetchStrMap(desc, v, opt, stack) + case TypeKind_List: + return fetchList(desc, v, opt, stack) + default: return v.Interface(), nil } @@ -180,7 +183,7 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa fieldValue := v.Field(fieldIdx) if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { if opt.DisallowNotFound { - stack.push(field.Name, field.ID, false, -1) + 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)} @@ -189,7 +192,7 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa } // Push field onto stack - stack.push(field.Name, field.ID, false, -1) + stack.push(TypeKind_Struct, field.Name, field.ID) // If field has a child descriptor, recursively fetch var err error @@ -217,13 +220,13 @@ func fetchStruct(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa // (e.g., map[FieldID]interface{} -> map[string]interface{} for nested structs) result[field.Name] = fetchUnknownValue(val, field.Desc) } else if opt.DisallowNotFound { - stack.push(field.Name, field.ID, false, -1) + 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(field.Name, field.ID, false, -1) + 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)} @@ -315,6 +318,34 @@ func fetchUnknownValue(val interface{}, desc *Descriptor) interface{} { } 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 } @@ -343,7 +374,7 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa } // Push map key onto stack - stack.push(keyStr, 0, true, -1) + stack.push(TypeKind_StrMap, keyStr, 0) var err error if wildcardDesc != nil { @@ -374,7 +405,7 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa // Check if specific keys are requested but not available in the map if !val.IsValid() { if opt.DisallowNotFound { - stack.push(key, 0, true, -1) + 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)} @@ -388,7 +419,7 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa } // Push map key onto stack - stack.push(key, 0, true, -1) + stack.push(TypeKind_StrMap, key, 0) var err error if child.Desc != nil { @@ -411,3 +442,98 @@ func fetchStrMap(desc *Descriptor, v reflect.Value, opt *FetchOptions, stack *pa 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 index 9f0861ee..b18285bc 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -116,6 +116,17 @@ func makeDesc(width int, depth int, withE bool) *Descriptor { }, } desc.Children[3].Desc = nd + // field_list is TypeKind_List with wildcard (all elements) + desc.Children[4].Desc = &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "*", // all elements + // Desc is nil, meaning list elements are scalar (int) + }, + }, + } return desc } @@ -130,7 +141,7 @@ func makeSampleAny(width int, depth int) interface{} { "field_a": int(1), "field_b": []*sampleFetch{}, "field_c": map[string]interface{}{}, - "field_list": []int{1, 2, 3}, + "field_list": []interface{}{int(1), int(2), int(3)}, "field_map": map[string]int{ "1": 1, "2": 2, @@ -164,6 +175,253 @@ func TestFetchAny(t *testing.T) { } } +// 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, + Name: "SampleFetch", + Children: []Field{ + { + Name: "field_a", + ID: 1, + }, + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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, + Name: "SampleFetch", + Children: []Field{ + { + Name: "field_list", + ID: 6, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "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}} + _, 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.Name != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) + } + }) + + // 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, + Name: "SampleFetch", + Children: []Field{ + { + Name: "field_b", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "LIST", + Children: []Field{ + { + Name: "0", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleFetch", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + { + Name: "2", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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 From 3d167ae13fb053f253e4c75c7cdaccae0d2cad5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Wed, 17 Dec 2025 13:53:17 +0800 Subject: [PATCH 11/17] feat: suport `AssignValue` and `XXX_NoUnkeyedLiteral` --- trim/all_test.go | 4 +- trim/assign.go | 395 ++--- trim/assign_test.go | 3819 +++++++++++++++++++++++++------------------ trim/desc.go | 6 +- trim/desc_test.go | 6 +- trim/fetch.go | 13 +- 6 files changed, 2334 insertions(+), 1909 deletions(-) diff --git a/trim/all_test.go b/trim/all_test.go index 41f1ee2c..64354a63 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -1058,7 +1058,7 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { // TestDescriptor_String_Scalar tests String() for scalar type descriptor func TestDescriptor_String_Scalar(t *testing.T) { desc := &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "ScalarType", } @@ -1302,7 +1302,7 @@ func TestDescriptor_String_MixedTypes(t *testing.T) { Name: "scalar_desc", ID: 4, Desc: &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "ScalarType", }, }, diff --git a/trim/assign.go b/trim/assign.go index d16128ae..03af980d 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -38,8 +38,10 @@ type Assigner struct { } // 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, they will be encoded -// to XXX_unrecognized field using protobuf binary encoding. +// 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 func (a Assigner) AssignAny(desc *Descriptor, src interface{}, dest interface{}) error { if src == nil || dest == nil || desc == nil { return nil @@ -69,11 +71,34 @@ type pbStructFieldInfo struct { 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 { @@ -83,21 +108,28 @@ func getPBStructFieldInfo(t reflect.Type) *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, + 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 == "XXX_unrecognized" { + 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 @@ -161,7 +193,7 @@ func assignValue(desc *Descriptor, src interface{}, destValue reflect.Value, opt case TypeKind_List: return assignList(desc, src, destValue, opt, stack) default: - return assignScalar(src, destValue) + return assignLeaf(reflect.ValueOf(src), destValue) } } @@ -215,7 +247,7 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op err = assignValueToField(descField.Desc, value, fieldValue, opt, stack) } else { // Otherwise, assign the value directly - err = assignScalar(value, fieldValue) + err = assignLeaf(reflect.ValueOf(value), fieldValue) } // Pop field from stack @@ -262,6 +294,25 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op } } + // 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 } @@ -322,7 +373,7 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op } else if child, ok := keyDescMap[key]; ok && child.Desc != nil { err = assignValueToField(child.Desc, value, elemValue, opt, stack) } else { - err = assignScalar(value, elemValue) + err = assignLeaf(reflect.ValueOf(value), elemValue) } // Pop map key from stack @@ -375,7 +426,7 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt if wildcardDesc != nil { err = assignValueToField(wildcardDesc, srcSlice[i], elemValue, opt, stack) } else { - err = assignScalar(srcSlice[i], elemValue) + err = assignLeaf(reflect.ValueOf(srcSlice[i]), elemValue) } // Pop list index from stack @@ -415,7 +466,7 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt err = assignValue(wildcardDesc, srcSlice[i], elemValue, opt, stack) } } else { - err = assignScalar(srcSlice[i], elemValue) + err = assignLeaf(reflect.ValueOf(srcSlice[i]), elemValue) } // Pop list index from stack @@ -493,7 +544,7 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt if child.Desc != nil { err2 = assignValueToField(child.Desc, srcVal, elemValue, opt, stack) } else { - err2 = assignScalar(srcVal, elemValue) + err2 = assignLeaf(reflect.ValueOf(srcVal), elemValue) } // Pop list index from stack @@ -558,7 +609,7 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt err2 = assignValue(child.Desc, srcVal, elemValue, opt, stack) } } else { - err2 = assignScalar(srcVal, elemValue) + err2 = assignLeaf(reflect.ValueOf(srcVal), elemValue) } // Pop list index from stack @@ -577,6 +628,8 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt 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 @@ -596,6 +649,12 @@ func getJSONStructFieldInfo(t reflect.Type) *jsonStructFieldInfo { 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 @@ -618,95 +677,19 @@ func getJSONStructFieldInfo(t reflect.Type) *jsonStructFieldInfo { return actual.(*jsonStructFieldInfo) } -// assignScalar assigns a scalar value to destValue -func assignScalar(src interface{}, destValue reflect.Value) error { - if src == nil { - return nil - } - - srcValue := reflect.ValueOf(src) - - // Handle pointer destination - if destValue.Kind() == reflect.Ptr { - if destValue.IsNil() { - destValue.Set(reflect.New(destValue.Type().Elem())) - } - destValue = destValue.Elem() - } - - // Dereference pointer source - for srcValue.Kind() == reflect.Ptr { - if srcValue.IsNil() { - return nil - } - srcValue = srcValue.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())) +// 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 } - // 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 := src.(map[string]interface{}) - if ok { - return assignMapToStruct(srcMapIface, destValue) - } - } + destValue := reflect.ValueOf(dest) - // Handle special cases for numeric type conversions - switch destValue.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if v, ok := toInt64(src); ok { - destValue.SetInt(v) - return nil - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if v, ok := toUint64(src); ok { - destValue.SetUint(v) - return nil - } - case reflect.Float32, reflect.Float64: - if v, ok := toFloat64(src); ok { - destValue.SetFloat(v) - return nil - } - case reflect.String: - if v, ok := src.(string); ok { - destValue.SetString(v) - return nil - } - case reflect.Bool: - if v, ok := src.(bool); ok { - destValue.SetBool(v) - return nil - } + if destValue.Kind() != reflect.Ptr { + return fmt.Errorf("dest must be a pointer") } - return fmt.Errorf("cannot assign %T to %v", src, destValue.Type()) + return assignLeaf(reflect.ValueOf(src), destValue) } // assignStructToStruct assigns a struct to another struct by matching json tags @@ -741,11 +724,32 @@ func assignStructToStruct(srcValue, destValue reflect.Value) error { } // Recursively assign the field value - if err := assignScalarValue(srcField, destField); err != nil { + 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 } @@ -777,7 +781,7 @@ func assignMapToStruct(srcMap map[string]interface{}, destValue reflect.Value) e } // Recursively assign the field value - if err := assignScalar(srcVal, destField); err != nil { + if err := assignLeaf(reflect.ValueOf(srcVal), destField); err != nil { return err } } @@ -785,16 +789,8 @@ func assignMapToStruct(srcMap map[string]interface{}, destValue reflect.Value) e return nil } -// assignScalarValue assigns a reflect.Value to another reflect.Value -func assignScalarValue(srcValue, destValue reflect.Value) error { - // Handle pointer destination - if destValue.Kind() == reflect.Ptr { - if destValue.IsNil() { - destValue.Set(reflect.New(destValue.Type().Elem())) - } - destValue = destValue.Elem() - } - +// 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() { @@ -803,14 +799,12 @@ func assignScalarValue(srcValue, destValue reflect.Value) error { srcValue = srcValue.Elem() } - // Handle interface{} source - extract the underlying value - if srcValue.Kind() == reflect.Interface { - if srcValue.IsNil() { - return nil + // Handle pointer destination + if destValue.Kind() == reflect.Ptr { + if destValue.IsNil() { + destValue.Set(reflect.New(destValue.Type().Elem())) } - // Get the underlying value and use assignScalar instead - // because the underlying value could be map[string]interface{} - return assignScalar(srcValue.Interface(), destValue) + destValue = destValue.Elem() } // Try direct assignment first @@ -825,6 +819,16 @@ func assignScalarValue(srcValue, destValue reflect.Value) error { 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) @@ -840,56 +844,11 @@ func assignScalarValue(srcValue, destValue reflect.Value) error { return assignMapToMap(srcValue, destValue) } - // Handle numeric conversions - switch destValue.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if srcValue.CanInt() { - destValue.SetInt(srcValue.Int()) - return nil - } - if srcValue.CanUint() { - destValue.SetInt(int64(srcValue.Uint())) - return nil - } - if srcValue.CanFloat() { - destValue.SetInt(int64(srcValue.Float())) - return nil - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if srcValue.CanUint() { - destValue.SetUint(srcValue.Uint()) - return nil - } - if srcValue.CanInt() { - destValue.SetUint(uint64(srcValue.Int())) - return nil - } - if srcValue.CanFloat() { - destValue.SetUint(uint64(srcValue.Float())) - return nil - } - case reflect.Float32, reflect.Float64: - if srcValue.CanFloat() { - destValue.SetFloat(srcValue.Float()) - return nil - } - if srcValue.CanInt() { - destValue.SetFloat(float64(srcValue.Int())) - return nil - } - if srcValue.CanUint() { - destValue.SetFloat(float64(srcValue.Uint())) - return nil - } - case reflect.String: - if srcValue.Kind() == reflect.String { - destValue.SetString(srcValue.String()) - return nil - } - case reflect.Bool: - if srcValue.Kind() == reflect.Bool { - destValue.SetBool(srcValue.Bool()) - return nil + // 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) } } @@ -925,12 +884,12 @@ func assignSliceToSlice(srcValue, destValue reflect.Value) error { continue } newElem := reflect.New(destElemType.Elem()) - if err := assignScalarValue(srcElem, newElem.Elem()); err != nil { + if err := assignLeaf(srcElem, newElem.Elem()); err != nil { return err } destElem.Set(newElem) } else { - if err := assignScalarValue(srcElem, destElem); err != nil { + if err := assignLeaf(srcElem, destElem); err != nil { return err } } @@ -984,12 +943,12 @@ func assignMapToMap(srcValue, destValue reflect.Value) error { continue } newElem := reflect.New(destElemType.Elem()) - if err := assignScalarValue(srcVal, newElem.Elem()); err != nil { + if err := assignLeaf(srcVal, newElem.Elem()); err != nil { return err } destVal.Set(newElem) } else { - if err := assignScalarValue(srcVal, destVal); err != nil { + if err := assignLeaf(srcVal, destVal); err != nil { return err } } @@ -1001,102 +960,6 @@ func assignMapToMap(srcValue, destValue reflect.Value) error { return nil } -// toInt64 converts various numeric types to int64 -func toInt64(v interface{}) (int64, bool) { - switch n := v.(type) { - case int: - return int64(n), true - case int8: - return int64(n), true - case int16: - return int64(n), true - case int32: - return int64(n), true - case int64: - return n, true - case uint: - return int64(n), true - case uint8: - return int64(n), true - case uint16: - return int64(n), true - case uint32: - return int64(n), true - case uint64: - return int64(n), true - case float32: - return int64(n), true - case float64: - return int64(n), true - default: - return 0, false - } -} - -// toUint64 converts various numeric types to uint64 -func toUint64(v interface{}) (uint64, bool) { - switch n := v.(type) { - case int: - return uint64(n), true - case int8: - return uint64(n), true - case int16: - return uint64(n), true - case int32: - return uint64(n), true - case int64: - return uint64(n), true - case uint: - return uint64(n), true - case uint8: - return uint64(n), true - case uint16: - return uint64(n), true - case uint32: - return uint64(n), true - case uint64: - return n, true - case float32: - return uint64(n), true - case float64: - return uint64(n), true - default: - return 0, false - } -} - -// toFloat64 converts various numeric types to float64 -func toFloat64(v interface{}) (float64, bool) { - switch n := v.(type) { - case int: - return float64(n), true - case int8: - return float64(n), true - case int16: - return float64(n), true - case int32: - return float64(n), true - case int64: - return float64(n), true - case uint: - return float64(n), true - case uint8: - return float64(n), true - case uint16: - return float64(n), true - case uint32: - return float64(n), true - case uint64: - return float64(n), true - case float32: - return float64(n), true - case float64: - return n, true - default: - return 0, false - } -} - // encodeUnknownField encodes a field value to protobuf binary format func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{}) error { if value == nil { diff --git a/trim/assign_test.go b/trim/assign_test.go index 71d25d6f..4715317b 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -19,9 +19,15 @@ package trim import ( "fmt" "reflect" + "strings" "testing" + "github.com/bytedance/sonic" "github.com/cloudwego/dynamicgo/proto/binary" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" ) func assignAny(desc *Descriptor, src interface{}, dest interface{}) error { @@ -30,14 +36,15 @@ func assignAny(desc *Descriptor, src interface{}, dest interface{}) error { } type sampleAssign struct { - 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:"-"` + 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 { @@ -66,9 +73,10 @@ func makeSampleAssign(width, depth int) *sampleAssign { // sampleAssignSmall is a struct with fewer fields than SampleAssign // Used to test XXX_unrecognized field encoding type sampleAssignSmall struct { - FieldA *int `protobuf:"varint,1,req,name=field_a"` - FieldE string `protobuf:"bytes,5,opt,name=field_e"` - XXX_unrecognized []byte `json:"-"` + 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 { @@ -166,7 +174,7 @@ func TestAssignAny_List(t *testing.T) { Name: "field_list", ID: 6, Desc: &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "LIST", }, }, @@ -223,10 +231,13 @@ func TestAssignAny_Map(t *testing.T) { } } +// TestAssignAny_UnknownFields tests that the converted sample +// with XXX_unrecognized can be correctly serialized and deserialized func TestAssignAny_UnknownFields(t *testing.T) { - // Source has fields that don't exist in destination struct + // 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 } @@ -236,8 +247,147 @@ func TestAssignAny_UnknownFields(t *testing.T) { Name: "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) +} + +// 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, + Name: "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}, }, } @@ -247,64 +397,52 @@ func TestAssignAny_UnknownFields(t *testing.T) { t.Fatalf("AssignAny failed: %v", err) } - // Check known field + // 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) + } - // Check that XXX_unrecognized has data + // Verify XXX_unrecognized is set (protobuf binary encoded) if len(dest.XXX_unrecognized) == 0 { - t.Fatalf("XXX_unrecognized: expected non-empty bytes") + t.Errorf("XXX_unrecognized: expected non-empty, got empty") } - // Decode the unknown fields - bp := binary.NewBinaryProtol(dest.XXX_unrecognized) - defer binary.FreeBinaryProtocol(bp) - - foundInt := false - foundStr := false - - for bp.Read < len(bp.Buf) { - fieldNum, wireType, _, err := bp.ConsumeTag() - if err != nil { - t.Fatalf("failed to consume tag: %v", err) - } + // Verify XXX_NoUnkeyedLiteral contains the raw unknown field values + if dest.XXX_NoUnkeyedLiteral == nil { + t.Fatalf("XXX_NoUnkeyedLiteral: expected non-nil map") + } - switch fieldNum { - case 10: - // unknown_int - val, err := bp.ReadInt64() - if err != nil { - t.Fatalf("failed to read int64: %v", err) - } - if val != 100 { - t.Errorf("unknown_int: expected 100, got %v", val) - } - foundInt = true - case 11: - // unknown_str - if wireType != 2 { // length-delimited - t.Errorf("unknown_str: expected wire type 2, got %v", wireType) - } - val, err := bp.ReadString(true) - if err != nil { - t.Fatalf("failed to read string: %v", err) - } - if val != "secret" { - t.Errorf("unknown_str: expected 'secret', got %v", val) - } - foundStr = true - default: - t.Errorf("unexpected field number: %v", fieldNum) - } + // 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) } - if !foundInt { - t.Errorf("unknown_int field not found in XXX_unrecognized") + // 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) } - if !foundStr { - t.Errorf("unknown_str field not found in XXX_unrecognized") + + // 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) { @@ -339,7 +477,7 @@ func TestAssignAny_ListOfStructs(t *testing.T) { Name: "field_b", ID: 2, Desc: &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "LIST", }, }, @@ -1110,627 +1248,381 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { } } -func BenchmarkassignAny(b *testing.B) { - src := map[string]interface{}{ - "field_a": 42, - "field_e": "hello", - "field_list": []interface{}{1, 2, 3, 4, 5}, - "field_map": map[string]interface{}{ - "key1": 100, - "key2": 200, - }, - } +// ===================== 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, - Name: "SampleAssign", + Name: "CircularNode", Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - { - Name: "field_list", - ID: 6, - Desc: &Descriptor{ - Kind: TypeKind_Scalar, - Name: "LIST", - }, - }, - { - Name: "field_map", - ID: 7, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{{Name: "*"}}, - }, - }, + {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 +} - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) +// makeCircularAssignTreeDesc creates a tree descriptor that references itself +func makeCircularAssignTreeDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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 BenchmarkAssignAny_WithUnknownFields(b *testing.B) { +func TestAssignAny_CircularDescriptor_LinkedList(t *testing.T) { + // Create source data: linked list 1 -> 2 -> 3 -> nil src := map[string]interface{}{ - "field_a": 42, - "field_e": "hello", - "unknown1": 100, - "unknown2": "secret", - "unknown3": 3.14, + "value": 1, + "next": map[string]interface{}{ + "value": 2, + "next": map[string]interface{}{ + "value": 3, + // next is nil (absent) + }, + }, } - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "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}, - }, + desc := makeCircularAssignDesc() + + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - dest := &sampleAssignSmall{} - _ = assignAny(desc, src, dest) + // Verify the assigned structure + if dest.Value != 1 { + t.Errorf("value: expected 1, got %v", dest.Value) } -} -// SourceStruct is used for struct-to-struct assignment tests via json tag matching -type SourceStruct struct { - Name string `json:"name"` - Age int `json:"age"` - Score float64 `json:"score"` - Active bool `json:"active"` -} + 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) + } -// DestStruct has same json tags but different Go field names -type DestStruct struct { - UserName string `json:"name"` - UserAge int `json:"age"` - UserScore float64 `json:"score"` - IsActive bool `json:"active"` -} + 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) + } -// NestedSourceStruct contains nested struct -type NestedSourceStruct struct { - ID int `json:"id"` - Data *SourceStruct `json:"data"` + // 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) + } } -// NestedDestStruct contains nested struct with different types -type NestedDestStruct struct { - ID int `json:"id"` - Data *DestStruct `json:"data"` -} +func TestAssignAny_CircularDescriptor_SingleNode(t *testing.T) { + // Single node with nil next + src := map[string]interface{}{ + "value": 42, + // next is not present (nil) + } -// ListSourceStruct contains a list of structs -type ListSourceStruct struct { - Items []*SourceStruct `json:"items"` -} + desc := makeCircularAssignDesc() -// ListDestStruct contains a list of different struct types -type ListDestStruct struct { - Items []*DestStruct `json:"items"` -} + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } -// MapSourceStruct contains a map of structs -type MapSourceStruct struct { - Data map[string]*SourceStruct `json:"data"` -} + if dest.Value != 42 { + t.Errorf("value: expected 42, got %v", dest.Value) + } -// MapDestStruct contains a map of different struct types -type MapDestStruct struct { - Data map[string]*DestStruct `json:"data"` + if dest.Next != nil { + t.Errorf("next: expected nil, got %v", dest.Next) + } } -func TestAssignScalar_StructToStruct(t *testing.T) { - t.Run("basic struct to struct via json tag", func(t *testing.T) { - src := &SourceStruct{ - Name: "Alice", - Age: 30, - Score: 95.5, - Active: true, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "data", ID: 1}, +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, + }, + } - type Wrapper struct { - Data *DestStruct `protobuf:"bytes,1,opt,name=data" json:"data"` - } - - srcMap := map[string]interface{}{ - "data": src, - } - - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } - - if dest.Data == nil { - t.Fatalf("dest.Data should not be nil") - } - if dest.Data.UserName != "Alice" { - t.Errorf("UserName: expected 'Alice', got '%s'", dest.Data.UserName) - } - if dest.Data.UserAge != 30 { - t.Errorf("UserAge: expected 30, got %d", dest.Data.UserAge) - } - if dest.Data.UserScore != 95.5 { - t.Errorf("UserScore: expected 95.5, got %f", dest.Data.UserScore) - } - if dest.Data.IsActive != true { - t.Errorf("IsActive: expected true, got %v", dest.Data.IsActive) - } - }) - - t.Run("nested struct to struct via json tag", func(t *testing.T) { - src := &NestedSourceStruct{ - ID: 100, - Data: &SourceStruct{ - Name: "Bob", - Age: 25, - Score: 88.0, - Active: false, - }, - } + desc := makeCircularAssignTreeDesc() - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "nested", ID: 1}, - }, - } + dest := &circularAssignTree{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } - type Wrapper struct { - Nested *NestedDestStruct `protobuf:"bytes,1,opt,name=nested" json:"nested"` - } + // Verify root + if dest.Value != 1 { + t.Errorf("value: expected 1, got %v", dest.Value) + } - srcMap := map[string]interface{}{ - "nested": src, - } + // 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) + } - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } + 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) + } - if dest.Nested == nil { - t.Fatalf("dest.Nested should not be nil") - } - if dest.Nested.ID != 100 { - t.Errorf("ID: expected 100, got %d", dest.Nested.ID) - } - if dest.Nested.Data == nil { - t.Fatalf("dest.Nested.Data should not be nil") - } - if dest.Nested.Data.UserName != "Bob" { - t.Errorf("UserName: expected 'Bob', got '%s'", dest.Nested.Data.UserName) - } - if dest.Nested.Data.UserAge != 25 { - t.Errorf("UserAge: expected 25, got %d", dest.Nested.Data.UserAge) - } - }) + // 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 TestAssignScalar_SliceToSlice(t *testing.T) { - t.Run("slice of structs via json tag", func(t *testing.T) { - src := &ListSourceStruct{ - Items: []*SourceStruct{ - {Name: "Alice", Age: 30, Score: 95.5, Active: true}, - {Name: "Bob", Age: 25, Score: 88.0, Active: false}, - }, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "list", ID: 1}, - }, - } +func TestAssignAny_CircularDescriptor_NilSrc(t *testing.T) { + desc := makeCircularAssignDesc() - type Wrapper struct { - List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` - } + dest := &circularAssignNode{Value: 999} - srcMap := map[string]interface{}{ - "list": src, - } + // Assign with nil src should not modify dest + err := assignAny(desc, nil, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } - dest := &Wrapper{} - err := assignAny(desc, srcMap, 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) + } +} - if dest.List == nil { - t.Fatalf("dest.List should not be nil") - } - if len(dest.List.Items) != 2 { - t.Fatalf("Items length: expected 2, got %d", len(dest.List.Items)) - } - if dest.List.Items[0].UserName != "Alice" { - t.Errorf("Items[0].UserName: expected 'Alice', got '%s'", dest.List.Items[0].UserName) +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 dest.List.Items[1].UserName != "Bob" { - t.Errorf("Items[1].UserName: expected 'Bob', got '%s'", dest.List.Items[1].UserName) + if src != nil { + node["next"] = src } - }) + src = node + } - t.Run("slice of primitives", func(t *testing.T) { - src := []int32{1, 2, 3, 4, 5} + desc := makeCircularAssignDesc() - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "nums", ID: 1}, - }, - } + dest := &circularAssignNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } - type Wrapper struct { - Nums []int64 `protobuf:"bytes,1,opt,name=nums" json:"nums"` + // 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) } - - srcMap := map[string]interface{}{ - "nums": src, + 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") + } } + } +} - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } +// 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:"-"` +} - expected := []int64{1, 2, 3, 4, 5} - if !reflect.DeepEqual(dest.Nums, expected) { - t.Errorf("Nums: expected %v, got %v", expected, dest.Nums) - } - }) +func makeCircularAssignMapDesc() *Descriptor { + desc := &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "ChildrenMap", + Children: []Field{ + {Name: "*", Desc: desc}, // Wildcard with circular reference + }, + } + return desc } -func TestAssignScalar_MapToMap(t *testing.T) { - t.Run("map of structs via json tag", func(t *testing.T) { - src := &MapSourceStruct{ - Data: map[string]*SourceStruct{ - "user1": {Name: "Alice", Age: 30, Score: 95.5, Active: true}, - "user2": {Name: "Bob", Age: 25, Score: 88.0, Active: false}, +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 + }, + }, }, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "map_data", ID: 1}, + "child2": map[string]interface{}{ + "name": "child2", + // children is nil }, - } - - type Wrapper struct { - MapData *MapDestStruct `protobuf:"bytes,1,opt,name=map_data" json:"map_data"` - } - - srcMap := map[string]interface{}{ - "map_data": src, - } + }, + } - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } + desc := makeCircularAssignMapDesc() - if dest.MapData == nil { - t.Fatalf("dest.MapData should not be nil") - } - if len(dest.MapData.Data) != 2 { - t.Fatalf("Data length: expected 2, got %d", len(dest.MapData.Data)) - } - if dest.MapData.Data["user1"].UserName != "Alice" { - t.Errorf("Data['user1'].UserName: expected 'Alice', got '%s'", dest.MapData.Data["user1"].UserName) - } - if dest.MapData.Data["user2"].UserName != "Bob" { - t.Errorf("Data['user2'].UserName: expected 'Bob', got '%s'", dest.MapData.Data["user2"].UserName) - } - }) + dest := &circularAssignMapNode{} + err := assignAny(desc, src, dest) + if err != nil { + t.Fatalf("AssignAny failed: %v", err) + } - t.Run("map of primitives with type conversion", func(t *testing.T) { - src := map[string]int32{"a": 1, "b": 2, "c": 3} + // Verify root + if dest.Name != "root" { + t.Errorf("name: expected 'root', got %v", dest.Name) + } - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "data", ID: 1}, - }, - } + if dest.Children == nil { + t.Fatalf("children: expected non-nil") + } - type Wrapper struct { - Data map[string]int64 `protobuf:"bytes,1,opt,name=data" json:"data"` - } + 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) + } - srcMap := map[string]interface{}{ - "data": src, - } + if child1.Children == nil { + t.Fatalf("child1.children: expected non-nil") + } - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } + 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) + } - expected := map[string]int64{"a": 1, "b": 2, "c": 3} - if !reflect.DeepEqual(dest.Data, expected) { - t.Errorf("Data: expected %v, got %v", expected, dest.Data) - } - }) + 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) + } } -func TestAssignScalar_ComplexNested(t *testing.T) { - // Test complex nested structure similar to TestFetchAndAssign scenario - t.Run("complex nested with lists and maps", func(t *testing.T) { - type InnerSource struct { - Value int `json:"value"` - Label string `json:"label"` - } - - type OuterSource struct { - ID int `json:"id"` - Children []*InnerSource `json:"children"` - Mapping map[string]*InnerSource `json:"mapping"` - } - - type InnerDest struct { - Value int `json:"value"` - Label string `json:"label"` - } - - type OuterDest struct { - ID int `json:"id"` - Children []*InnerDest `json:"children"` - Mapping map[string]*InnerDest `json:"mapping"` - } - - src := &OuterSource{ - ID: 1, - Children: []*InnerSource{ - {Value: 10, Label: "first"}, - {Value: 20, Label: "second"}, - }, - Mapping: map[string]*InnerSource{ - "key1": {Value: 100, Label: "mapped1"}, - "key2": {Value: 200, Label: "mapped2"}, - }, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "data", ID: 1}, - }, - } - - type Wrapper struct { - Data *OuterDest `protobuf:"bytes,1,opt,name=data" json:"data"` - } - - srcMap := map[string]interface{}{ - "data": src, - } - - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } - - if dest.Data == nil { - t.Fatalf("dest.Data should not be nil") - } - if dest.Data.ID != 1 { - t.Errorf("ID: expected 1, got %d", dest.Data.ID) - } - if len(dest.Data.Children) != 2 { - t.Fatalf("Children length: expected 2, got %d", len(dest.Data.Children)) - } - if dest.Data.Children[0].Value != 10 { - t.Errorf("Children[0].Value: expected 10, got %d", dest.Data.Children[0].Value) - } - if dest.Data.Children[0].Label != "first" { - t.Errorf("Children[0].Label: expected 'first', got '%s'", dest.Data.Children[0].Label) - } - if len(dest.Data.Mapping) != 2 { - t.Fatalf("Mapping length: expected 2, got %d", len(dest.Data.Mapping)) - } - if dest.Data.Mapping["key1"].Value != 100 { - t.Errorf("Mapping['key1'].Value: expected 100, got %d", dest.Data.Mapping["key1"].Value) - } - }) -} - -func TestAssignScalar_NilHandling(t *testing.T) { - t.Run("nil pointer in source struct", func(t *testing.T) { - src := &NestedSourceStruct{ - ID: 100, - Data: nil, // nil pointer - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "nested", ID: 1}, - }, - } - - type Wrapper struct { - Nested *NestedDestStruct `protobuf:"bytes,1,opt,name=nested" json:"nested"` - } - - srcMap := map[string]interface{}{ - "nested": src, - } - - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } - - if dest.Nested == nil { - t.Fatalf("dest.Nested should not be nil") - } - if dest.Nested.ID != 100 { - t.Errorf("ID: expected 100, got %d", dest.Nested.ID) - } - if dest.Nested.Data != nil { - t.Errorf("Data: expected nil, got %v", dest.Nested.Data) - } - }) - - t.Run("nil slice in source struct", func(t *testing.T) { - src := &ListSourceStruct{ - Items: nil, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "list", ID: 1}, - }, - } - - type Wrapper struct { - List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` - } - - srcMap := map[string]interface{}{ - "list": src, - } - - dest := &Wrapper{} - err := assignAny(desc, srcMap, dest) - if err != nil { - t.Fatalf("AssignAny failed: %v", err) - } - - if dest.List == nil { - t.Fatalf("dest.List should not be nil") - } - if dest.List.Items != nil { - t.Errorf("Items: expected nil, got %v", dest.List.Items) - } - }) -} - -func BenchmarkAssignScalar_StructToStruct(b *testing.B) { - src := &SourceStruct{ - Name: "Alice", - Age: 30, - Score: 95.5, - Active: true, - } - - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "Wrapper", - Children: []Field{ - {Name: "data", ID: 1}, - }, - } - - type Wrapper struct { - Data *DestStruct `protobuf:"bytes,1,opt,name=data" json:"data"` - } - - srcMap := map[string]interface{}{ - "data": src, - } - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - dest := &Wrapper{} - _ = assignAny(desc, srcMap, dest) +// 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"` } -} -func BenchmarkAssignScalar_SliceOfStructs(b *testing.B) { - src := &ListSourceStruct{ - Items: []*SourceStruct{ - {Name: "Alice", Age: 30, Score: 95.5, Active: true}, - {Name: "Bob", Age: 25, Score: 88.0, Active: false}, - {Name: "Charlie", Age: 35, Score: 92.0, Active: true}, + srcList := &circularFetchNode{ + Value: 1, + Next: &circularFetchNode{ + Value: 2, + Next: &circularFetchNode{ + Value: 3, + Next: nil, + }, }, } - desc := &Descriptor{ + // Create circular descriptor for fetch + fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Wrapper", + Name: "CircularNode", Children: []Field{ - {Name: "list", ID: 1}, + {Name: "value", ID: 1}, + {Name: "next", ID: 2}, }, } + fetchDesc.Children[1].Desc = fetchDesc - type Wrapper struct { - List *ListDestStruct `protobuf:"bytes,1,opt,name=list" json:"list"` - } - - srcMap := map[string]interface{}{ - "list": src, - } - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - dest := &Wrapper{} - _ = assignAny(desc, srcMap, dest) + // Fetch + fetched, err := fetchAny(fetchDesc, srcList) + if err != nil { + t.Fatalf("FetchAny failed: %v", err) } -} - -// ===================== 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{ + // Create circular descriptor for assign (with different IDs if needed) + assignDesc := &Descriptor{ Kind: TypeKind_Struct, Name: "CircularNode", Children: []Field{ @@ -1738,404 +1630,27 @@ func makeCircularAssignDesc() *Descriptor { {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, - Name: "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() + assignDesc.Children[1].Desc = assignDesc + // Assign dest := &circularAssignNode{} - err := assignAny(desc, src, dest) + err = assignAny(assignDesc, fetched, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) } - // Verify the assigned structure + // Verify 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 == nil || dest.Next.Value != 2 { + t.Errorf("next.value: expected 2") } - if dest.Next.Next.Value != 3 { - t.Errorf("next.next.value: expected 3, got %v", dest.Next.Next.Value) + if dest.Next.Next == nil || dest.Next.Next.Value != 3 { + t.Errorf("next.next.value: expected 3") } - - // 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, - Name: "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, - Name: "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) - } -} - -func BenchmarkAssignAny_CircularDescriptor(b *testing.B) { - // Create a linked list of depth 10 as source - depth := 10 - 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() - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - dest := &circularAssignNode{} - _ = assignAny(desc, src, dest) - } -} - -// 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, - Name: "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, - Name: "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") + t.Errorf("next.next.next: expected nil") } } @@ -2225,15 +1740,111 @@ func TestAssignAny_PathTracking(t *testing.T) { }, }, }, - expectedErr: "not found missing_field at SampleAssign: field 'missing_field' not found in struct at path $.field_d.field_d.missing_field", + 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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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}} + 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, + Name: "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, + Name: "SampleAssign", + Children: []Field{ + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + errorSubstr: "expected map[string]interface{} for struct at $.field_d", }, { - name: "field not found in map", + name: "type error in map value", src: map[string]interface{}{ "field_c": map[string]interface{}{ - "key1": map[string]interface{}{ - "bad_field": 999, - }, + "key1": []int{1, 2, 3}, // Should be a struct map }, }, desc: &Descriptor{ @@ -2262,764 +1873,1704 @@ func TestAssignAny_PathTracking(t *testing.T) { }, }, }, - expectedErr: "not found bad_field at SampleAssign: field 'bad_field' not found in struct at path $.field_c[key1].bad_field", + 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, + }, + }, + }, + }, + }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dest := &sampleAssign{} - as := Assigner{AssignOptions{DisallowNotDefined: true}} - 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()) - } - }) + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + _ = assignAny(desc, src, dest) } } -// 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, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - }, - errorSubstr: "expected map[string]interface{} for struct at $", +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, + Name: "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}, }, - { - name: "type error in nested struct", - src: map[string]interface{}{ - "field_d": "not a map", // Should be map[string]interface{} + } + + 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", }, - desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, + "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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + }, }, }, }, }, }, - 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 + } + + 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "field_d", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + }, }, }, - desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - { - Name: "field_c", - ID: 3, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - }, + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + _ = 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, + Name: "SampleAssign", + Children: []Field{ + {Name: "field_a", ID: 1}, + {Name: "field_e", ID: 5}, + { + Name: "field_c", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "MAP", + Children: []Field{ + { + Name: "*", + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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) { + 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{} - 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()) - } - }) - } + _ = 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() + for i := 0; i < b.N; i++ { + dest := &sampleAssign{} + as := Assigner{AssignOptions{DisallowNotDefined: true}} + _ = as.AssignAny(desc, srcWithError, dest) + } + }) } -// 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) +// 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", + }, }, - 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) + ID: 42, + Simple: SimpleStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, }, - expected: "$.field_c[my_key]", - }, - { - name: "array index", - ops: func(s *pathStack) { - s.push(TypeKind_Struct, "field_b", 2) - s.push(TypeKind_List, "", 0) + PtrSimple: &SimpleStruct{ + Name: "Bob", + Age: 25, + Score: 88.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) + } + + 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, }, - 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() + } + + 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, }, - expected: "$.field_a", - }, - } + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - stack := getStackFrames() - defer putStackFrames(stack) + dest := NestedStruct{} - tt.ops(stack) - result := stack.buildPath() - if result != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, result) - } - }) - } + 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) + } + }) } -// 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)) - } +func TestAssignValue_Slices(t *testing.T) { + assigner := Assigner{} - // Use it and return it - *frames1 = append(*frames1, stackFrame{name: "test", id: 1}) - putStackFrames(frames1) + t.Run("int_slice", func(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + var dest []int - // Get another one - should be reused - frames2 := getStackFrames() - if len(*frames2) != 0 { - t.Errorf("expected frames to be reset, got length %d", len(*frames2)) - } + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // Return it - putStackFrames(frames2) -} + if !reflect.DeepEqual(dest, src) { + t.Errorf("slices not equal: got %v, want %v", dest, src) + } + }) -// 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 - }, - }, - }, - } + t.Run("string_slice", func(t *testing.T) { + src := []string{"a", "b", "c"} + var dest []string - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_c", - ID: 3, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - dest := &sampleAssign{} - as := Assigner{AssignOptions{DisallowNotDefined: true}} - err := as.AssignAny(desc, src, dest) - if err == nil { - t.Fatalf("expected error, got nil") - } + if !reflect.DeepEqual(dest, src) { + t.Errorf("slices not equal: got %v, want %v", dest, src) + } + }) - 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()) - } -} + t.Run("int_to_int64_slice", func(t *testing.T) { + src := []int{10, 20, 30} + var dest []int64 -// 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))) + 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 anyIndex(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true +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) } - } - return false + + 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) + } + }) } -// Benchmark tests for path tracking overhead +func TestAssignValue_MapToStruct(t *testing.T) { + assigner := Assigner{} -// BenchmarkAssignAny_SimpleStruct tests baseline performance on simple struct -func BenchmarkAssignAny_SimpleStruct(b *testing.B) { - src := map[string]interface{}{ - "field_a": 42, - "field_e": "hello", - } + t.Run("basic_map_to_struct", func(t *testing.T) { + src := map[string]interface{}{ + "name": "Alice", + "age": 30, + "score": 95.5, + } + dest := SimpleStruct{} - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - }, - } + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) - } + 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) + } + }) } -// 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", - }, - }, - } +func TestAssignValue_NestedStructs(t *testing.T) { + assigner := Assigner{} - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - }, - }, - }, - }, - }, + t.Run("nested_struct", func(t *testing.T) { + src := NestedStruct{ + ID: 1, + Simple: SimpleStruct{ + Name: "Alice", + Age: 30, + Score: 95.5, }, - }, - } + } + dest := NestedStruct{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) - } -} + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } -// 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", - }, - }, - } + 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) + } + }) - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_c", - ID: 3, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - }, - }, - }, - }, - }, + t.Run("nested_pointer_struct", func(t *testing.T) { + src := NestedStruct{ + ID: 2, + PtrSimple: &SimpleStruct{ + Name: "Bob", + Age: 25, }, - }, - } + } + dest := NestedStruct{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) - } -} + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } -// BenchmarkAssignAny_DeepNesting tests performance with deeply nested structures -func BenchmarkAssignAny_DeepNesting(b *testing.B) { - // Create a deeply nested structure - src := map[string]interface{}{ - "field_a": 1, - } - current := src - for i := 0; i < 10; i++ { - nested := map[string]interface{}{ - "field_a": i + 2, + if dest.PtrSimple == nil { + t.Fatal("PtrSimple is nil") } - current["field_d"] = nested - current = nested - } + if dest.PtrSimple.Name != "Bob" || dest.PtrSimple.Age != 25 { + t.Errorf("nested pointer struct not copied: %+v", dest.PtrSimple) + } + }) - // Create corresponding descriptor - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - } - currentDesc := desc - for i := 0; i < 10; i++ { - childDesc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, + 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, }, } - currentDesc.Children = append(currentDesc.Children, Field{ - Name: "field_d", - ID: 4, - Desc: childDesc, - }) - currentDesc = childDesc - } + dest := NestedStruct{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) - } -} + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } -// 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 - }, - } + 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) + } + }) - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - }, + 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{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - as := Assigner{AssignOptions{DisallowNotDefined: true}} - _ = as.AssignAny(desc, src, dest) - } + 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) + } + }) } -// BenchmarkAssignAny_ComplexMixed tests performance with complex mixed structure -func BenchmarkAssignAny_ComplexMixed(b *testing.B) { - src := map[string]interface{}{ - "field_a": 1, - "field_e": "root", - "field_b": []interface{}{ - map[string]interface{}{ - "field_a": 10, - "field_e": "list1", - }, - map[string]interface{}{ - "field_a": 20, - "field_e": "list2", - }, - }, - "field_c": map[string]interface{}{ - "key1": map[string]interface{}{ - "field_a": 100, - "field_d": map[string]interface{}{ - "field_a": 200, - "field_e": "deep", - }, - }, - "key2": map[string]interface{}{ - "field_a": 300, - }, - }, - "field_map": map[string]interface{}{ - "mk1": 1000, - "mk2": 2000, - }, - } +func TestAssignValue_ComplexNested(t *testing.T) { + assigner := Assigner{} - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - { - Name: "field_b", - ID: 2, - Desc: &Descriptor{ - Kind: TypeKind_Scalar, - Name: "LIST", - }, + 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}, }, - { - Name: "field_c", - ID: 3, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - { - Name: "field_d", - ID: 4, - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - }, - }, - }, - }, - }, - }, - }, + } + 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, }, }, - { - Name: "field_map", - ID: 7, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", + } + 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{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - _ = assignAny(desc, src, dest) - } + 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) + } + }) } -// 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) +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) } }) - 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) + t.Run("interface_wrapping", func(t *testing.T) { + var src interface{} = 42 + var dest int - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = stack.buildPath() + err := assigner.AssignValue(src, &dest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if dest != 42 { + t.Errorf("expected 42, got %d", dest) } }) - b.Run("build_path_deep", func(b *testing.B) { - stack := getStackFrames() - for i := 0; i < 10; i++ { - stack.push(TypeKind_Struct, "field", i) + 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) } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = stack.buildPath() + if str, ok := dest.(string); !ok || str != "hello" { + t.Errorf("expected 'hello', got %v", dest) } }) - 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) + t.Run("slice_with_interface_elements", func(t *testing.T) { + src := []interface{}{1, "two", 3.0} + var dest []interface{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = stack.buildPath() + 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) } }) } -// BenchmarkStackFramePool tests the performance of memory pool -func BenchmarkStackFramePool(b *testing.B) { - b.Run("with_pool", func(b *testing.B) { +// 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++ { - frames := getStackFrames() - for j := 0; j < 16; j++ { - *frames = append(*frames, stackFrame{name: "test", id: j}) - } - putStackFrames(frames) + dest := ComplexStruct{} + _ = assigner.AssignValue(src, &dest) } }) - b.Run("without_pool", func(b *testing.B) { + b.Run("JSON", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() for i := 0; i < b.N; i++ { - frames := make([]stackFrame, 0, 16) - for j := 0; j < 16; j++ { - frames = append(frames, stackFrame{name: "test", id: j}) + // 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) } - _ = frames } }) } -// 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, +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, + Name: "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] + }, + }, + }, }, - }, - } + } - desc := &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - {Name: "field_e", ID: 5}, - { - Name: "field_c", - ID: 3, - Desc: &Descriptor{ - Kind: TypeKind_StrMap, - Name: "MAP", - Children: []Field{ - { - Name: "*", - Desc: &Descriptor{ - Kind: TypeKind_Struct, - Name: "SampleAssign", - Children: []Field{ - {Name: "field_a", ID: 1}, - }, - }, + dest := &sampleAssignArray{} + assigner := &Assigner{} + 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{} + 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) + } + }) - 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) + 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}} + 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) } }) - 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, + t.Run("Array_NegativeIndex", func(t *testing.T) { + src := map[string]interface{}{ + "field_array": []interface{}{10}, } - b.ResetTimer() - for i := 0; i < b.N; i++ { - dest := &sampleAssign{} - as := Assigner{AssignOptions{DisallowNotDefined: true}} - _ = as.AssignAny(desc, srcWithError, dest) + 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}} + 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 index 734a7e44..3fe75e8f 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -29,8 +29,8 @@ import ( type TypeKind int const ( - // TypeKind_Scalar indicates Descriptor is a leaf node, its underlying type can be anything (event go struct/map/list) - TypeKind_Scalar TypeKind = iota + // 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 @@ -114,7 +114,7 @@ func (d *Descriptor) String() string { // Get type prefix based on Kind var typePrefix string switch desc.Kind { - case TypeKind_Scalar: + case TypeKind_Leaf: sb.WriteString("-") return case TypeKind_StrMap: diff --git a/trim/desc_test.go b/trim/desc_test.go index 1a30eb21..bb801148 100644 --- a/trim/desc_test.go +++ b/trim/desc_test.go @@ -33,7 +33,7 @@ func TestDescriptorMarshalJSON(t *testing.T) { Name: "field1", ID: 1, Desc: &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "Leaf1", }, }, @@ -48,7 +48,7 @@ func TestDescriptorMarshalJSON(t *testing.T) { Name: "key1", ID: 0, Desc: &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "Leaf2", }, }, @@ -189,7 +189,7 @@ func TestDescriptorMarshalJSONNil(t *testing.T) { func TestDescriptorMarshalJSONMultipleReferences(t *testing.T) { // Create a shared descriptor referenced by multiple fields shared := &Descriptor{ - Kind: TypeKind_Scalar, + Kind: TypeKind_Leaf, Name: "Shared", } diff --git a/trim/fetch.go b/trim/fetch.go index f8276969..69bc70c2 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -75,6 +75,17 @@ type structFieldInfo struct { // 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 { @@ -92,7 +103,7 @@ func getStructFieldInfo(t reflect.Type) *structFieldInfo { field := t.Field(i) // Check for _unknownFields field - if field.Name == "_unknownFields" { + if field.Name == thriftUnknownFieldName { info.unknownFieldsIndex = i continue } From 3d4fc2b4af53403f174c0edc977f80ccdc73fb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Wed, 17 Dec 2025 16:10:56 +0800 Subject: [PATCH 12/17] opt: pb unknowfield supports nested types --- trim/assign.go | 129 ++++++++---- trim/assign_test.go | 468 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 555 insertions(+), 42 deletions(-) diff --git a/trim/assign.go b/trim/assign.go index 03af980d..e6635c9c 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -278,7 +278,8 @@ func assignStruct(desc *Descriptor, src interface{}, destValue reflect.Value, op for key, val := range unassignedFields { // Encode this field to XXX_unrecognized - if err := encodeUnknownField(bp, descFieldMap[key].ID, val); err != nil { + 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) } } @@ -961,11 +962,89 @@ func assignMapToMap(srcValue, destValue reflect.Value) error { } // encodeUnknownField encodes a field value to protobuf binary format -func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{}) error { +// 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 @@ -1009,23 +1088,21 @@ func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{ bp.WriteBytes(v) case []interface{}: - // Encode list as repeated field + // Encode list as repeated field (without descriptor) for _, elem := range v { - if err := encodeUnknownField(bp, fieldID, elem); err != nil { + if err := encodeUnknownField(bp, fieldID, elem, nil); err != nil { return err } } case map[string]interface{}: - // Encode as embedded message - // First encode the message content + // Encode as embedded message (without descriptor) subBp := binary.NewBinaryProtocolBuffer() defer binary.FreeBinaryProtocol(subBp) for key, val := range v { - // For unknown map, we assume string keys with field ID based on some hash - // This is a simplified approach - in practice, you'd need proper field descriptors - if err := encodeUnknownField(subBp, hashFieldName(key), val); err != nil { + // For unknown map without descriptor, use hash-based field ID + if err := encodeUnknownField(subBp, hashFieldName(key), val, nil); err != nil { return err } } @@ -1034,39 +1111,7 @@ func encodeUnknownField(bp *binary.BinaryProtocol, fieldID int, value interface{ bp.WriteBytes(subBp.Buf) default: - // Try to use reflection for other types - rv := reflect.ValueOf(value) - switch rv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bp.Buf = appendTag(bp.Buf, fieldID, 0) - bp.WriteInt64(rv.Int()) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bp.Buf = appendTag(bp.Buf, fieldID, 0) - bp.WriteUint64(rv.Uint()) - case reflect.Float32: - bp.Buf = appendTag(bp.Buf, fieldID, 5) - bp.WriteFloat(float32(rv.Float())) - case reflect.Float64: - bp.Buf = appendTag(bp.Buf, fieldID, 1) - bp.WriteDouble(rv.Float()) - case reflect.String: - bp.Buf = appendTag(bp.Buf, fieldID, 2) - bp.WriteString(rv.String()) - case reflect.Slice: - if rv.Type().Elem().Kind() == reflect.Uint8 { - bp.Buf = appendTag(bp.Buf, fieldID, 2) - bp.WriteBytes(rv.Bytes()) - } else { - // Encode as repeated field - for i := 0; i < rv.Len(); i++ { - if err := encodeUnknownField(bp, fieldID, rv.Index(i).Interface()); err != nil { - return err - } - } - } - default: - return fmt.Errorf("unsupported type for unknown field encoding: %T", value) - } + return fmt.Errorf("unsupported type for unknown field encoding: %T", value) } return nil diff --git a/trim/assign_test.go b/trim/assign_test.go index 4715317b..311a01aa 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -26,6 +26,7 @@ import ( "github.com/cloudwego/dynamicgo/proto/binary" "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" ) @@ -367,6 +368,473 @@ func TestAssignAny_UnknownFields(t *testing.T) { 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, + Name: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_struct", + ID: 2, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_list", + ID: 3, + Desc: &Descriptor{ + Kind: TypeKind_List, + Name: "NestedList", + Children: []Field{ + { + Name: "*", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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, + Name: "SampleNestedUnknown", + Children: []Field{ + {Name: "field_a", ID: 1}, + { + Name: "nested_map", + ID: 4, + Desc: &Descriptor{ + Kind: TypeKind_StrMap, + Name: "NestedMap", + Children: []Field{ + { + Name: "*", + ID: 0, + Desc: &Descriptor{ + Kind: TypeKind_Struct, + Name: "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{}{ From 8afd24efc8a1244be0dff043533cb3a788554803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Tue, 23 Dec 2025 19:36:04 +0800 Subject: [PATCH 13/17] refactor: rename `Descriptor.Name` to `Descriptor.Type` --- trim/all_test.go | 80 ++++++++++---------- trim/assign_test.go | 178 ++++++++++++++++++++++---------------------- trim/desc.go | 18 ++--- trim/desc_test.go | 28 +++---- trim/fetch.go | 4 +- trim/fetch_test.go | 132 ++++++++++++++++---------------- 6 files changed, 220 insertions(+), 220 deletions(-) diff --git a/trim/all_test.go b/trim/all_test.go index 64354a63..9b14b7c5 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -107,7 +107,7 @@ func TestFetchAndAssign_UnknownToUnrecognized(t *testing.T) { // Note: these descriptor IDs are Thrift IDs, NOT protobuf IDs! desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + 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 @@ -133,7 +133,7 @@ func TestFetchAndAssign_UnknownToUnrecognized(t *testing.T) { // The key point: descriptor.ID here represents the TARGET protobuf field ID assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + 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 @@ -201,7 +201,7 @@ func TestFetchAndAssign_UnknownToKnown(t *testing.T) { // Descriptor for fetching (Thrift IDs) fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + Type: "TestStruct", Children: []Field{ {Name: "field_a", ID: 1}, // Thrift ID 1 {Name: "field_b", ID: 2}, // Thrift ID 2, in _unknownFields @@ -223,7 +223,7 @@ func TestFetchAndAssign_UnknownToKnown(t *testing.T) { // Descriptor for assigning (Protobuf IDs) assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + 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 @@ -263,7 +263,7 @@ func TestFetchAndAssign_KnownToUnrecognized(t *testing.T) { // Descriptor for fetching (Thrift IDs) fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + Type: "TestStruct", Children: []Field{ {Name: "field_a", ID: 1}, // Thrift ID 1 {Name: "field_b", ID: 2}, // Thrift ID 2 @@ -289,7 +289,7 @@ func TestFetchAndAssign_KnownToUnrecognized(t *testing.T) { // Note: IDs here are protobuf field IDs, different from thrift IDs! assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "TestStruct", + 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 @@ -375,7 +375,7 @@ func TestFetchAndAssign_MixedScenario(t *testing.T) { // Fetch descriptor (Thrift IDs) fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "MixedStruct", + Type: "MixedStruct", Children: []Field{ {Name: "field_a", ID: 1}, // known in thrift {Name: "field_b", ID: 2}, // in thrift _unknownFields @@ -398,7 +398,7 @@ func TestFetchAndAssign_MixedScenario(t *testing.T) { // Assign descriptor (Protobuf IDs) assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "MixedStruct", + Type: "MixedStruct", Children: []Field{ {Name: "field_a", ID: 10}, // known in pb {Name: "field_b", ID: 20}, // known in pb (from thrift _unknownFields) @@ -487,7 +487,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { // Fetch descriptor fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Outer", + Type: "Outer", Children: []Field{ {Name: "id", ID: 1}, { @@ -495,7 +495,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Inner", + Type: "Inner", Children: []Field{ {Name: "name", ID: 1}, {Name: "value", ID: 2}, @@ -521,7 +521,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { // Assign descriptor (Protobuf IDs) assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Outer", + Type: "Outer", Children: []Field{ {Name: "id", ID: 10}, { @@ -529,7 +529,7 @@ func TestFetchAndAssign_NestedStructWithUnknownFields(t *testing.T) { ID: 20, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Inner", + Type: "Inner", Children: []Field{ {Name: "name", ID: 1}, {Name: "value", ID: 2}, @@ -564,7 +564,7 @@ func TestFetchAndAssign_ShallowDescriptor(t *testing.T) { // Create a shallow descriptor (depth=1) - no nested Desc for Children shallowDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, // scalar field, no Desc needed {Name: "field_e", ID: 5}, // scalar field, no Desc needed @@ -642,7 +642,7 @@ func TestFetchAndAssign_MissingFieldsInDescriptor(t *testing.T) { // Descriptor only includes field_a, field_c, field_e (missing field_b, field_d) partialDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "PartialStruct", + Type: "PartialStruct", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_c", ID: 3}, @@ -728,7 +728,7 @@ func TestFetchAndAssign_NestedMissingFields(t *testing.T) { // Also missing outer.tag partialDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Outer", + Type: "Outer", Children: []Field{ {Name: "id", ID: 1}, { @@ -736,7 +736,7 @@ func TestFetchAndAssign_NestedMissingFields(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Inner", + Type: "Inner", Children: []Field{ {Name: "name", ID: 1}, // only fetch name, not value or extra }, @@ -823,7 +823,7 @@ func TestFetchAndAssign_DescShallowerThanNestedList(t *testing.T) { // Shallow descriptor - no Desc for items, so items are fetched as-is shallowDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Container", + Type: "Container", Children: []Field{ {Name: "items", ID: 1}, // No Desc, so list is fetched as raw value }, @@ -887,14 +887,14 @@ func TestFetchAndAssign_DescShallowerThanNestedMap(t *testing.T) { // Shallow descriptor - no Desc for map values shallowDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Container", + Type: "Container", Children: []Field{ { Name: "data", ID: 1, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "DataMap", + Type: "DataMap", Children: []Field{ {Name: "*"}, // Wildcard, but no Desc for values -> fetch as raw }, @@ -953,14 +953,14 @@ func TestFetchAndAssign_PartialMapKeys(t *testing.T) { // Descriptor only specifies key1 and key3 (not key2, key4) partialDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Container", + Type: "Container", Children: []Field{ { Name: "data", ID: 1, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "DataMap", + Type: "DataMap", Children: []Field{ {Name: "key1"}, {Name: "key3"}, @@ -1026,7 +1026,7 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { // Empty descriptor - no children emptyDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "EmptyStruct", + Type: "EmptyStruct", Children: []Field{}, } @@ -1059,7 +1059,7 @@ func TestFetchAndAssign_EmptyDescriptor(t *testing.T) { func TestDescriptor_String_Scalar(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Leaf, - Name: "ScalarType", + Type: "ScalarType", } result := desc.String() @@ -1070,7 +1070,7 @@ func TestDescriptor_String_Scalar(t *testing.T) { func TestDescriptor_String_EmptyStruct(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "EmptyStruct", + Type: "EmptyStruct", } result := desc.String() @@ -1081,7 +1081,7 @@ func TestDescriptor_String_EmptyStruct(t *testing.T) { func TestDescriptor_String_SimpleStruct(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SimpleStruct", + Type: "SimpleStruct", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_b", ID: 2}, @@ -1100,7 +1100,7 @@ func TestDescriptor_String_SimpleStruct(t *testing.T) { func TestDescriptor_String_NestedStruct(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "OuterStruct", + Type: "OuterStruct", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1108,7 +1108,7 @@ func TestDescriptor_String_NestedStruct(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "InnerStruct", + Type: "InnerStruct", Children: []Field{ {Name: "name", ID: 1}, {Name: "value", ID: 2}, @@ -1133,7 +1133,7 @@ func TestDescriptor_String_NestedStruct(t *testing.T) { func TestDescriptor_String_MapWithWildcard(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "ContainerStruct", + Type: "ContainerStruct", Children: []Field{ {Name: "id", ID: 1}, { @@ -1141,13 +1141,13 @@ func TestDescriptor_String_MapWithWildcard(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "DataMap", + Type: "DataMap", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "ItemStruct", + Type: "ItemStruct", Children: []Field{ {Name: "name", ID: 1}, }, @@ -1175,7 +1175,7 @@ func TestDescriptor_String_MapWithWildcard(t *testing.T) { func TestDescriptor_String_MapWithSpecificKeys(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_StrMap, - Name: "SpecificKeyMap", + Type: "SpecificKeyMap", Children: []Field{ {Name: "key1"}, {Name: "key2"}, @@ -1183,7 +1183,7 @@ func TestDescriptor_String_MapWithSpecificKeys(t *testing.T) { Name: "key3", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "NestedType", + Type: "NestedType", Children: []Field{ {Name: "value", ID: 1}, }, @@ -1207,21 +1207,21 @@ func TestDescriptor_String_MapWithSpecificKeys(t *testing.T) { func TestDescriptor_String_DeeplyNested(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Level1", + Type: "Level1", Children: []Field{ { Name: "level2", ID: 1, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Level2", + Type: "Level2", Children: []Field{ { Name: "level3", ID: 1, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Level3", + Type: "Level3", Children: []Field{ {Name: "value", ID: 1}, }, @@ -1249,7 +1249,7 @@ func TestDescriptor_String_CircularReference(t *testing.T) { // Create a descriptor that references itself desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SelfRef", + Type: "SelfRef", Children: []Field{ {Name: "value", ID: 1}, }, @@ -1273,7 +1273,7 @@ func TestDescriptor_String_CircularReference(t *testing.T) { func TestDescriptor_String_MixedTypes(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "MixedStruct", + Type: "MixedStruct", Children: []Field{ {Name: "scalar_field", ID: 1}, { @@ -1281,7 +1281,7 @@ func TestDescriptor_String_MixedTypes(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "NestedStruct", + Type: "NestedStruct", Children: []Field{ {Name: "a", ID: 1}, }, @@ -1292,7 +1292,7 @@ func TestDescriptor_String_MixedTypes(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "NestedMap", + Type: "NestedMap", Children: []Field{ {Name: "*"}, }, @@ -1303,7 +1303,7 @@ func TestDescriptor_String_MixedTypes(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Leaf, - Name: "ScalarType", + Type: "ScalarType", }, }, }, diff --git a/trim/assign_test.go b/trim/assign_test.go index 311a01aa..d50c25b2 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -92,7 +92,7 @@ func TestAssignAny_Basic(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -124,7 +124,7 @@ func TestAssignAny_NestedStruct(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -132,7 +132,7 @@ func TestAssignAny_NestedStruct(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -169,14 +169,14 @@ func TestAssignAny_List(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_Leaf, - Name: "LIST", + Type: "LIST", }, }, }, @@ -204,14 +204,14 @@ func TestAssignAny_Map(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_map", ID: 7, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ {Name: "*"}, }, @@ -245,7 +245,7 @@ func TestAssignAny_UnknownFields(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssignSmall", + Type: "SampleAssignSmall", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -387,7 +387,7 @@ func TestEncodeUnknownField_NestedStruct(t *testing.T) { // Create descriptor for the struct with nested struct field desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleNestedUnknown", + Type: "SampleNestedUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -395,7 +395,7 @@ func TestEncodeUnknownField_NestedStruct(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "NestedStruct", + Type: "NestedStruct", Children: []Field{ {Name: "inner_field1", ID: 1}, {Name: "inner_field2", ID: 2}, @@ -528,7 +528,7 @@ func TestEncodeUnknownField_NestedList(t *testing.T) { // Create descriptor for the struct with nested list field desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleNestedUnknown", + Type: "SampleNestedUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -536,14 +536,14 @@ func TestEncodeUnknownField_NestedList(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "NestedList", + Type: "NestedList", Children: []Field{ { Name: "*", ID: 0, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "ListItem", + Type: "ListItem", Children: []Field{ {Name: "item_field", ID: 1}, }, @@ -673,7 +673,7 @@ func TestEncodeUnknownField_NestedMap(t *testing.T) { // Create descriptor for the struct with nested map field desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleNestedUnknown", + Type: "SampleNestedUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -681,14 +681,14 @@ func TestEncodeUnknownField_NestedMap(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "NestedMap", + Type: "NestedMap", Children: []Field{ { Name: "*", ID: 0, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "MapValue", + Type: "MapValue", Children: []Field{ {Name: "value_field", ID: 1}, }, @@ -849,7 +849,7 @@ func TestAssignAny_NoUnkeyedLiteral(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssignSmall", + Type: "SampleAssignSmall", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -939,14 +939,14 @@ func TestAssignAny_ListOfStructs(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_Leaf, - Name: "LIST", + Type: "LIST", }, }, }, @@ -992,7 +992,7 @@ func TestAssignAny_MapOfStructs(t *testing.T) { nestedDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1001,14 +1001,14 @@ func TestAssignAny_MapOfStructs(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_c", ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ {Name: "*", Desc: nestedDesc}, }, @@ -1048,14 +1048,14 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "*"}, // wildcard - all elements }, @@ -1086,14 +1086,14 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "0", ID: 0}, // src[0] -> dest[0] {Name: "2", ID: 2}, // src[1] -> dest[2] @@ -1144,14 +1144,14 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "0", ID: 0}, // src[0] -> dest[0] {Name: "1", ID: 1}, // src[1] -> dest[1] @@ -1201,14 +1201,14 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "0", ID: 0}, // src[0] -> dest[0] {Name: "1", ID: 1}, // src[1] -> dest[1] @@ -1230,8 +1230,8 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { if !ok { t.Fatalf("expected ErrNotFound, got %T: %v", err, err) } - if notFoundErr.Parent.Name != "LIST" { - t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) + if notFoundErr.Parent.Type != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Type) } }) @@ -1256,20 +1256,20 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1325,21 +1325,21 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "0", ID: 0, // src[0] -> dest[0] Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1351,7 +1351,7 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { ID: 2, // src[1] -> dest[2] Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1421,21 +1421,21 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "1", ID: 1, // src[0] -> dest[1] Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1447,7 +1447,7 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { ID: 3, // src[1] -> dest[3] Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1537,21 +1537,21 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "5", ID: 5, // src[0] -> dest[5] Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -1616,20 +1616,20 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -1676,7 +1676,7 @@ func TestAssignAny_NilValues(t *testing.T) { t.Errorf("expected nil error for nil inputs, got %v", err) } - desc := &Descriptor{Kind: TypeKind_Struct, Name: "Test"} + desc := &Descriptor{Kind: TypeKind_Struct, Type: "Test"} dest := &sampleAssign{} err = assignAny(desc, nil, dest) @@ -1693,7 +1693,7 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, // nonexistent is not in descriptor @@ -1739,7 +1739,7 @@ type circularAssignTree struct { func makeCircularAssignDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularNode", + Type: "CircularNode", Children: []Field{ {Name: "value", ID: 1}, {Name: "next", ID: 2}, @@ -1754,7 +1754,7 @@ func makeCircularAssignDesc() *Descriptor { func makeCircularAssignTreeDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularTree", + Type: "CircularTree", Children: []Field{ {Name: "value", ID: 1}, {Name: "left", ID: 2}, @@ -1963,7 +1963,7 @@ type circularAssignMapNode struct { func makeCircularAssignMapDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularMapNode", + Type: "CircularMapNode", Children: []Field{ {Name: "name", ID: 1}, {Name: "children", ID: 2}, @@ -1972,7 +1972,7 @@ func makeCircularAssignMapDesc() *Descriptor { // Make children field circular: it's a map with values of the same type desc.Children[1].Desc = &Descriptor{ Kind: TypeKind_StrMap, - Name: "ChildrenMap", + Type: "ChildrenMap", Children: []Field{ {Name: "*", Desc: desc}, // Wildcard with circular reference }, @@ -2075,7 +2075,7 @@ func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { // Create circular descriptor for fetch fetchDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularNode", + Type: "CircularNode", Children: []Field{ {Name: "value", ID: 1}, {Name: "next", ID: 2}, @@ -2092,7 +2092,7 @@ func TestAssignAny_CircularDescriptor_FetchThenAssign(t *testing.T) { // Create circular descriptor for assign (with different IDs if needed) assignDesc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularNode", + Type: "CircularNode", Children: []Field{ {Name: "value", ID: 1}, {Name: "next", ID: 2}, @@ -2137,7 +2137,7 @@ func TestAssignAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2153,14 +2153,14 @@ func TestAssignAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_d", ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2182,14 +2182,14 @@ func TestAssignAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_d", ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2197,7 +2197,7 @@ func TestAssignAny_PathTracking(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2221,20 +2221,20 @@ func TestAssignAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_c", ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2277,7 +2277,7 @@ func TestAssignAny_PathTracking_TypeErrors(t *testing.T) { src: "not a map", // Should be map[string]interface{} desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2291,14 +2291,14 @@ func TestAssignAny_PathTracking_TypeErrors(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_d", ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2317,20 +2317,20 @@ func TestAssignAny_PathTracking_TypeErrors(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ { Name: "field_c", ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2483,7 +2483,7 @@ func TestAssignAny_PathTracking_Integration(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2491,13 +2491,13 @@ func TestAssignAny_PathTracking_Integration(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2505,7 +2505,7 @@ func TestAssignAny_PathTracking_Integration(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2566,7 +2566,7 @@ func BenchmarkAssignAny_NestedStruct(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2574,7 +2574,7 @@ func BenchmarkAssignAny_NestedStruct(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2582,7 +2582,7 @@ func BenchmarkAssignAny_NestedStruct(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -2613,7 +2613,7 @@ func BenchmarkAssignAny_WithUnknownFields(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssignSmall", + Type: "SampleAssignSmall", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -2653,7 +2653,7 @@ func BenchmarkAssignAny_WithMap(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2661,13 +2661,13 @@ func BenchmarkAssignAny_WithMap(b *testing.B) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -2699,7 +2699,7 @@ func BenchmarkAssignAny_ErrorPath(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -2707,7 +2707,7 @@ func BenchmarkAssignAny_ErrorPath(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -2795,7 +2795,7 @@ func BenchmarkPathTracking_Overhead(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -2804,13 +2804,13 @@ func BenchmarkPathTracking_Overhead(b *testing.B) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssign", + Type: "SampleAssign", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -3917,7 +3917,7 @@ func TestAssignAny_Array(t *testing.T) { // Assign 10 to index 0, 30 to index 2 desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleAssignArray", + Type: "SampleAssignArray", Children: []Field{ { Name: "field_array", diff --git a/trim/desc.go b/trim/desc.go index 3fe75e8f..acd46655 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -46,8 +46,8 @@ type Descriptor struct { // Based on this, we can decide how to manipulate the data (e.g. mapKey or strucField) Kind TypeKind - // Name of the type - Name string + // 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 "*" @@ -106,7 +106,7 @@ func (d *Descriptor) String() string { printer = func(desc *Descriptor, indent string) { // Handle circular references if visited[desc] { - sb.WriteString("<" + desc.Name + ">") + sb.WriteString("<" + desc.Type + ">") return } visited[desc] = true @@ -120,7 +120,7 @@ func (d *Descriptor) String() string { case TypeKind_StrMap: typePrefix = "" default: // TypeKind_Struct - typePrefix = "<" + desc.Name + ">" + typePrefix = "<" + desc.Type + ">" } sb.WriteString(typePrefix) @@ -205,7 +205,7 @@ func (d *Descriptor) marshalWithPath(path string, visited map[*Descriptor]string result := &descriptorJSON{ Kind: d.Kind, - Name: d.Name, + Name: d.Type, Children: make([]fieldJSON, 0, len(d.Children)), } @@ -252,7 +252,7 @@ func (d *Descriptor) UnmarshalJSON(data []byte) error { // unmarshalFromJSON populates the descriptor from JSON representation func (d *Descriptor) unmarshalFromJSON(raw *descriptorJSON, path string, refs map[string]*Descriptor) { d.Kind = raw.Kind - d.Name = raw.Name + d.Type = raw.Name d.Children = make([]Field, 0, len(raw.Children)) d.ids = nil d.names = nil @@ -270,7 +270,7 @@ func (d *Descriptor) unmarshalFromJSON(raw *descriptorJSON, path string, refs ma if fj.Ref != "" { // This is a reference, will be resolved later // Create a placeholder descriptor with special name - f.Desc = &Descriptor{Name: "$ref:" + fj.Ref} + f.Desc = &Descriptor{Type: "$ref:" + fj.Ref} } else if fj.Desc != nil { f.Desc = &Descriptor{} f.Desc.unmarshalFromJSON(fj.Desc, childPath, refs) @@ -287,8 +287,8 @@ func (d *Descriptor) resolveRefs(path string, refs map[string]*Descriptor) { childPath := fmt.Sprintf("%s.children[%d].desc", path, i) // Check if this is a reference - if strings.HasPrefix(d.Children[i].Desc.Name, "$ref:") { - refPath := strings.TrimPrefix(d.Children[i].Desc.Name, "$ref:") + 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 } diff --git a/trim/desc_test.go b/trim/desc_test.go index bb801148..24437786 100644 --- a/trim/desc_test.go +++ b/trim/desc_test.go @@ -27,14 +27,14 @@ func TestDescriptorMarshalJSON(t *testing.T) { // Create a simple descriptor without circular reference desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Root", + Type: "Root", Children: []Field{ { Name: "field1", ID: 1, Desc: &Descriptor{ Kind: TypeKind_Leaf, - Name: "Leaf1", + Type: "Leaf1", }, }, { @@ -42,14 +42,14 @@ func TestDescriptorMarshalJSON(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "Map1", + Type: "Map1", Children: []Field{ { Name: "key1", ID: 0, Desc: &Descriptor{ Kind: TypeKind_Leaf, - Name: "Leaf2", + Type: "Leaf2", }, }, }, @@ -70,24 +70,24 @@ func TestDescriptorMarshalJSON(t *testing.T) { // Verify structure require.Equal(t, desc.Kind, desc2.Kind) - require.Equal(t, desc.Name, desc2.Name) + 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.Name, desc2.Children[0].Desc.Name) + 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, - Name: "Root", + Type: "Root", } child := &Descriptor{ Kind: TypeKind_Struct, - Name: "Child", + Type: "Child", } // Root -> Child -> Root (circular reference) @@ -119,10 +119,10 @@ func TestDescriptorMarshalJSONWithCircularReference(t *testing.T) { // Verify structure require.Equal(t, root.Kind, root2.Kind) - require.Equal(t, root.Name, root2.Name) + 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.Name) + require.Equal(t, "Child", root2.Children[0].Desc.Type) // Verify circular reference is resolved require.NotNil(t, root2.Children[0].Desc.Children) @@ -136,7 +136,7 @@ func TestDescriptorMarshalJSONSelfReference(t *testing.T) { // Create a descriptor that references itself self := &Descriptor{ Kind: TypeKind_Struct, - Name: "Self", + Type: "Self", } self.Children = []Field{ @@ -165,7 +165,7 @@ func TestDescriptorMarshalJSONNil(t *testing.T) { // Descriptor with nil child desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Root", + Type: "Root", Children: []Field{ { Name: "field1", @@ -190,12 +190,12 @@ func TestDescriptorMarshalJSONMultipleReferences(t *testing.T) { // Create a shared descriptor referenced by multiple fields shared := &Descriptor{ Kind: TypeKind_Leaf, - Name: "Shared", + Type: "Shared", } root := &Descriptor{ Kind: TypeKind_Struct, - Name: "Root", + Type: "Root", Children: []Field{ { Name: "ref1", diff --git a/trim/fetch.go b/trim/fetch.go index 69bc70c2..00c97332 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -61,9 +61,9 @@ type ErrNotFound struct { func (e ErrNotFound) Error() string { if e.Msg != "" { - return fmt.Sprintf("not found %v at %v: %s", e.Field.Name, e.Parent.Name, 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.Name) + return fmt.Sprintf("not found %v at %v", e.Field.Name, e.Parent.Type) } // structFieldInfo caches field mapping information for a struct type diff --git a/trim/fetch_test.go b/trim/fetch_test.go index b18285bc..4b8bad4f 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -68,7 +68,7 @@ func makeDesc(width int, depth int, withE bool) *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ { Name: "field_a", @@ -107,7 +107,7 @@ func makeDesc(width int, depth int, withE bool) *Descriptor { nd := makeDesc(width, depth-1, withE) desc.Children[2].Desc = &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", @@ -119,7 +119,7 @@ func makeDesc(width int, depth int, withE bool) *Descriptor { // field_list is TypeKind_List with wildcard (all elements) desc.Children[4].Desc = &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "*", // all elements @@ -187,7 +187,7 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { t.Run("specific_indices", func(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ { Name: "field_a", @@ -198,7 +198,7 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "0", ID: 0}, // index 0 {Name: "2", ID: 2}, // index 2 @@ -245,14 +245,14 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { t.Run("out_of_bounds_index", func(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "1", ID: 1}, // index 1 - valid {Name: "10", ID: 10}, // index 10 - out of bounds @@ -295,14 +295,14 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { t.Run("disallow_not_found_out_of_bounds", func(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ { Name: "field_list", ID: 6, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ {Name: "1", ID: 1}, // index 1 - valid {Name: "10", ID: 10}, // index 10 - out of bounds @@ -322,8 +322,8 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { if !ok { t.Fatalf("expected ErrNotFound, got %T: %v", err, err) } - if notFoundErr.Parent.Name != "LIST" { - t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Name) + if notFoundErr.Parent.Type != "LIST" { + t.Errorf("expected parent name 'LIST', got '%s'", notFoundErr.Parent.Type) } }) @@ -340,21 +340,21 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ { Name: "field_b", ID: 2, Desc: &Descriptor{ Kind: TypeKind_List, - Name: "LIST", + Type: "LIST", Children: []Field{ { Name: "0", ID: 0, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, }, @@ -365,7 +365,7 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_e", ID: 5}, }, @@ -513,7 +513,7 @@ func TestFetchAnyWithUnknownFields(t *testing.T) { // Create a descriptor that asks for all fields desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, // Static field {Name: "field_e", ID: 5}, // From unknownFields (string) @@ -584,7 +584,7 @@ func TestFetchAnyWithEmptyUnknownFields(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, // Not present in unknownFields @@ -618,7 +618,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, // Not present @@ -635,8 +635,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { if !ok { t.Fatalf("expected ErrNotFound, got %T: %v", err, err) } - if notFoundErr.Parent.Name != "SampleWithUnknown" { - t.Errorf("expected parent name 'SampleWithUnknown', got '%s'", notFoundErr.Parent.Name) + 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) @@ -652,14 +652,14 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Test", + Type: "Test", Children: []Field{ { Name: "data", ID: 1, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ {Name: "key1"}, // exists {Name: "key2"}, // not exists @@ -679,8 +679,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { if !ok { t.Fatalf("expected ErrNotFound, got %T: %v", err, err) } - if notFoundErr.Parent.Name != "MAP" { - t.Errorf("expected parent name 'MAP', got '%s'", notFoundErr.Parent.Name) + 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) @@ -700,14 +700,14 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "Outer", + Type: "Outer", Children: []Field{ { Name: "inner", ID: 1, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Inner", + Type: "Inner", Children: []Field{ {Name: "value", ID: 1}, // exists {Name: "missing", ID: 99}, // not exists @@ -727,8 +727,8 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { if !ok { t.Fatalf("expected ErrNotFound, got %T: %v", err, err) } - if notFoundErr.Parent.Name != "Inner" { - t.Errorf("expected parent name 'Inner', got '%s'", notFoundErr.Parent.Name) + 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) @@ -740,7 +740,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, // exists }, @@ -788,7 +788,7 @@ type circularTree struct { func makeCircularDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularNode", + Type: "CircularNode", Children: []Field{ {Name: "value", ID: 1}, {Name: "next", ID: 2}, @@ -803,7 +803,7 @@ func makeCircularDesc() *Descriptor { func makeCircularTreeDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularTree", + Type: "CircularTree", Children: []Field{ {Name: "value", ID: 1}, {Name: "left", ID: 2}, @@ -1026,7 +1026,7 @@ type circularMapNode struct { func makeCircularMapDesc() *Descriptor { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "CircularMapNode", + Type: "CircularMapNode", Children: []Field{ {Name: "name", ID: 1}, {Name: "children", ID: 2}, @@ -1035,7 +1035,7 @@ func makeCircularMapDesc() *Descriptor { // Make children field circular: it's a map with values of the same type desc.Children[1].Desc = &Descriptor{ Kind: TypeKind_StrMap, - Name: "ChildrenMap", + Type: "ChildrenMap", Children: []Field{ {Name: "*", Desc: desc}, // Wildcard with circular reference }, @@ -1162,7 +1162,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { // This means we don't want to further fetch into the struct desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "nested_struct", ID: 10}, // No Desc, so no further fetch @@ -1228,7 +1228,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { // This means we want to further fetch into the struct and convert it desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1236,7 +1236,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { ID: 10, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "NestedStruct", + Type: "NestedStruct", Children: []Field{ {Name: "name", ID: 1}, // maps to field 1 {Name: "count", ID: 2}, // maps to field 2 @@ -1317,7 +1317,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { // Create a descriptor for deeply nested fetch desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1325,7 +1325,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { ID: 10, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Level1Struct", + Type: "Level1Struct", Children: []Field{ {Name: "level1_name", ID: 1}, { @@ -1333,7 +1333,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { ID: 2, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Level2Struct", + Type: "Level2Struct", Children: []Field{ {Name: "level2_name", ID: 1}, {Name: "level2_value", ID: 2}, @@ -1416,7 +1416,7 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { // Create a descriptor with map containing struct descriptor desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleWithUnknown", + Type: "SampleWithUnknown", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1424,13 +1424,13 @@ func TestFetchAnyWithUnknownFieldsStruct(t *testing.T) { ID: 10, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "DataMap", + Type: "DataMap", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "Data", + Type: "Data", Children: []Field{ {Name: "name", ID: 1}, {Name: "count", ID: 2}, @@ -1504,7 +1504,7 @@ func TestFetchAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "unknown_field", ID: 99}, @@ -1522,7 +1522,7 @@ func TestFetchAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1530,7 +1530,7 @@ func TestFetchAny_PathTracking(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "missing_field", ID: 88}, @@ -1554,7 +1554,7 @@ func TestFetchAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1562,7 +1562,7 @@ func TestFetchAny_PathTracking(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1570,7 +1570,7 @@ func TestFetchAny_PathTracking(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "bad_field", ID: 77}, @@ -1594,7 +1594,7 @@ func TestFetchAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1602,7 +1602,7 @@ func TestFetchAny_PathTracking(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ {Name: "key1"}, {Name: "missing_key"}, @@ -1623,7 +1623,7 @@ func TestFetchAny_PathTracking(t *testing.T) { }, desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1631,13 +1631,13 @@ func TestFetchAny_PathTracking(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "nonexistent", ID: 66}, @@ -1683,7 +1683,7 @@ func TestFetchAny_PathTracking_Integration(t *testing.T) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1691,13 +1691,13 @@ func TestFetchAny_PathTracking_Integration(t *testing.T) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1705,7 +1705,7 @@ func TestFetchAny_PathTracking_Integration(t *testing.T) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "missing", ID: 55}, @@ -1743,7 +1743,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1771,7 +1771,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1779,7 +1779,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1787,7 +1787,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1819,7 +1819,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1827,13 +1827,13 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { ID: 3, Desc: &Descriptor{ Kind: TypeKind_StrMap, - Name: "MAP", + Type: "MAP", Children: []Field{ { Name: "*", Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "field_e", ID: 5}, @@ -1863,7 +1863,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { desc := &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, { @@ -1871,7 +1871,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { ID: 4, Desc: &Descriptor{ Kind: TypeKind_Struct, - Name: "SampleFetch", + Type: "SampleFetch", Children: []Field{ {Name: "field_a", ID: 1}, {Name: "missing", ID: 99}, From 7bb7cb9e262d2bb634af26c2919b1b2de109656f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Wed, 24 Dec 2025 17:54:37 +0800 Subject: [PATCH 14/17] feat: support get Namespace and File from TypeDescriptor --- thrift/descriptor.go | 22 ++++-- thrift/idl.go | 98 ++++++++++++++++++++++++ thrift/idl_test.go | 172 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+), 5 deletions(-) diff --git a/thrift/descriptor.go b/thrift/descriptor.go index 4cc4dba9..faca7438 100644 --- a/thrift/descriptor.go +++ b/thrift/descriptor.go @@ -117,17 +117,29 @@ func (p Type) IsComplex() bool { // TypeDescriptor is the runtime descriptor of a thrift type type TypeDescriptor struct { - typ Type - name string - key *TypeDescriptor // for map key - elem *TypeDescriptor // for slice or map element - struc *StructDescriptor // for struct + typ Type + name string + key *TypeDescriptor // for map key + elem *TypeDescriptor // for slice or map element + struc *StructDescriptor // for struct + namespace string // for idl namespace + file string // for idl absolute filepath } const ( nameBinary = "binary" ) +// Namespace returns the namespace of the type +func (d TypeDescriptor) Namespace() string { + return d.namespace +} + +// File returns the file of the type +func (d TypeDescriptor) File() string { + return d.file +} + // IsBinary tells if the type is binary type ([]byte) func (d TypeDescriptor) IsBinary() bool { if d.name == nameBinary { diff --git a/thrift/idl.go b/thrift/idl.go index 02434488..d9163909 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 @@ -572,16 +597,22 @@ func parseType(ctx context.Context, t *parser.Type, tree *parser.Thrift, cache c switch t.Name { case "list": ty := &TypeDescriptor{name: t.Name} + ty.namespace = thriftNamespace(tree) + ty.file = tree.GetFilename() ty.typ = LIST ty.elem, err = parseType(ctx, t.ValueType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget) return ty, err case "set": ty := &TypeDescriptor{name: t.Name} + ty.namespace = thriftNamespace(tree) + ty.file = tree.GetFilename() ty.typ = SET ty.elem, err = parseType(ctx, t.ValueType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget) return ty, err case "map": ty := &TypeDescriptor{name: t.Name} + ty.namespace = thriftNamespace(tree) + ty.file = tree.GetFilename() ty.typ = MAP if ty.key, err = parseType(ctx, t.KeyType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget); err != nil { return nil, err @@ -655,6 +686,8 @@ func parseType(ctx context.Context, t *parser.Type, tree *parser.Thrift, cache c annotations: oannos, }, } + ty.namespace = thriftNamespace(tree) + ty.file = tree.GetFilename() if recursionDepth == 0 { ctx = context.WithValue(ctx, CtxKeyIsBodyRoot, true) @@ -924,3 +957,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..91af2985 100644 --- a/thrift/idl_test.go +++ b/thrift/idl_test.go @@ -488,3 +488,175 @@ 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) + // verify file and namespace correctness + require.Equal(t, "base.thrift", desc.File()) + require.Equal(t, "kitex.test.base", desc.Namespace()) + + 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) + // verify file and namespace correctness + require.Equal(t, "a/b/main.thrift", desc.File()) + require.Equal(t, "kitex.test.server", desc.Namespace()) + + 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()) + }) +} From 2f4b886f5a173b2f5986e5da846a295a2893eeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Fri, 26 Dec 2025 20:07:54 +0800 Subject: [PATCH 15/17] fix: only assign leaf nodes --- trim/assign.go | 111 +++++++++++++++++++++++++++++++++++++------- trim/assign_test.go | 86 +++++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 19 deletions(-) diff --git a/trim/assign.go b/trim/assign.go index e6635c9c..5e882c26 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -322,6 +322,11 @@ func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Va // 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) @@ -329,6 +334,27 @@ func assignValueToField(desc *Descriptor, src interface{}, fieldValue reflect.Va 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{}) @@ -355,24 +381,55 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op elemType := destValue.Type().Elem() for key, value := range srcMap { - // Create a new element + 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 { - // Set nil value in map (zero value for the element type, e.g., nil pointer) - destValue.SetMapIndex(reflect.ValueOf(key), elemValue) + // 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) - // Find the appropriate descriptor var err error - if wildcardDesc != nil { - err = assignValueToField(wildcardDesc, value, elemValue, opt, stack) - } else if child, ok := keyDescMap[key]; ok && child.Desc != nil { - err = assignValueToField(child.Desc, value, elemValue, opt, stack) + 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) } @@ -384,7 +441,7 @@ func assignStrMap(desc *Descriptor, src interface{}, destValue reflect.Value, op return err } - destValue.SetMapIndex(reflect.ValueOf(key), elemValue) + destValue.SetMapIndex(keyValue, elemValue) } return nil @@ -440,14 +497,27 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt return nil } - // Handle slice (dynamic size) + // Handle slice (dynamic size). Reuse existing slice when possible; only allocate when growing. elemType := destValue.Type().Elem() - newSlice := reflect.MakeSlice(destValue.Type(), srcLen, srcLen) + 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 := newSlice.Index(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 { - // Keep as zero value + // Preserve existing value continue } @@ -456,12 +526,15 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt var err error if wildcardDesc != nil { - // Handle pointer element type + // Handle pointer element type without dropping existing value if elemType.Kind() == reflect.Ptr { - newElem := reflect.New(elemType.Elem()) - err = assignValue(wildcardDesc, srcSlice[i], newElem.Elem(), opt, stack) + targetPtr := elemValue + if elemValue.IsNil() { + targetPtr = reflect.New(elemType.Elem()) + } + err = assignValue(wildcardDesc, srcSlice[i], targetPtr.Elem(), opt, stack) if err == nil { - elemValue.Set(newElem) + elemValue.Set(targetPtr) } } else { err = assignValue(wildcardDesc, srcSlice[i], elemValue, opt, stack) @@ -478,7 +551,9 @@ func assignList(desc *Descriptor, src interface{}, destValue reflect.Value, opt } } - destValue.Set(newSlice) + if useSlice.Pointer() != destValue.Pointer() || destLen != useSlice.Len() { + destValue.Set(useSlice) + } return nil } diff --git a/trim/assign_test.go b/trim/assign_test.go index d50c25b2..93439660 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -24,6 +24,7 @@ import ( "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" @@ -232,6 +233,89 @@ func TestAssignAny_Map(t *testing.T) { } } +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{} + 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) { @@ -1656,7 +1740,7 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { } // Wildcard should completely replace with source length - if len(dest.FieldB) != 2 { + if len(dest.FieldB) != 3 { t.Fatalf("field_b: expected length 2 (from source), got %d", len(dest.FieldB)) } From e9d9bb97f4c25a3b6ef96c115ad50535e74afa29 Mon Sep 17 00:00:00 2001 From: "duanyi.aster" Date: Mon, 5 Jan 2026 20:19:03 +0800 Subject: [PATCH 16/17] fix: desc race issue --- go.mod | 1 + go.sum | 1 + trim/all_test.go | 93 +++++++++++++++++++++++++++++++-------------- trim/assign.go | 4 +- trim/assign_test.go | 14 +++++++ trim/desc.go | 13 ++++--- trim/fetch.go | 4 +- trim/fetch_test.go | 12 ++++++ 8 files changed, 105 insertions(+), 37 deletions(-) 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/trim/all_test.go b/trim/all_test.go index 9b14b7c5..49dd1763 100644 --- a/trim/all_test.go +++ b/trim/all_test.go @@ -2,6 +2,7 @@ package trim import ( "encoding/json" + "sync" "testing" "github.com/cloudwego/dynamicgo/proto" @@ -11,9 +12,23 @@ import ( "github.com/stretchr/testify/require" ) -func fetchAny(desc *Descriptor, any interface{}) (interface{}, error) { - fetcher := &Fetcher{} - return fetcher.FetchAny(desc, any) +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) { @@ -23,35 +38,55 @@ func TestFetchAndAssign(t *testing.T) { t.Fatalf("json.Marshal failed: %v", err) } desc := makeDesc(3, 3, true) - 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) - } + 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() +} - 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() +func fetchAny(desc *Descriptor, any interface{}) (interface{}, error) { + if desc != nil { + desc.Normalize() } - require.Equal(t, srcAny, destAny) + fetcher := &Fetcher{} + return fetcher.FetchAny(desc, any) } // ===================== UnknownFields FetchAndAssign Tests ===================== diff --git a/trim/assign.go b/trim/assign.go index 5e882c26..3c3276f1 100644 --- a/trim/assign.go +++ b/trim/assign.go @@ -42,12 +42,14 @@ type Assigner struct { // - 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() + // desc.Normalize() destValue := reflect.ValueOf(dest) if destValue.Kind() != reflect.Ptr { diff --git a/trim/assign_test.go b/trim/assign_test.go index 93439660..6643f7f5 100644 --- a/trim/assign_test.go +++ b/trim/assign_test.go @@ -33,6 +33,9 @@ import ( ) func assignAny(desc *Descriptor, src interface{}, dest interface{}) error { + if desc != nil { + desc.Normalize() + } assigner := &Assigner{} return assigner.AssignAny(desc, src, dest) } @@ -294,6 +297,7 @@ func TestAssignAny_OnlyAssignLeafNodes(t *testing.T) { } assigner := &Assigner{} + desc.Normalize() err := assigner.AssignAny(desc, src, dest) if err != nil { t.Fatalf("AssignAny failed: %v", err) @@ -1305,6 +1309,7 @@ func TestAssignAny_ListWithSpecificIndices(t *testing.T) { 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") @@ -1786,6 +1791,7 @@ func TestAssignAny_DisallowNotFound(t *testing.T) { 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") @@ -2337,6 +2343,7 @@ func TestAssignAny_PathTracking(t *testing.T) { 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") @@ -2606,6 +2613,7 @@ func TestAssignAny_PathTracking_Integration(t *testing.T) { dest := &sampleAssign{} as := Assigner{AssignOptions{DisallowNotDefined: true}} + desc.Normalize() err := as.AssignAny(desc, src, dest) if err == nil { t.Fatalf("expected error, got nil") @@ -2804,6 +2812,7 @@ func BenchmarkAssignAny_ErrorPath(b *testing.B) { for i := 0; i < b.N; i++ { dest := &sampleAssign{} as := Assigner{AssignOptions{DisallowNotDefined: true}} + desc.Normalize() _ = as.AssignAny(desc, src, dest) } } @@ -2922,6 +2931,7 @@ func BenchmarkPathTracking_Overhead(b *testing.B) { "unknown": 999, } b.ResetTimer() + desc.Normalize() for i := 0; i < b.N; i++ { dest := &sampleAssign{} as := Assigner{AssignOptions{DisallowNotDefined: true}} @@ -4019,6 +4029,7 @@ func TestAssignAny_Array(t *testing.T) { dest := &sampleAssignArray{} assigner := &Assigner{} + desc.Normalize() err := assigner.AssignAny(desc, src, dest) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -4055,6 +4066,7 @@ func TestAssignAny_Array(t *testing.T) { } dest := &sampleAssignArray{} assigner := &Assigner{} + desc.Normalize() err := assigner.AssignAny(desc, src, dest) if err == nil { t.Fatal("expected error, got nil") @@ -4086,6 +4098,7 @@ func TestAssignAny_Array(t *testing.T) { } 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") @@ -4117,6 +4130,7 @@ func TestAssignAny_Array(t *testing.T) { } 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") diff --git a/trim/desc.go b/trim/desc.go index acd46655..a0a72042 100644 --- a/trim/desc.go +++ b/trim/desc.go @@ -22,7 +22,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" ) // TypeKind is the kind of type. @@ -41,6 +40,8 @@ const ( // 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) @@ -56,7 +57,8 @@ type Descriptor struct { Children []Field // for speed-up search - normalized int32 // atomic flag: 0=not started, 1=in progress/done + // WARNING: user must call Normalize() before using this + normalized bool ids map[int]Field names map[string]Field } @@ -77,12 +79,11 @@ type Field struct { // 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() { - // Use atomic to detect if we're already normalizing this descriptor - // This prevents infinite recursion in circular references - if !atomic.CompareAndSwapInt32(&d.normalized, 0, 1) { - // Already being normalized or already done + // 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)) diff --git a/trim/fetch.go b/trim/fetch.go index 00c97332..df912885 100644 --- a/trim/fetch.go +++ b/trim/fetch.go @@ -37,12 +37,14 @@ type FetchOptions struct { } // 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() + // desc.Normalize() // Initialize path stack from pool stack := getStackFrames() diff --git a/trim/fetch_test.go b/trim/fetch_test.go index 4b8bad4f..e000ae17 100644 --- a/trim/fetch_test.go +++ b/trim/fetch_test.go @@ -128,6 +128,7 @@ func makeDesc(width int, depth int, withE bool) *Descriptor { }, } + desc.Normalize() return desc } @@ -313,6 +314,7 @@ func TestFetchAny_ListWithSpecificIndices(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") @@ -626,6 +628,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") @@ -670,6 +673,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") @@ -718,6 +722,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected ErrNotFound, got nil") @@ -747,6 +752,7 @@ func TestFetchAnyWithDisallowNotFound(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() ret, err := f.FetchAny(desc, obj) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -1656,6 +1662,7 @@ func TestFetchAny_PathTracking(t *testing.T) { 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") @@ -1722,6 +1729,7 @@ func TestFetchAny_PathTracking_Integration(t *testing.T) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() _, err := f.FetchAny(desc, obj) if err == nil { t.Fatalf("expected error, got nil") @@ -1751,6 +1759,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { } f := Fetcher{} + desc.Normalize() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = f.FetchAny(desc, obj) @@ -1801,6 +1810,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { } f := Fetcher{} + desc.Normalize() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = f.FetchAny(desc, obj) @@ -1847,6 +1857,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { } f := Fetcher{} + desc.Normalize() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = f.FetchAny(desc, obj) @@ -1882,6 +1893,7 @@ func BenchmarkFetchAny_PathTracking(b *testing.B) { } f := Fetcher{FetchOptions: FetchOptions{DisallowNotFound: true}} + desc.Normalize() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = f.FetchAny(desc, obj) From 70e95f647e398716617787e2e70079dcd020db4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B5=E4=BB=AA?= Date: Thu, 8 Jan 2026 19:25:19 +0800 Subject: [PATCH 17/17] chore: get namespace from annos --- thrift/descriptor.go | 22 +++++----------------- thrift/idl.go | 8 -------- thrift/idl_test.go | 6 ------ 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/thrift/descriptor.go b/thrift/descriptor.go index faca7438..4cc4dba9 100644 --- a/thrift/descriptor.go +++ b/thrift/descriptor.go @@ -117,29 +117,17 @@ func (p Type) IsComplex() bool { // TypeDescriptor is the runtime descriptor of a thrift type type TypeDescriptor struct { - typ Type - name string - key *TypeDescriptor // for map key - elem *TypeDescriptor // for slice or map element - struc *StructDescriptor // for struct - namespace string // for idl namespace - file string // for idl absolute filepath + typ Type + name string + key *TypeDescriptor // for map key + elem *TypeDescriptor // for slice or map element + struc *StructDescriptor // for struct } const ( nameBinary = "binary" ) -// Namespace returns the namespace of the type -func (d TypeDescriptor) Namespace() string { - return d.namespace -} - -// File returns the file of the type -func (d TypeDescriptor) File() string { - return d.file -} - // IsBinary tells if the type is binary type ([]byte) func (d TypeDescriptor) IsBinary() bool { if d.name == nameBinary { diff --git a/thrift/idl.go b/thrift/idl.go index d9163909..3df59b99 100644 --- a/thrift/idl.go +++ b/thrift/idl.go @@ -597,22 +597,16 @@ func parseType(ctx context.Context, t *parser.Type, tree *parser.Thrift, cache c switch t.Name { case "list": ty := &TypeDescriptor{name: t.Name} - ty.namespace = thriftNamespace(tree) - ty.file = tree.GetFilename() ty.typ = LIST ty.elem, err = parseType(ctx, t.ValueType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget) return ty, err case "set": ty := &TypeDescriptor{name: t.Name} - ty.namespace = thriftNamespace(tree) - ty.file = tree.GetFilename() ty.typ = SET ty.elem, err = parseType(ctx, t.ValueType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget) return ty, err case "map": ty := &TypeDescriptor{name: t.Name} - ty.namespace = thriftNamespace(tree) - ty.file = tree.GetFilename() ty.typ = MAP if ty.key, err = parseType(ctx, t.KeyType, tree, cache, nextRecursionDepth, opts, nextAnns, parseTarget); err != nil { return nil, err @@ -686,8 +680,6 @@ func parseType(ctx context.Context, t *parser.Type, tree *parser.Thrift, cache c annotations: oannos, }, } - ty.namespace = thriftNamespace(tree) - ty.file = tree.GetFilename() if recursionDepth == 0 { ctx = context.WithValue(ctx, CtxKeyIsBodyRoot, true) diff --git a/thrift/idl_test.go b/thrift/idl_test.go index 91af2985..8aa6a466 100644 --- a/thrift/idl_test.go +++ b/thrift/idl_test.go @@ -580,9 +580,6 @@ func TestNewDescriptorByName(t *testing.T) { // Test parsing User struct with cross-file reference notation desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "base.User", includes) - // verify file and namespace correctness - require.Equal(t, "base.thrift", desc.File()) - require.Equal(t, "kitex.test.base", desc.Namespace()) require.NoError(t, err) require.NotNil(t, desc) @@ -638,9 +635,6 @@ func TestNewDescriptorByName(t *testing.T) { // Test parsing UserRequest with relative path that goes up directories desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "UserRequest", includes) - // verify file and namespace correctness - require.Equal(t, "a/b/main.thrift", desc.File()) - require.Equal(t, "kitex.test.server", desc.Namespace()) require.NoError(t, err) require.NotNil(t, desc)