diff --git a/issue256_test.go b/issue256_test.go new file mode 100644 index 0000000..96fea69 --- /dev/null +++ b/issue256_test.go @@ -0,0 +1,64 @@ +package mergo_test + +import ( + "reflect" + "testing" + + "dario.cat/mergo" +) + +func TestIssue256MergeMapCaseInsensitively(t *testing.T) { + dst := map[string]map[string]string{ + "Config": { + "Host": "localhost", + }, + } + src := map[string]map[string]string{ + "config": { + "host": "example.com", + "Port": "443", + }, + } + + if err := mergo.Merge(&dst, src, mergo.WithOverride, mergo.WithCaseInsensitiveMapKeys); err != nil { + t.Fatal(err) + } + + expected := map[string]map[string]string{ + "Config": { + "Host": "example.com", + "Port": "443", + }, + } + if !reflect.DeepEqual(dst, expected) { + t.Fatalf("got %#v, want %#v", dst, expected) + } +} + +func TestIssue256MergeMapCaseInsensitiveWithOverwriteEmpty(t *testing.T) { + dst := map[string]string{"Host": "localhost", "User": "root"} + src := map[string]string{"host": "example.com"} + + if err := mergo.Merge(&dst, src, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue, mergo.WithCaseInsensitiveMapKeys); err != nil { + t.Fatal(err) + } + + expected := map[string]string{"Host": "example.com"} + if !reflect.DeepEqual(dst, expected) { + t.Fatalf("got %#v, want %#v", dst, expected) + } +} + +func TestIssue256MergeMapCaseSensitiveByDefault(t *testing.T) { + dst := map[string]string{"Host": "localhost"} + src := map[string]string{"host": "example.com"} + + if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil { + t.Fatal(err) + } + + expected := map[string]string{"Host": "localhost", "host": "example.com"} + if !reflect.DeepEqual(dst, expected) { + t.Fatalf("got %#v, want %#v", dst, expected) + } +} diff --git a/merge.go b/merge.go index 5488338..4f0182a 100644 --- a/merge.go +++ b/merge.go @@ -11,6 +11,7 @@ package mergo import ( "fmt" "reflect" + "strings" ) func hasMergeableFields(dst reflect.Value) (exported bool) { @@ -46,6 +47,7 @@ type Config struct { overwriteWithEmptyValue bool overwriteSliceWithEmptyValue bool sliceDeepCopy bool + caseInsensitiveMapKeys bool } type Transformers interface { @@ -116,16 +118,26 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co } for _, key := range src.MapKeys() { + mergeKey := key + if config.caseInsensitiveMapKeys && key.Kind() == reflect.String && dst.Type().Key().Kind() == reflect.String { + for _, dstKey := range dst.MapKeys() { + if strings.EqualFold(dstKey.String(), key.String()) { + mergeKey = dstKey + break + } + } + } + srcElement := src.MapIndex(key) if !srcElement.IsValid() { continue } - dstElement := dst.MapIndex(key) + dstElement := dst.MapIndex(mergeKey) switch srcElement.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice: if srcElement.IsNil() { if overwrite { - dst.SetMapIndex(key, srcElement) + dst.SetMapIndex(mergeKey, srcElement) } continue } @@ -190,7 +202,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co } } - dst.SetMapIndex(key, dstSlice) + dst.SetMapIndex(mergeKey, dstSlice) } } @@ -207,7 +219,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) } - dst.SetMapIndex(key, srcElement) + dst.SetMapIndex(mergeKey, srcElement) } } @@ -215,6 +227,14 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co if overwriteWithEmptySrc { for _, key := range dst.MapKeys() { srcElement := src.MapIndex(key) + if !srcElement.IsValid() && config.caseInsensitiveMapKeys && key.Kind() == reflect.String && src.Type().Key().Kind() == reflect.String { + for _, srcKey := range src.MapKeys() { + if strings.EqualFold(srcKey.String(), key.String()) { + srcElement = src.MapIndex(srcKey) + break + } + } + } if !srcElement.IsValid() { dst.SetMapIndex(key, reflect.Value{}) } @@ -367,6 +387,11 @@ func WithSliceDeepCopy(config *Config) { config.Overwrite = true } +// WithCaseInsensitiveMapKeys will make merge match string map keys case-insensitively. +func WithCaseInsensitiveMapKeys(config *Config) { + config.caseInsensitiveMapKeys = true +} + func merge(dst, src interface{}, opts ...func(*Config)) error { if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr { return ErrNonPointerArgument