Skip to content
Open
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
38 changes: 37 additions & 1 deletion copier.go
Original file line number Diff line number Diff line change
Expand Up @@ -875,5 +875,41 @@ 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
var candidates []reflect.Value
var candidateNames []string

structType := v.Type()
for i := 0; i < structType.NumField(); i++ {
fieldType := structType.Field(i)
if strings.EqualFold(fieldType.Name, name) {
field := v.Field(i)
candidates = append(candidates, field)
candidateNames = append(candidateNames, fieldType.Name)
}
}

// If there are multiple candidate fields, prioritize exported ones
for i, candidate := range candidates {
fieldName := candidateNames[i]
// Check if field is exported (starts with uppercase letter)
if len(fieldName) > 0 && unicode.IsUpper(rune(fieldName[0])) {
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{}
}
71 changes: 71 additions & 0 deletions copier_case_insensitive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 == uint32(source.State) {
t.Errorf("state field not copied correctly. Expected: %d, Got: %d", source.State, 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)
}
}