Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions copier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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{}
}
110 changes: 110 additions & 0 deletions copier_case_insensitive_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading