From 0cb9604208a9c436c14101ce193c9913a99c42c4 Mon Sep 17 00:00:00 2001 From: Huyaohua Date: Sat, 11 Oct 2025 16:19:49 +0800 Subject: [PATCH 1/3] Fix: preserve nil values in MapToMap conversion --- conv.go | 13 +++++++++++- conv_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/conv.go b/conv.go index b8d034e..d3d8465 100644 --- a/conv.go +++ b/conv.go @@ -451,7 +451,18 @@ func (c *Conv) MapToMap(m interface{}, typ reflect.Type) (interface{}, error) { return nil, errForFunctionF(fnName, "cannot covert value of key '%v' to %v: %v", srcKey, dstValueType, err.Error()) } - dst.SetMapIndex(reflect.ValueOf(dstKey), reflect.ValueOf(dstVal)) + // Handle nil value case. When dstVal is nil, reflect.ValueOf(dstVal) returns zero reflect.Value, + // and SetMapIndex interprets zero reflect.Value as a delete key operation. + // To preserve nil values, we need to create a valid reflect.Value representing nil. + var dstValReflect reflect.Value + if dstVal == nil { + // Create a valid reflect.Value representing nil + dstValReflect = reflect.Zero(dstValueType) + } else { + dstValReflect = reflect.ValueOf(dstVal) + } + + dst.SetMapIndex(reflect.ValueOf(dstKey), dstValReflect) } return dst.Interface(), nil diff --git a/conv_test.go b/conv_test.go index c66a4bd..b1746a7 100644 --- a/conv_test.go +++ b/conv_test.go @@ -1763,3 +1763,62 @@ func TestConv_tryFlattenEmptyKeyMap(t *testing.T) { }) } } + +// TestConv_MapToMapWithNilValues tests the MapToMap function handling nil values. +func TestConv_mapToMapWithNilValues(t *testing.T) { + // Test data: map slice containing nil values + value := []map[string]interface{}{ + {"name": "25"}, + {"name": "56"}, + {"name": "98"}, + {"name": nil}, // This nil value may cause issues + } + + // Target template type + template := []map[string]any{} + + // Use ConvertType for conversion + result, err := _defaultConv.ConvertType(value, reflect.TypeOf(template)) + if err != nil { + t.Fatalf("ConvertType failed: %v", err) + } + + // Check results + resultSlice, ok := result.([]map[string]interface{}) + if !ok { + t.Fatalf("Expected []map[string]interface{}, got %T", result) + } + + // Verify result length should match original data + if len(resultSlice) != len(value) { + t.Errorf("Expected length %d, got %d", len(value), len(resultSlice)) + } + + // Verify each element + for i, original := range value { + if i >= len(resultSlice) { + t.Errorf("Missing element at index %d", i) + continue + } + + converted := resultSlice[i] + + // Check if key exists + originalName, originalExists := original["name"] + convertedName, convertedExists := converted["name"] + + if originalExists != convertedExists { + t.Errorf("Index %d: key existence mismatch. Original has 'name': %v, Converted has 'name': %v", + i, originalExists, convertedExists) + } + + if originalExists && convertedExists { + // If original value is nil, converted value should also be nil + if originalName == nil && convertedName != nil { + t.Errorf("Index %d: expected nil value, got %v", i, convertedName) + } else if originalName != nil && !reflect.DeepEqual(originalName, convertedName) { + t.Errorf("Index %d: value mismatch. Expected %v, got %v", i, originalName, convertedName) + } + } + } +} From 48cdf720f61b98788d1e8b0ef351bc95b89d7d8b Mon Sep 17 00:00:00 2001 From: cmstar Date: Tue, 14 Oct 2025 14:57:21 +0800 Subject: [PATCH 2/3] Simplify test cases of MapToMap. --- conv_test.go | 183 ++++++++++++++++++--------------------------------- 1 file changed, 65 insertions(+), 118 deletions(-) diff --git a/conv_test.go b/conv_test.go index b1746a7..bf9a393 100644 --- a/conv_test.go +++ b/conv_test.go @@ -584,136 +584,83 @@ func TestConv_MapToStruct(t *testing.T) { } func TestConv_MapToMap(t *testing.T) { - type args struct { - m interface{} - dstTyp reflect.Type + run := func(v, dst, want interface{}) { + got, err := _defaultConv.MapToMap(v, reflect.TypeOf(dst)) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("want %v, got %v", want, got) + } } - tests := []struct { - name string - args args - want interface{} - errRegex string - }{ - { - "nil", - args{ - map[string]int(nil), - reflect.TypeOf(map[float32]int(nil)), - }, - map[float32]int(nil), - "", - }, - { - "si-si", - args{ - map[string]int{ - "a": 1, - "b": 2, - "c": 3, - }, - reflect.TypeOf(map[string]int(nil)), - }, - map[string]int{ - "a": 1, - "b": 2, - "c": 3, - }, - "", - }, + t.Run("NilToNil", func(t *testing.T) { + run(map[string]int(nil), map[float32]int(nil), map[float32]int(nil)) + }) - { - "si-is", - args{ - map[string]int{ - "1": 11, - "2": 22, - "33": 3, - }, - reflect.TypeOf(map[int]string(nil)), - }, - map[int]string{ - 1: "11", - 2: "22", - 33: "3", - }, - "", - }, + t.Run("StringIntToStringInt", func(t *testing.T) { + v := map[string]int{ + "a": 1, + "b": 2, + "c": 3, + } + run(v, map[string]int(nil), v) + }) - { - "[]interface-interface", - args{ - map[string]interface{}{ - "1": []interface{}{1, 2, 3}, - }, - reflect.TypeOf(map[interface{}]interface{}(nil)), - }, - map[interface{}]interface{}{ - "1": []interface{}{1, 2, 3}, - }, - "", - }, + t.Run("StringIntToIntString", func(t *testing.T) { + v := map[string]int{ + "1": 11, + "2": 22, + "33": 3, + } + want := map[int]string{ + 1: "11", + 2: "22", + 33: "3", + } + run(v, map[int]string(nil), want) + }) - { - "err-src", - args{ - 123, - reflect.TypeOf(map[int]string(nil)), - }, - nil, - "must be a map", - }, + t.Run("StringAnyToAnyAny", func(t *testing.T) { + v := map[string]interface{}{ + "1": []interface{}{1, 2, 3}, + } + want := map[interface{}]interface{}{ + "1": []interface{}{1, 2, 3}, + } + run(v, map[interface{}]interface{}(nil), want) + }) - { - "err-typ", - args{ - map[string]int{}, - reflect.TypeOf(1), - }, - nil, - "destination type must be map", - }, + runErr := func(v, dst interface{}, errRegex string) { + got, err := _defaultConv.MapToMap(v, reflect.TypeOf(dst)) - { - "err-key", - args{ - map[string]int{"a": 1}, - reflect.TypeOf(map[int]int{}), - }, - nil, - "cannot covert key 'a' to int: .+", - }, + if err == nil { + t.Errorf("expected error matches: %s, got %v", errRegex, got) + } - { - "err-elem", - args{ - map[string]string{"aa": "x"}, - reflect.TypeOf(map[string]int{}), - }, - nil, - "cannot covert value of key 'aa' to int: .+", - }, + if match, _ := regexp.MatchString(errRegex, err.Error()); !match { + t.Errorf("error = %v , must match %v", + strconv.Quote(err.Error()), strconv.Quote(errRegex)) + } } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := _defaultConv.MapToMap(tt.args.m, tt.args.dstTyp) - if err != nil { - if tt.errRegex == "" { - t.Errorf("MapToMap() unexpected error = %v", err) - } + t.Run("ErrWrongSource", func(t *testing.T) { + runErr(123, map[int]string(nil), "must be a map") + }) - if match, _ := regexp.MatchString(tt.errRegex, err.Error()); !match { - t.Errorf("MapToMap() error = %v , must match %v", - strconv.Quote(err.Error()), strconv.Quote(tt.errRegex)) - } - } + t.Run("ErrWrongDestinationType", func(t *testing.T) { + runErr(map[string]int{}, 1, "destination type must be map") + }) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MapToMap() = %v, want %v", got, tt.want) - } - }) - } + t.Run("ErrConvertKey", func(t *testing.T) { + runErr(map[string]int{"a": 1}, map[int]int(nil), "cannot covert key 'a' to int: .+") + }) + + t.Run("ErrConvertValue", func(t *testing.T) { + runErr(map[string]string{"aa": "x"}, map[string]int(nil), "cannot covert value of key 'aa' to int: .+") + }) } func TestConv_StructToMap(t *testing.T) { From 423f7356ff6ff03296814ee6aa620b2848adc67e Mon Sep 17 00:00:00 2001 From: cmstar Date: Tue, 14 Oct 2025 14:59:17 +0800 Subject: [PATCH 3/3] Merge test case TestConv_mapToMapWithNilValues into TestConv_MapToMap, to reduce code redundancy. --- conv_test.go | 65 ++++------------------------------------------------ 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/conv_test.go b/conv_test.go index bf9a393..b7d26d9 100644 --- a/conv_test.go +++ b/conv_test.go @@ -625,10 +625,12 @@ func TestConv_MapToMap(t *testing.T) { t.Run("StringAnyToAnyAny", func(t *testing.T) { v := map[string]interface{}{ - "1": []interface{}{1, 2, 3}, + "1": []interface{}{1, 2, 3}, + "nil": nil, } want := map[interface{}]interface{}{ - "1": []interface{}{1, 2, 3}, + "1": []interface{}{1, 2, 3}, + "nil": nil, } run(v, map[interface{}]interface{}(nil), want) }) @@ -1710,62 +1712,3 @@ func TestConv_tryFlattenEmptyKeyMap(t *testing.T) { }) } } - -// TestConv_MapToMapWithNilValues tests the MapToMap function handling nil values. -func TestConv_mapToMapWithNilValues(t *testing.T) { - // Test data: map slice containing nil values - value := []map[string]interface{}{ - {"name": "25"}, - {"name": "56"}, - {"name": "98"}, - {"name": nil}, // This nil value may cause issues - } - - // Target template type - template := []map[string]any{} - - // Use ConvertType for conversion - result, err := _defaultConv.ConvertType(value, reflect.TypeOf(template)) - if err != nil { - t.Fatalf("ConvertType failed: %v", err) - } - - // Check results - resultSlice, ok := result.([]map[string]interface{}) - if !ok { - t.Fatalf("Expected []map[string]interface{}, got %T", result) - } - - // Verify result length should match original data - if len(resultSlice) != len(value) { - t.Errorf("Expected length %d, got %d", len(value), len(resultSlice)) - } - - // Verify each element - for i, original := range value { - if i >= len(resultSlice) { - t.Errorf("Missing element at index %d", i) - continue - } - - converted := resultSlice[i] - - // Check if key exists - originalName, originalExists := original["name"] - convertedName, convertedExists := converted["name"] - - if originalExists != convertedExists { - t.Errorf("Index %d: key existence mismatch. Original has 'name': %v, Converted has 'name': %v", - i, originalExists, convertedExists) - } - - if originalExists && convertedExists { - // If original value is nil, converted value should also be nil - if originalName == nil && convertedName != nil { - t.Errorf("Index %d: expected nil value, got %v", i, convertedName) - } else if originalName != nil && !reflect.DeepEqual(originalName, convertedName) { - t.Errorf("Index %d: value mismatch. Expected %v, got %v", i, originalName, convertedName) - } - } - } -}