diff --git a/copier.go b/copier.go index 3e78f24..b08347d 100644 --- a/copier.go +++ b/copier.go @@ -211,7 +211,7 @@ func copier(toValue interface{}, fromValue interface{}, opt Option) (err error) to.SetMapIndex(toKey, toValue) break } - elemType = reflect.PointerTo(elemType) + elemType = reflect.PtrTo(elemType) toValue = toValue.Addr() } } @@ -875,5 +875,37 @@ func fieldByName(v reflect.Value, name string, caseSensitive bool) reflect.Value return v.FieldByName(name) } - return v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, name) }) + // For case-insensitive matching, prioritize exported fields + // First try exact match (might be the exported field) + if field := v.FieldByName(name); field.IsValid() { + return field + } + + // If exact match fails, perform case-insensitive matching + // but prioritize exported fields while preserving promoted-field lookup. + var candidates []reflect.Value + var candidateFields []reflect.StructField + + for _, fieldType := range reflect.VisibleFields(v.Type()) { + if strings.EqualFold(fieldType.Name, name) { + field := v.FieldByIndex(fieldType.Index) + candidates = append(candidates, field) + candidateFields = append(candidateFields, fieldType) + } + } + + // If there are multiple candidate fields, prioritize exported ones + for i, candidate := range candidates { + if candidateFields[i].IsExported() { + return candidate + } + } + + // If no exported field found, return the first match (preserve original behavior) + if len(candidates) > 0 { + return candidates[0] + } + + // If no match found, return zero value + return reflect.Value{} } diff --git a/copier_case_insensitive_test.go b/copier_case_insensitive_test.go new file mode 100644 index 0000000..5ebce8a --- /dev/null +++ b/copier_case_insensitive_test.go @@ -0,0 +1,110 @@ +package copier + +import ( + "testing" +) + +// Test case for issue where case-insensitive field matching +// fails when there are both exported and unexported fields with similar names +func TestCaseInsensitiveFieldMatching(t *testing.T) { + // Simulate protobuf generated struct with both unexported 'state' and exported 'State' fields + type ProtoStruct struct { + state uint32 + PageNumber int32 `json:"pageNumber"` + State int32 `json:"state"` + } + + type SourceStruct struct { + PageNumber int32 `json:"pageNumber"` + State int32 `json:"state"` + } + + source := SourceStruct{ + PageNumber: 1, + State: 99, + } + + dest := &ProtoStruct{} + + err := Copy(dest, &source) + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + + // Verify that the exported State field was copied correctly + if dest.State != source.State { + t.Errorf("State field not copied correctly. Expected: %d, Got: %d", source.State, dest.State) + } + + if dest.state != 0 { + t.Errorf("unexported field state should remain zero, got: %d", dest.state) + } + + if dest.PageNumber != source.PageNumber { + t.Errorf("PageNumber field not copied correctly. Expected: %d, Got: %d", source.PageNumber, dest.PageNumber) + } +} + +// Test that exact case matching still works +func TestExactCaseMatching(t *testing.T) { + type Source struct { + Name string + Age int + } + + type Dest struct { + Name string + Age int + } + + source := Source{Name: "John", Age: 30} + dest := &Dest{} + + err := Copy(dest, &source) + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + + if dest.Name != source.Name || dest.Age != source.Age { + t.Errorf("Fields not copied correctly. Expected: %+v, Got: %+v", source, *dest) + } +} + +func TestCaseInsensitiveMatchingWithEmbeddedField(t *testing.T) { + type Embedded struct { + Name string + } + + type Dest struct { + name string + Embedded + } + + type Source struct { + NAME string + } + + source := Source{ + NAME: "John", + } + + dest := &Dest{} + + err := CopyWithOption(dest, &source, Option{ + CaseSensitive: false, + }) + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + + if dest.Name != source.NAME { + t.Fatalf("embedded promoted field not copied correctly. expected %q, got %q", source.NAME, dest.Name) + } + + if dest.Embedded.Name != source.NAME { + t.Fatalf("embedded field not copied correctly. expected %q, got %q", source.NAME, dest.Embedded.Name) + } + if dest.name != "" { + t.Fatalf("unexported field should not be copied, got %q", dest.name) + } +}