From 72d670be0bdf7b873746c19eef1e0fd2a8d4976a Mon Sep 17 00:00:00 2001 From: what Date: Tue, 9 Dec 2025 16:55:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20map=20=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F=E5=86=99=E4=B8=8D=E6=95=8F=E6=84=9F=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=92=8C=E5=87=BD=E6=95=B0=E9=87=8D=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tryMapFieldKey 函数,返回 map 中实际存在的键(支持大小写转换) - 优化 setFieldValue 方法,使用 tryMapFieldKey 查找已存在字段并更新 - 优化 setNestedValue 方法,修复嵌套 map 大小写处理和 interface 包装问题 - 重命名函数以提高代码清晰度: - tryStructField → tryStructFieldValue - tryMapField → tryMapFieldValue - tryMapFieldWithKey → tryMapFieldKey - 新增 rfx_map_case_test.go 包含 16 个测试用例,覆盖基本、嵌套、边界等场景 --- rfx.go | 39 +++- rfx_map_case_test.go | 435 +++++++++++++++++++++++++++++++++++++++++++ util.go | 27 ++- 3 files changed, 485 insertions(+), 16 deletions(-) create mode 100644 rfx_map_case_test.go diff --git a/rfx.go b/rfx.go index c39503c..186edbd 100644 --- a/rfx.go +++ b/rfx.go @@ -131,7 +131,7 @@ func (r *rfx) setNestedValue(current reflect.Value, keys []string, v any) bool { switch current.Kind() { case reflect.Struct: - field := tryStructField(current, firstKey) + field := tryStructFieldValue(current, firstKey) if !field.IsValid() { return false } @@ -139,24 +139,34 @@ func (r *rfx) setNestedValue(current reflect.Value, keys []string, v any) bool { case reflect.Map: // Map 的特殊处理 - mapKey := reflect.ValueOf(firstKey) - mapValue := current.MapIndex(mapKey) + // 使用 tryMapFieldKey 获取实际的键 + actualKey := tryMapFieldKey(current, firstKey) + if !actualKey.IsValid() { + return false + } + mapValue := current.MapIndex(actualKey) if !mapValue.IsValid() { return false } + // 解开 interface 包装获取实际的值 + actualValue := mapValue + for actualValue.Kind() == reflect.Interface && !actualValue.IsNil() { + actualValue = actualValue.Elem() + } + // 创建 map 值的副本以便修改 - valueCopy := reflect.New(mapValue.Type()).Elem() - valueCopy.Set(mapValue) + valueCopy := reflect.New(actualValue.Type()).Elem() + valueCopy.Set(actualValue) // 在副本上递归设置值 if !r.setNestedValue(valueCopy, remainingKeys, v) { return false } - // 将修改后的值设置回 map - current.SetMapIndex(mapKey, valueCopy) + // 将修改后的值设置回 map,使用实际找到的键 + current.SetMapIndex(actualKey, valueCopy) return true case reflect.Slice, reflect.Array: @@ -200,7 +210,7 @@ func (r *rfx) setFieldValue(target reflect.Value, key string, v any) bool { switch target.Kind() { case reflect.Struct: - field := tryStructField(target, key) + field := tryStructFieldValue(target, key) if !field.IsValid() || !field.CanSet() { return false } @@ -210,6 +220,19 @@ func (r *rfx) setFieldValue(target reflect.Value, key string, v any) bool { target.Set(reflect.MakeMap(target.Type())) } + // 先尝试使用 tryMapFieldKey 检查字段是否已存在并获取实际的键 + actualKey := tryMapFieldKey(target, key) + if actualKey.IsValid() { + // 字段已存在,创建新值用于设置 + newValue := reflect.New(target.Type().Elem()).Elem() + if !r.setValue(newValue, v) { + return false + } + target.SetMapIndex(actualKey, newValue) + return true + } + + // 字段不存在,创建新的 map 值 // 处理 nil 值的情况 if v == nil { target.SetMapIndex(reflect.ValueOf(key), reflect.Zero(target.Type().Elem())) diff --git a/rfx_map_case_test.go b/rfx_map_case_test.go new file mode 100644 index 0000000..ea2fcc8 --- /dev/null +++ b/rfx_map_case_test.go @@ -0,0 +1,435 @@ +package reflux + +import "testing" + +// TestMapCaseInsensitiveKeys 测试 map 大小写不敏感的键访问 +func TestMapCaseInsensitiveKeys(t *testing.T) { + t.Run("Get with lowercase key when map has uppercase", func(t *testing.T) { + m := map[string]any{ + "name": "Alice", + "age": 30, + } + rfx := New(&m) + + // 使用大写键访问小写键 + name := rfx.Get("Name").String() + if name != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", name) + } + + age := rfx.Get("Age").Int() + if age != 30 { + t.Errorf("Expected 30, got %d", age) + } + }) + + t.Run("Set with uppercase key to map with lowercase keys", func(t *testing.T) { + m := map[string]any{ + "name": "Alice", + } + rfx := New(&m) + + // 使用大写键设置值 + rfx.Set("Name", "Bob") + + // 验证小写键被更新 + if m["name"] != "Bob" { + t.Errorf("Expected 'Bob', got '%v'", m["name"]) + } + }) + + t.Run("Set with lowercase key creates new entry", func(t *testing.T) { + m := map[string]any{ + "Name": "Alice", + } + rfx := New(&m) + + // 使用小写键设置值(应该创建新条目) + rfx.Set("age", 25) + + // 验证新条目被创建 + if m["age"] != 25 { + t.Errorf("Expected 25, got '%v'", m["age"]) + } + + // 验证原始大写键仍然存在 + if m["Name"] != "Alice" { + t.Errorf("Expected 'Alice', got '%v'", m["Name"]) + } + }) + + t.Run("Exists with case-insensitive key", func(t *testing.T) { + m := map[string]any{ + "name": "Alice", + } + rfx := New(&m) + + // 使用大写键检查是否存在 + if !rfx.Exists("Name") { + t.Error("Expected key 'Name' to exist (case-insensitive)") + } + + // 使用小写键检查 + if !rfx.Exists("name") { + t.Error("Expected key 'name' to exist") + } + }) +} + +// TestMapNestedCaseInsensitiveKeys 测试嵌套 map 的大小写不敏感访问 +func TestMapNestedCaseInsensitiveKeys(t *testing.T) { + t.Run("Get nested map with mixed case keys", func(t *testing.T) { + m := map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 30, + }, + } + rfx := New(&m) + + // 使用大写键访问嵌套的小写键 + name := rfx.Get("User", "Name").String() + if name != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", name) + } + + age := rfx.Get("User", "Age").Int() + if age != 30 { + t.Errorf("Expected 30, got %d", age) + } + }) + + t.Run("Set nested map with mixed case keys", func(t *testing.T) { + m := map[string]any{ + "user": map[string]any{ + "name": "Alice", + }, + } + rfx := New(&m) + + // 使用大写键设置嵌套值 + rfx.Set("User.Name", "Bob") + + // 验证值被更新 + userMap := m["user"].(map[string]any) + if userMap["name"] != "Bob" { + t.Errorf("Expected 'Bob', got '%v'", userMap["name"]) + } + }) + + t.Run("Set deeply nested map with case-insensitive keys", func(t *testing.T) { + m := map[string]any{ + "data": map[string]any{ + "user": map[string]any{ + "profile": map[string]any{ + "name": "Alice", + }, + }, + }, + } + rfx := New(&m) + + // 使用大写键访问深层嵌套 + rfx.Set("Data.User.Profile.Name", "Charlie") + + // 验证值被更新 + dataMap := m["data"].(map[string]any) + userMap := dataMap["user"].(map[string]any) + profileMap := userMap["profile"].(map[string]any) + if profileMap["name"] != "Charlie" { + t.Errorf("Expected 'Charlie', got '%v'", profileMap["name"]) + } + }) + + t.Run("Get from nested map with all uppercase path", func(t *testing.T) { + m := map[string]any{ + "company": map[string]any{ + "department": map[string]any{ + "name": "Engineering", + "employee": map[string]any{"count": 50}, + }, + }, + } + rfx := New(&m) + + // 使用全大写路径访问 + name := rfx.Get("Company", "Department", "Name").String() + if name != "Engineering" { + t.Errorf("Expected 'Engineering', got '%s'", name) + } + + count := rfx.Get("Company", "Department", "Employee", "Count").Int() + if count != 50 { + t.Errorf("Expected 50, got %d", count) + } + }) + + t.Run("Set creates nested structure with mixed case", func(t *testing.T) { + m := map[string]any{ + "settings": map[string]any{}, + } + rfx := New(&m) + + // 设置嵌套值,使用大写键访问 + rfx.Set("Settings.theme", "dark") + rfx.Set("Settings.language", "en") + + settingsMap := m["settings"].(map[string]any) + if settingsMap["theme"] != "dark" { + t.Errorf("Expected 'dark', got '%v'", settingsMap["theme"]) + } + if settingsMap["language"] != "en" { + t.Errorf("Expected 'en', got '%v'", settingsMap["language"]) + } + }) +} + +// TestMapCaseInsensitiveWithStructs 测试 map 和 struct 混合时的大小写处理 +func TestMapCaseInsensitiveWithStructs(t *testing.T) { + type Address struct { + City string + Street string + } + + type User struct { + Name string + Age int + Address Address + } + + t.Run("Struct embedded in map with case-insensitive access", func(t *testing.T) { + m := map[string]any{ + "user": User{ + Name: "Alice", + Age: 30, + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + }, + } + rfx := New(&m) + + // 使用大写 User 访问小写 user 键 + name := rfx.Get("User", "Name").String() + if name != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", name) + } + + city := rfx.Get("User", "Address", "City").String() + if city != "Beijing" { + t.Errorf("Expected 'Beijing', got '%s'", city) + } + }) + + t.Run("Set struct field through map with case-insensitive key (pointer)", func(t *testing.T) { + m := map[string]any{ + "user": &User{ + Name: "Alice", + Age: 30, + }, + } + rfx := New(&m) + + // 使用大写键路径设置 struct 字段(通过指针) + rfx.Set("User.Name", "Bob") + + user := m["user"].(*User) + if user.Name != "Bob" { + t.Errorf("Expected 'Bob', got '%s'", user.Name) + } + }) + + t.Run("Map inside struct with case-insensitive access", func(t *testing.T) { + type Config struct { + Settings map[string]any + } + + config := Config{ + Settings: map[string]any{ + "theme": "light", + "lang": "en", + }, + } + rfx := New(&config) + + // 使用大写键访问 map 中的小写键 + theme := rfx.Get("Settings", "Theme").String() + if theme != "light" { + t.Errorf("Expected 'light', got '%s'", theme) + } + + // 设置值 + rfx.Set("Settings.Lang", "zh") + if config.Settings["lang"] != "zh" { + t.Errorf("Expected 'zh', got '%v'", config.Settings["lang"]) + } + }) +} + +// TestMapCaseInsensitiveEdgeCases 测试大小写处理的边界情况 +func TestMapCaseInsensitiveEdgeCases(t *testing.T) { + t.Run("Single character keys", func(t *testing.T) { + m := map[string]any{ + "a": 1, + "b": 2, + } + rfx := New(&m) + + // 使用大写访问小写 + valA := rfx.Get("A").Int() + if valA != 1 { + t.Errorf("Expected 1, got %d", valA) + } + + valB := rfx.Get("B").Int() + if valB != 2 { + t.Errorf("Expected 2, got %d", valB) + } + }) + + t.Run("Unicode characters in keys", func(t *testing.T) { + m := map[string]any{ + "名字": "Alice", + "年龄": 30, + } + rfx := New(&m) + + name := rfx.Get("名字").String() + if name != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", name) + } + }) + + t.Run("Multiple nested maps with same key names", func(t *testing.T) { + m := map[string]any{ + "level1": map[string]any{ + "name": "L1", + "level2": map[string]any{ + "name": "L2", + "level3": map[string]any{ + "name": "L3", + }, + }, + }, + } + rfx := New(&m) + + // 使用大写路径访问 + l1Name := rfx.Get("Level1", "Name").String() + if l1Name != "L1" { + t.Errorf("Expected 'L1', got '%s'", l1Name) + } + + l2Name := rfx.Get("Level1", "Level2", "Name").String() + if l2Name != "L2" { + t.Errorf("Expected 'L2', got '%s'", l2Name) + } + + l3Name := rfx.Get("Level1.Level2.Level3.Name").String() + if l3Name != "L3" { + t.Errorf("Expected 'L3', got '%s'", l3Name) + } + }) + + t.Run("Set value updates existing lowercase key", func(t *testing.T) { + m := map[string]any{ + "config": map[string]any{ + "timeout": 30, + "retry": 3, + }, + } + rfx := New(&m) + + // 使用大写键设置已存在的小写键 + rfx.Set("Config.Timeout", 60) + rfx.Set("Config.Retry", 5) + + configMap := m["config"].(map[string]any) + if configMap["timeout"] != 60 { + t.Errorf("Expected 60, got %v", configMap["timeout"]) + } + if configMap["retry"] != 5 { + t.Errorf("Expected 5, got %v", configMap["retry"]) + } + }) + + t.Run("Mixed case keys with numbers", func(t *testing.T) { + m := map[string]any{ + "server1": map[string]any{"host": "localhost"}, + "server2": map[string]any{"host": "example.com"}, + } + rfx := New(&m) + + host1 := rfx.Get("Server1", "Host").String() + if host1 != "localhost" { + t.Errorf("Expected 'localhost', got '%s'", host1) + } + + host2 := rfx.Get("Server2.Host").String() + if host2 != "example.com" { + t.Errorf("Expected 'example.com', got '%s'", host2) + } + }) +} + +// TestMapCaseInsensitiveSetWithTypeConversion 测试设置值时的类型转换和大小写处理 +func TestMapCaseInsensitiveSetWithTypeConversion(t *testing.T) { + t.Run("Set with type conversion on existing lowercase key", func(t *testing.T) { + m := map[string]any{ + "config": map[string]int{ + "timeout": 30, + }, + } + rfx := New(&m) + + // 使用大写键和字符串值设置 int 类型的 map + rfx.Set("Config.Timeout", "60") + + configMap := m["config"].(map[string]int) + if configMap["timeout"] != 60 { + t.Errorf("Expected 60, got %v", configMap["timeout"]) + } + }) + + t.Run("Set nested value with multiple type conversions", func(t *testing.T) { + m := map[string]any{ + "data": map[string]any{ + "stats": map[string]float64{ + "rate": 0.5, + }, + }, + } + rfx := New(&m) + + // 使用大写路径和整数值设置 float64 + rfx.Set("Data.Stats.Rate", 100) + + dataMap := m["data"].(map[string]any) + statsMap := dataMap["stats"].(map[string]float64) + if statsMap["rate"] != 100.0 { + t.Errorf("Expected 100.0, got %v", statsMap["rate"]) + } + }) + + t.Run("Set with exact case match updates existing key", func(t *testing.T) { + m := map[string]any{ + "user": map[string]any{ + "name": "Alice", + }, + } + rfx := New(&m) + + // 使用大写键访问小写键,然后设置新字段 + rfx.Set("User.age", 30) + + // 验证在现有的小写键中添加了新字段 + if userMap, ok := m["user"].(map[string]any); ok { + if userMap["age"] != 30 { + t.Errorf("Expected 30, got %v", userMap["age"]) + } + } else { + t.Error("Expected 'user' key to contain the new field") + } + }) +} diff --git a/util.go b/util.go index 9d49563..4e9f094 100644 --- a/util.go +++ b/util.go @@ -209,9 +209,9 @@ func getValueByPath(v reflect.Value, p ...string) reflect.Value { switch v.Kind() { case reflect.Struct: - v = tryStructField(v, key) + v = tryStructFieldValue(v, key) case reflect.Map: - v = tryMapField(v, key) + v = tryMapFieldValue(v, key) case reflect.Slice, reflect.Array: // 尝试将 key 转换为索引 idx, err := strconv.Atoi(key) @@ -257,10 +257,10 @@ func lowercaseFirst(s string) string { return string(runes) } -// tryStructField 尝试获取 struct 字段,支持小写字段名自动转大写 +// tryStructFieldValue 尝试获取 struct 字段,支持小写字段名自动转大写 // 1. 首先尝试原始字段名 // 2. 如果失败且首字母是小写,尝试首字母大写的版本 -func tryStructField(v reflect.Value, fieldName string) reflect.Value { +func tryStructFieldValue(v reflect.Value, fieldName string) reflect.Value { // 首先尝试原始字段名 field := v.FieldByName(fieldName) if field.IsValid() { @@ -279,10 +279,21 @@ func tryStructField(v reflect.Value, fieldName string) reflect.Value { return reflect.Value{} } -// tryMapField 尝试从 map 中获取值,支持 string 键的小写字段名自动转大写 +// tryMapFieldValue 尝试从 map 中获取值,支持 string 键的小写字段名自动转大写 // 1. 首先使用原始 key 尝试 // 2. 如果 map 的键类型为 string 且 key 首字母是大写,再尝试首字母小写的版本 -func tryMapField(m reflect.Value, key string) reflect.Value { +func tryMapFieldValue(m reflect.Value, key string) reflect.Value { + actualKey := tryMapFieldKey(m, key) + if !actualKey.IsValid() { + return reflect.Value{} + } + return m.MapIndex(actualKey) +} + +// tryMapFieldKey 尝试从 map 中获取实际存在的键 +// 支持大小写不敏感查找 +// 返回: 实际的键 +func tryMapFieldKey(m reflect.Value, key string) reflect.Value { if !m.IsValid() || m.Kind() != reflect.Map { return reflect.Value{} } @@ -308,7 +319,7 @@ func tryMapField(m reflect.Value, key string) reflect.Value { // 尝试原始 key val := m.MapIndex(mapKey) if val.IsValid() { - return val + return mapKey } // 如果键类型是 string 且首字母大写,尝试首字母小写版本 @@ -317,7 +328,7 @@ func tryMapField(m reflect.Value, key string) reflect.Value { mapKey = reflect.ValueOf(lowercased) val = m.MapIndex(mapKey) if val.IsValid() { - return val + return mapKey } }