commit d9f178020a1c3d8173b089f22116babf73825a4f Author: what Date: Tue Dec 2 19:52:29 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..680acce --- /dev/null +++ b/README.md @@ -0,0 +1,685 @@ +# Reflux + +Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌套的结构体字段、切片元素和映射值。通过字符串路径的方式,可以方便地访问和修改深层嵌套的数据结构。 + +## 特性 + +- 🔍 **统一访问**: 使用字符串路径访问结构体、Map、切片中的任意嵌套值 +- 🎯 **点号路径**: 支持点号分割的路径语法,如 `Get("Address.City")` +- 🔄 **类型转换**: 基于 [spf13/cast](https://github.com/spf13/cast) 的强大类型转换支持 +- ✏️ **修改数据**: 支持通过路径设置和删除值 +- 🔗 **链式调用**: Set 和 Delete 方法支持链式调用,如 `rfx.Set("name", "Alice").Delete("age").Set("city", "Beijing")` +- 📋 **深度克隆**: Scope 方法创建深度克隆,修改不影响原始数据 +- 🔤 **大小写不敏感**: 支持使用小写字段名访问和修改结构体字段 +- 🔑 **键名提取**: Keys 方法可获取结构体或 Map 的所有键名 +- 📝 **JSON 集成**: Ptr() 方法返回的指针可直接用于 json.Unmarshal(),实现动态 JSON 反序列化 +- 🎯 **类型安全**: 使用反射但保证类型安全 +- 🚀 **高性能**: 优化的反射操作,低内存开销 +- 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast + +## 安装 + +```bash +go get git.fsdpf.net/go/reflux +``` + +## 快速开始 + +```go +package main + +import ( + "fmt" + "git.fsdpf.net/go/reflux" +) + +type User struct { + Name string + Age int + Address struct { + City string + } +} + +func main() { + user := User{ + Name: "Alice", + Age: 30, + } + user.Address.City = "Beijing" + + rfx := reflux.New(&user) + + // 获取值 + name := rfx.Get("Name").String() + age := rfx.Get("Age").Int() + city := rfx.Get("Address", "City").String() + + fmt.Printf("Name: %s, Age: %d, City: %s\n", name, age, city) + + // 链式设置值 + rfx.Set("Name", "Bob").Set("Age", 35).Set("Address.City", "Shanghai") + + fmt.Printf("Updated: %+v\n", user) +} +``` + +## 核心功能 + +### 1. 创建 Reflux + +```go +user := User{Name: "Alice"} +rfx := reflux.New(&user) // 传入指针以支持修改 +``` + +**重要**: `New()` 函数必须传入指针类型,否则会 panic。 + +### 2. 获取值 (Get) + +```go +// 获取顶层字段 +name := rfx.Get("Name").String() + +// 获取嵌套字段 - 使用多个参数 +city := rfx.Get("Address", "City").String() + +// 获取嵌套字段 - 使用点号路径 +city := rfx.Get("Address.City").String() + +// 深层嵌套 - 点号路径 +ceoCity := rfx.Get("Company.CEO.Address.City").String() + +// 混合使用点号和参数 +value := rfx.Get("User.Profile", "Settings", "Theme").String() + +// 获取切片元素 - 使用索引 +tag := rfx.Get("Tags", "0").String() + +// 获取切片元素 - 使用点号 +tag := rfx.Get("Tags.0").String() + +// 获取 Map 值 - 使用多个参数 +value := rfx.Get("Meta", "key").String() + +// 获取 Map 值 - 使用点号 +value := rfx.Get("Meta.key").String() + +// 大小写不敏感访问 (自动转换为正确的字段名) +name := rfx.Get("name").String() // 等同于 Get("Name") +city := rfx.Get("address.city").String() // 等同于 Get("Address.City") +``` + +### 3. 作用域 (Scope) + +`Scope` 方法创建指定路径的**深度克隆**,在克隆上的修改不会影响原始数据: + +```go +type Address struct { + City string + Street string +} + +type Person struct { + Name string + Address Address +} + +person := Person{ + Name: "Alice", + Address: Address{ + City: "Beijing", + Street: "Main St", + }, +} + +rfx := reflux.New(&person) + +// 创建 Address 字段的深度克隆 +addressScope := rfx.Scope("Address") + +// 在克隆上修改值 +addressScope.Set("City", "Shanghai") +addressScope.Set("Street", "New St") + +// 原始数据不受影响 +fmt.Println(person.Address.City) // 输出: Beijing +fmt.Println(person.Address.Street) // 输出: Main St + +// 克隆数据已修改 +fmt.Println(addressScope.Get("City").String()) // 输出: Shanghai +fmt.Println(addressScope.Get("Street").String()) // 输出: New St + +// 不传参数时,克隆整个对象 +clone := rfx.Scope() +clone.Set("Name", "Bob") +fmt.Println(person.Name) // 输出: Alice (原始数据不变) +``` + +**重要**: `Scope` 返回的是深度克隆,与原始数据完全独立。 + +### 4. 设置值 (Set) + +`Set` 方法使用新的 API 签名: `Set(key string, v any) Reflux`,支持链式调用: + +```go +// 链式设置多个值 +rfx.Set("Name", "Bob").Set("Age", 35).Set("Address.City", "Shanghai") + +// 设置顶层字段 +rfx.Set("Name", "Bob") + +// 设置嵌套字段 - 使用点号路径 +rfx.Set("Address.City", "Shanghai") + +// 深层嵌套设置 +rfx.Set("Company.CEO.Address.City", "Guangzhou") + +// 设置 Map 值 +rfx.Set("Meta.key", "value") + +// 设置切片元素 +rfx.Set("Tags.0", "newValue") + +// 大小写不敏感设置 +rfx.Set("name", "Bob") // 等同于 Set("Name", "Bob") +rfx.Set("address.city", "Shanghai") // 等同于 Set("Address.City", "Shanghai") + +// 自动类型转换 +rfx.Set("Age", int32(35)) // int32 -> int + +// Map 自动初始化 +rfx.Set("NewMap.key", "value") // 如果 NewMap 是 nil,会自动初始化 +``` + +**注意**: 如果设置失败(路径不存在、类型不匹配等),会 panic。 + +### 5. 删除值 (Delete) + +`Delete` 方法支持链式调用,返回 Reflux 自身: + +```go +// 删除 Map 键 - 使用多个参数 +rfx.Delete("Meta", "key") + +// 删除 Map 键 - 使用点号 +rfx.Delete("Meta.key") + +// 删除切片元素 - 使用索引 +rfx.Delete("Tags", "1") // 删除索引 1 的元素 + +// 删除切片元素 - 使用点号 +rfx.Delete("Tags.1") + +// 链式调用 - 删除多个值 +rfx.Delete("Meta.key1").Delete("Meta.key2").Set("Meta.key3", "value") +``` + +**注意**: 如果删除失败(路径不存在、类型不支持删除等),会 panic。 + +### 6. 检查存在性 (Exists) + +```go +if rfx.Exists("Name") { + fmt.Println("Name field exists") +} + +// 使用多个参数检查嵌套字段 +if rfx.Exists("Address", "City") { + fmt.Println("Nested field exists") +} + +// 使用点号路径检查嵌套字段 +if rfx.Exists("Address.City") { + fmt.Println("Nested field exists") +} + +// 检查深层嵌套 +if rfx.Exists("Company.CEO.Address.City") { + fmt.Println("Deep nested field exists") +} +``` + +### 7. 获取键名 (Keys) + +```go +type User struct { + Name string + Age int +} + +user := User{Name: "Alice", Age: 30} +rfx := reflux.New(&user) + +// 获取结构体的所有字段名 +keys := rfx.Keys() +fmt.Println(keys) // 输出: [Name Age] + +// 对于 Map +config := map[string]any{ + "host": "localhost", + "port": 8080, +} +sm2 := reflux.New(&config) +keys2 := sm2.Keys() +fmt.Println(keys2) // 输出: [host port] + +// 获取嵌套对象的键名 +addressKeys := rfx.Get("Address").Keys() +``` + +### 8. 数组操作 (Array) + +```go +// 将切片转换为 Reflux 数组 +tags := rfx.Get("Tags").Array() +for i, tag := range tags { + fmt.Printf("Tag[%d]: %s\n", i, tag.String()) +} + +// 可以对数组元素进行进一步操作 +users := rfx.Get("Users").Array() +for i, user := range users { + name := user.Get("Name").String() + age := user.Get("Age").Int() + fmt.Printf("User[%d]: %s, %d\n", i, name, age) +} +``` + +### 9. 类型转换 + +#### 基本类型 + +```go +age := rfx.Get("Age").Int() // int +age64 := rfx.Get("Age").Int64() // int64 +age32 := rfx.Get("Age").Int32() // int32 +age16 := rfx.Get("Age").Int16() // int16 +age8 := rfx.Get("Age").Int8() // int8 +uage := rfx.Get("Age").Uint() // uint +uage64 := rfx.Get("Age").Uint64() // uint64 +uage32 := rfx.Get("Age").Uint32() // uint32 +uage16 := rfx.Get("Age").Uint16() // uint16 +uage8 := rfx.Get("Age").Uint8() // uint8 +fage := rfx.Get("Age").Float64() // float64 +fage32 := rfx.Get("Age").Float32() // float32 +sage := rfx.Get("Age").String() // string +active := rfx.Get("Active").Bool() // bool +``` + +**注意**: 类型转换使用 [spf13/cast](https://github.com/spf13/cast) 库,支持智能类型转换。转换失败会 panic。 + +#### Map 类型 + +```go +// map[string]string +strMap := rfx.Get("Meta").StringMapString() + +// map[string]int +intMap := rfx.Get("Scores").StringMapInt() + +// map[string]int64 +int64Map := rfx.Get("Scores").StringMapInt64() + +// map[string]bool +boolMap := rfx.Get("Flags").StringMapBool() + +// map[string]any +anyMap := rfx.Get("Data").StringMap() + +// map[string][]string +sliceMap := rfx.Get("Tags").StringMapStringSlice() +``` + +#### 切片类型 + +```go +// []string +tags := rfx.Get("Tags").StringSlice() + +// []int +scores := rfx.Get("Scores").IntSlice() + +// []bool +flags := rfx.Get("Flags").BoolSlice() + +// []any +items := rfx.Get("Items").Slice() +``` + +### 10. 底层访问 + +```go +// 获取 reflect.Value +val := rfx.Get("Name").Value() + +// 获取指针 +ptr := rfx.Get("Name").Ptr() + +// 获取 any 类型 +any := rfx.Get("Name").Any() +``` + +#### 与 json.Unmarshal() 配合使用 + +`Ptr()` 方法返回的指针可以直接用于 `json.Unmarshal()`,实现动态的 JSON 反序列化: + +```go +import ( + "encoding/json" + "git.fsdpf.net/go/reflux" +) + +type Person struct { + Name string + Age int + Address Address + Meta map[string]string + Tags []string +} + +type Address struct { + City string + Street string + ZipCode int +} + +func main() { + p := Person{ + Name: "Alice", + Address: Address{City: "OldCity"}, + Meta: make(map[string]string), + } + + rfx := reflux.New(&p) + + // 1. 更新嵌套结构体 + addressJSON := []byte(`{ + "City": "Shanghai", + "Street": "Nanjing Road", + "ZipCode": 200000 + }`) + json.Unmarshal(addressJSON, rfx.Get("Address").Ptr()) + // p.Address 已被完整更新 + + // 2. 更新 Map 字段 + metaJSON := []byte(`{ + "key1": "value1", + "key2": "value2" + }`) + json.Unmarshal(metaJSON, rfx.Get("Meta").Ptr()) + // p.Meta 已被填充 + + // 3. 更新切片字段 + tagsJSON := []byte(`["go", "rust", "python"]`) + json.Unmarshal(tagsJSON, rfx.Get("Tags").Ptr()) + // p.Tags 已被替换 + + // 4. 更新整个对象 + personJSON := []byte(`{ + "Name": "Bob", + "Age": 35 + }`) + json.Unmarshal(personJSON, rfx.Ptr()) + // 整个 Person 对象被更新 +} +``` + +**使用场景**: +- 动态配置更新: 从 JSON 文件或 API 响应更新配置的特定部分 +- 部分数据刷新: 只更新对象的某个嵌套字段,其他字段保持不变 +- 插件系统: 动态加载和更新插件配置 +- 热更新: 在运行时更新应用配置而无需重启 + +## 使用场景 + +### 1. 配置文件处理 + +```go +type Config struct { + Database struct { + Host string + Port int + } + Redis struct { + Addr string + } +} + +config := loadConfig() +rfx := reflux.New(&config) + +// 动态读取配置 +dbHost := rfx.Get("Database", "Host").String() +dbPort := rfx.Get("Database", "Port").Int() + +// 链式修改配置 +rfx.Set("Database.Host", "localhost").Set("Database.Port", 3306) +``` + +### 2. API 响应处理 + +```go +response := map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 30, + }, + "status": "success", +} + +rfx := reflux.New(&response) +userName := rfx.Get("user", "name").String() +userAge := rfx.Get("user", "age").Int() + +// 修改响应数据 +rfx.Set("user.name", "Bob").Set("status", "updated") +``` + +### 3. 动态表单数据 + +```go +formData := map[string]any{ + "name": "Bob", + "email": "bob@example.com", + "age": "25", // 字符串形式的数字 +} + +rfx := reflux.New(&formData) + +// 自动类型转换 +name := rfx.Get("name").String() +age := rfx.Get("age").Int() // "25" -> 25 + +// 验证和修改 +if age < 18 { + rfx.Set("verified", false) +} +``` + +### 4. 测试数据构建 + +```go +testUser := User{} +rfx := reflux.New(&testUser) + +// 链式快速设置测试数据 +rfx.Set("Name", "TestUser"). + Set("Age", 25). + Set("Address.City", "Beijing"). + Set("Address.Street", "Test St") +``` + +### 5. 数据克隆和隔离 + +```go +original := Config{Host: "localhost"} +rfx := reflux.New(&original) + +// 创建深度克隆进行测试 +testConfig := rfx.Scope() +testConfig.Set("Host", "test-server").Set("Port", 9999) + +// 原始配置不受影响 +fmt.Println(original.Host) // 输出: localhost +``` + +### 6. JSON 动态更新 + +使用 `Ptr()` 方法配合 `json.Unmarshal()` 实现动态更新: + +```go +type AppConfig struct { + Server ServerConfig + Database DatabaseConfig + Features map[string]bool +} + +type ServerConfig struct { + Host string + Port int +} + +type DatabaseConfig struct { + Driver string + DSN string +} + +func main() { + config := AppConfig{ + Server: ServerConfig{ + Host: "localhost", + Port: 8080, + }, + Features: make(map[string]bool), + } + + rfx := reflux.New(&config) + + // 从配置文件或 API 只更新 Server 配置 + serverJSON := []byte(`{ + "Host": "production.example.com", + "Port": 443 + }`) + json.Unmarshal(serverJSON, rfx.Get("Server").Ptr()) + + // 动态启用功能开关 + featuresJSON := []byte(`{ + "newFeature": true, + "experimentalUI": false + }`) + json.Unmarshal(featuresJSON, rfx.Get("Features").Ptr()) + + // config.Server 已更新,config.Database 保持不变 + fmt.Printf("%+v\n", config) +} +``` + +这种方式特别适合: +- **微服务配置**: 从配置中心动态更新特定模块配置 +- **A/B 测试**: 实时更新功能开关 +- **插件热加载**: 更新插件配置而无需重启 +- **API 部分响应**: 只处理 API 返回的部分字段 + +## 路径语法 + +Reflux 支持灵活的点号路径语法: + +```go +// 以下调用是等价的: +rfx.Get("a.b.c") +rfx.Get("a.b", "c") +rfx.Get("a", "b.c") +rfx.Get("a", "b", "c") + +// 空段会被忽略 +rfx.Get("a..b") // 等价于 rfx.Get("a", "b") +rfx.Get(".a.b.") // 等价于 rfx.Get("a", "b") + +// 点号路径适用于所有方法 +rfx.Set("a.b.c", value) +rfx.Delete("a.b.c") +rfx.Exists("a.b.c") +rfx.Scope("a.b.c") + +// 访问切片元素 +tag := rfx.Get("Tags.0").String() + +// 访问 Map 值 +value := rfx.Get("Config.Database.Host").String() +``` + +## API 文档 + +### Reflux 接口 + +```go +type Reflux interface { + // 路径操作 + Get(p ...string) Reflux + Scope(p ...string) Reflux + + // 修改操作 (支持链式调用) + Set(key string, v any) Reflux + Delete(p ...string) Reflux + Exists(p ...string) bool + + // 数组和键操作 + Array() []Reflux + Keys() []string + + // 底层访问 + Value() reflect.Value + Ptr() any + Any() any + + // 基本类型转换 + Bool() bool + Int() int + Int8() int8 + Int16() int16 + Int32() int32 + Int64() int64 + Uint() uint + Uint8() uint8 + Uint16() uint16 + Uint32() uint32 + Uint64() uint64 + Float32() float32 + Float64() float64 + String() string + + // Map 类型转换 + StringMapString() map[string]string + StringMapStringSlice() map[string][]string + StringMapBool() map[string]bool + StringMapInt() map[string]int + StringMapInt64() map[string]int64 + StringMap() map[string]any + + // Slice 类型转换 + Slice() []any + BoolSlice() []bool + StringSlice() []string + IntSlice() []int +} +``` + +## 注意事项 + +1. **指针传递**: `New()` 函数必须传递指针,否则会 panic +2. **Scope 行为**: `Scope` 返回深度克隆,修改不会影响原始数据 +3. **链式调用**: Set 和 Delete 方法都支持链式调用,返回 Reflux 自身 +4. **类型转换**: 使用 spf13/cast 进行类型转换,转换失败会 panic +5. **切片删除**: 删除切片元素会创建新切片并重新赋值 +6. **Map 初始化**: 对 nil map 调用 Set 会自动初始化 map +7. **大小写不敏感**: 支持使用小写字段名访问结构体字段 +8. **JSON 集成**: Ptr() 返回的指针可以安全地用于 json.Unmarshal() +9. **并发安全**: Reflux 本身不是并发安全的,需要外部同步 +10. **错误处理**: 大多数转换、设置和删除操作失败时会 panic,请确保路径和类型正确 + +## 示例代码 + +查看测试文件获取更多示例: +- [structmap_test.go](structmap_test.go) - 核心功能测试 +- [structmap_example_test.go](structmap_example_test.go) - 可执行示例 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..80ee4ef --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.fsdpf.net/go/reflux + +go 1.23.10 + +require github.com/spf13/cast v1.10.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7708012 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= diff --git a/reflux.go b/reflux.go new file mode 100644 index 0000000..918a26f --- /dev/null +++ b/reflux.go @@ -0,0 +1,131 @@ +package reflux + +import "reflect" + +// R 提供了一个统一的接口,用于访问和操作嵌套的结构体字段、切片元素和映射值 +type R interface { + // Get 通过路径获取嵌套字段的值,返回一个新的 R 实例 + // 参数 p 为路径片段,例如 Get("user", "profile", "name") + Get(p ...string) R + + // Scope 类似 Get,但用于创建一个指定路径的作用域视图 + // 后续操作将基于这个作用域进行 + Scope(p ...string) R + + // Set 设置指定路径的值,支持链式调用 + // 参数 key 为路径(支持点号分割),v 为要设置的值 + // 返回当前 R 实例以支持链式调用 + // 示例: rfx.Set("name", "Alice").Set("age", 30) + Set(key string, v any) R + + // Delete 删除指定路径的值 + // 参数 p 为路径片段 + // 返回当前 R 实例以支持链式调用 + Delete(p ...string) R + + // Exists 检查指定路径的值是否存在 + // 参数 p 为路径片段 + // 返回 true 表示存在,false 表示不存在 + Exists(p ...string) bool + + // Array 将当前值转换为 R 切片 + // 适用于数组或切片类型的值 + Array() []R + + // Keys 返回当前映射或结构体的所有键名 + Keys() []string + + // Value 返回底层的 reflect.Value + // 用于需要直接操作反射值的场景 + Value() reflect.Value + + // Ptr 返回指向当前值的指针 + Ptr() any + + // Any 将当前值转换为 any 类型 + Any() any + + // Bool 将当前值转换为 bool 类型 + Bool() bool + + // Float64 将当前值转换为 float64 类型 + Float64() float64 + + // Float32 将当前值转换为 float32 类型 + Float32() float32 + + // Int64 将当前值转换为 int64 类型 + Int64() int64 + + // Int32 将当前值转换为 int32 类型 + Int32() int32 + + // Int16 将当前值转换为 int16 类型 + Int16() int16 + + // Int8 将当前值转换为 int8 类型 + Int8() int8 + + // Int 将当前值转换为 int 类型 + Int() int + + // Uint 将当前值转换为 uint 类型 + Uint() uint + + // Uint64 将当前值转换为 uint64 类型 + Uint64() uint64 + + // Uint32 将当前值转换为 uint32 类型 + Uint32() uint32 + + // Uint16 将当前值转换为 uint16 类型 + Uint16() uint16 + + // Uint8 将当前值转换为 uint8 类型 + Uint8() uint8 + + // String 将当前值转换为 string 类型 + String() string + + // StringMapString 将当前值转换为 map[string]string 类型 + StringMapString() map[string]string + + // StringMapStringSlice 将当前值转换为 map[string][]string 类型 + StringMapStringSlice() map[string][]string + + // StringMapBool 将当前值转换为 map[string]bool 类型 + StringMapBool() map[string]bool + + // StringMapInt 将当前值转换为 map[string]int 类型 + StringMapInt() map[string]int + + // StringMapInt64 将当前值转换为 map[string]int64 类型 + StringMapInt64() map[string]int64 + + // StringMap 将当前值转换为 map[string]any 类型 + StringMap() map[string]any + + // Slice 将当前值转换为 []any 切片 + Slice() []any + + // BoolSlice 将当前值转换为 []bool 切片 + BoolSlice() []bool + + // StringSlice 将当前值转换为 []string 切片 + StringSlice() []string + + // IntSlice 将当前值转换为 []int 切片 + IntSlice() []int +} + +// New 创建一个新的 R 实例 +// 参数 v 必须是指针类型,否则会 panic +// 返回一个 R 接口实例,可用于访问和操作嵌套的字段、元素和键值对 +func New(v any) R { + rv := reflect.ValueOf(v) + // 如果传入的不是指针,panic + if rv.Kind() != reflect.Ptr { + panic("structmap: New() requires a pointer argument") + } + return &rfx{value: rv} +} diff --git a/rfx.go b/rfx.go new file mode 100644 index 0000000..1e9ee23 --- /dev/null +++ b/rfx.go @@ -0,0 +1,689 @@ +package reflux + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/spf13/cast" +) + +// rfx 是 R 接口的具体实现 +// 通过封装 reflect.Value 提供了对结构体、切片、映射等类型的统一访问方式 +type rfx struct { + // value 存储底层的反射值 + // 该值应该是指针类型,以支持修改操作 + value reflect.Value +} + +// Get 通过路径获取嵌套字段的值 +func (r *rfx) Get(p ...string) R { + return &rfx{value: r.getValueByPath(p...)} +} + +// Scope 创建一个指定路径的作用域视图(深度克隆) +func (r *rfx) Scope(p ...string) R { + v := r.getValueByPath(p...) + if !v.IsValid() { + return &rfx{value: reflect.Value{}} + } + + // 深度克隆值 + cloned := DeepClone(v) + return &rfx{value: cloned} +} + +// getValueByPath 通过路径获取值的辅助方法 +// 支持两种路径格式: +// 1. 多个参数: Get("Address", "City") +// 2. 点号分割: Get("Address.City") 或混合使用 Get("Address.City", "ZipCode") +func (r *rfx) getValueByPath(p ...string) reflect.Value { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + + // 展开所有路径片段,支持点号分割 + keys := expandPath(p...) + + for _, key := range keys { + if !v.IsValid() { + return reflect.Value{} + } + + switch v.Kind() { + case reflect.Struct: + v = tryStructField(v, key) + case reflect.Map: + v = v.MapIndex(reflect.ValueOf(key)) + case reflect.Slice, reflect.Array: + // 尝试将 key 转换为索引 + idx, err := strconv.Atoi(key) + if err == nil && idx >= 0 && idx < v.Len() { + v = v.Index(idx) + } else { + return reflect.Value{} + } + default: + return reflect.Value{} + } + + // 解引用指针和接口 + for v.IsValid() && (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + } + + return v +} + +// Set 设置指定路径的值,支持链式调用 +// 如果路径不存在或设置失败,会 panic +func (r *rfx) Set(key string, v any) R { + // 展开路径 + keys := expandPath(key) + if len(keys) == 0 { + panic("rfx: empty path") + } + + // 如果只有一个键,直接设置 + if len(keys) == 1 { + target := r.getParentValue() + if !target.IsValid() { + panic(fmt.Sprintf("rfx: invalid value for path '%s'", key)) + } + if !target.CanSet() { + panic(fmt.Sprintf("rfx: cannot set value at path '%s'", key)) + } + if !r.setFieldValue(target, keys[0], v) { + panic(fmt.Sprintf("rfx: failed to set value at path '%s'", key)) + } + return r + } + + // 多个键的情况,需要特殊处理 map 中的 struct + if !r.setNestedValue(r.value, keys, v) { + panic(fmt.Sprintf("rfx: failed to set value at path '%s'", key)) + } + return r +} + +// setNestedValue 递归设置嵌套值,特殊处理 map 中的 struct +func (r *rfx) setNestedValue(current reflect.Value, keys []string, v any) bool { + // 解引用指针和接口 + for current.Kind() == reflect.Ptr || current.Kind() == reflect.Interface { + if current.IsNil() { + return false + } + current = current.Elem() + } + + if !current.IsValid() { + return false + } + + // 如果只剩一个键,直接设置 + if len(keys) == 1 { + if !current.CanSet() { + return false + } + return r.setFieldValue(current, keys[0], v) + } + + // 多个键的情况 + firstKey := keys[0] + remainingKeys := keys[1:] + + switch current.Kind() { + case reflect.Struct: + field := tryStructField(current, firstKey) + if !field.IsValid() { + return false + } + return r.setNestedValue(field, remainingKeys, v) + + case reflect.Map: + // Map 的特殊处理 + mapKey := reflect.ValueOf(firstKey) + mapValue := current.MapIndex(mapKey) + + if !mapValue.IsValid() { + return false + } + + // 创建 map 值的副本以便修改 + valueCopy := reflect.New(mapValue.Type()).Elem() + valueCopy.Set(mapValue) + + // 在副本上递归设置值 + if !r.setNestedValue(valueCopy, remainingKeys, v) { + return false + } + + // 将修改后的值设置回 map + current.SetMapIndex(mapKey, valueCopy) + return true + + case reflect.Slice, reflect.Array: + idx, err := strconv.Atoi(firstKey) + if err != nil || idx < 0 || idx >= current.Len() { + return false + } + elem := current.Index(idx) + if !elem.IsValid() { + return false + } + return r.setNestedValue(elem, remainingKeys, v) + } + + return false +} + +// getParentValue 获取父级值的辅助方法 +func (r *rfx) getParentValue(p ...string) reflect.Value { + if len(p) == 0 { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + return v + } + return r.getValueByPath(p...) +} + +// setFieldValue 设置字段值的辅助方法 +func (r *rfx) setFieldValue(target reflect.Value, key string, v any) bool { + for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { + if target.IsNil() { + return false + } + target = target.Elem() + } + + switch target.Kind() { + case reflect.Struct: + field := tryStructField(target, key) + if !field.IsValid() || !field.CanSet() { + return false + } + return r.setValue(field, v) + case reflect.Map: + if target.IsNil() { + target.Set(reflect.MakeMap(target.Type())) + } + val := reflect.ValueOf(v) + if !val.Type().AssignableTo(target.Type().Elem()) { + // 尝试转换 + if val.Type().ConvertibleTo(target.Type().Elem()) { + val = val.Convert(target.Type().Elem()) + } else { + return false + } + } + target.SetMapIndex(reflect.ValueOf(key), val) + return true + case reflect.Slice, reflect.Array: + idx, err := strconv.Atoi(key) + if err != nil || idx < 0 || idx >= target.Len() { + return false + } + elem := target.Index(idx) + if !elem.CanSet() { + return false + } + return r.setValue(elem, v) + } + return false +} + +// setValue 设置值的辅助方法 +// 使用 cast 库进行智能类型转换,支持更多的转换场景 +func (r *rfx) setValue(field reflect.Value, v any) bool { + val := reflect.ValueOf(v) + + // 尝试直接赋值(类型完全匹配) + if val.Type().AssignableTo(field.Type()) { + field.Set(val) + return true + } + + // 优先使用 cast 进行智能类型转换 + // 这样可以处理 string <-> number, number <-> bool 等常见转换 + targetType := field.Type() + var converted any + var err error + + switch targetType.Kind() { + case reflect.Bool: + converted, err = cast.ToBoolE(v) + case reflect.Int: + converted, err = cast.ToIntE(v) + case reflect.Int8: + converted, err = cast.ToInt8E(v) + case reflect.Int16: + converted, err = cast.ToInt16E(v) + case reflect.Int32: + converted, err = cast.ToInt32E(v) + case reflect.Int64: + converted, err = cast.ToInt64E(v) + case reflect.Uint: + converted, err = cast.ToUintE(v) + case reflect.Uint8: + converted, err = cast.ToUint8E(v) + case reflect.Uint16: + converted, err = cast.ToUint16E(v) + case reflect.Uint32: + converted, err = cast.ToUint32E(v) + case reflect.Uint64: + converted, err = cast.ToUint64E(v) + case reflect.Float32: + converted, err = cast.ToFloat32E(v) + case reflect.Float64: + converted, err = cast.ToFloat64E(v) + case reflect.String: + converted, err = cast.ToStringE(v) + default: + // 对于 cast 不支持的类型,尝试标准的反射类型转换 + if val.Type().ConvertibleTo(field.Type()) { + field.Set(val.Convert(field.Type())) + return true + } + return false + } + + if err != nil { + // 如果 cast 失败,尝试标准的反射类型转换作为后备 + if val.Type().ConvertibleTo(field.Type()) { + field.Set(val.Convert(field.Type())) + return true + } + return false + } + + field.Set(reflect.ValueOf(converted)) + return true +} + +// Delete 删除指定路径的值,支持链式调用 +// 如果删除失败会 panic +func (r *rfx) Delete(p ...string) R { + if len(p) == 0 { + panic("rfx: empty path") + } + + // 展开路径 + keys := expandPath(p...) + if len(keys) == 0 { + panic("rfx: empty path") + } + + target := r.getParentValue(keys[:len(keys)-1]...) + if !target.IsValid() { + panic(fmt.Sprintf("rfx: invalid path '%s'", strings.Join(keys[:len(keys)-1], "."))) + } + + lastKey := keys[len(keys)-1] + for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { + if target.IsNil() { + panic(fmt.Sprintf("rfx: nil value at path '%s'", strings.Join(keys[:len(keys)-1], "."))) + } + target = target.Elem() + } + + switch target.Kind() { + case reflect.Map: + target.SetMapIndex(reflect.ValueOf(lastKey), reflect.Value{}) + return r + case reflect.Slice: + idx, err := strconv.Atoi(lastKey) + if err != nil { + panic(fmt.Sprintf("rfx: invalid slice index '%s'", lastKey)) + } + if idx < 0 || idx >= target.Len() { + panic(fmt.Sprintf("rfx: slice index out of range: %d", idx)) + } + // 删除切片元素 + newSlice := reflect.AppendSlice( + target.Slice(0, idx), + target.Slice(idx+1, target.Len()), + ) + target.Set(newSlice) + return r + default: + panic(fmt.Sprintf("rfx: cannot delete from type %s", target.Kind())) + } +} + +// Exists 检查指定路径的值是否存在 +func (r *rfx) Exists(p ...string) bool { + v := r.getValueByPath(p...) + return v.IsValid() +} + +// Array 将当前值转换为 R 切片 +func (r *rfx) Array() []R { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return nil + } + + result := make([]R, v.Len()) + for i := 0; i < v.Len(); i++ { + result[i] = &rfx{value: v.Index(i)} + } + return result +} + +// keys 返回当前映射或结构体的所有键名 +func (r *rfx) Keys() []string { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + switch v.Kind() { + case reflect.Map: + keys := v.MapKeys() + result := make([]string, len(keys)) + for i, k := range keys { + result[i] = fmt.Sprint(k.Interface()) + } + return result + case reflect.Struct: + t := v.Type() + result := make([]string, t.NumField()) + for i := 0; i < t.NumField(); i++ { + result[i] = t.Field(i).Name + } + return result + } + return nil +} + +// Value 返回底层的 reflect.Value +func (r *rfx) Value() reflect.Value { + return r.value +} + +// Ptr 返回指向当前值的指针 +func (r *rfx) Ptr() any { + v := r.value + if v.Kind() == reflect.Ptr { + return v.Interface() + } + if v.CanAddr() { + return v.Addr().Interface() + } + return nil +} + +// Any 将当前值转换为 any 类型 +func (r *rfx) Any() any { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if !v.IsValid() { + return nil + } + return v.Interface() +} + +// Bool 将当前值转换为 bool 类型 +func (r *rfx) Bool() bool { + result, err := cast.ToBoolE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to bool: %v", err)) + } + return result +} + +// Float64 将当前值转换为 float64 类型 +func (r *rfx) Float64() float64 { + result, err := cast.ToFloat64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to float64: %v", err)) + } + return result +} + +// Float32 将当前值转换为 float32 类型 +func (r *rfx) Float32() float32 { + result, err := cast.ToFloat32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to float32: %v", err)) + } + return result +} + +// Int64 将当前值转换为 int64 类型 +func (r *rfx) Int64() int64 { + result, err := cast.ToInt64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int64: %v", err)) + } + return result +} + +// Int32 将当前值转换为 int32 类型 +func (r *rfx) Int32() int32 { + result, err := cast.ToInt32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int32: %v", err)) + } + return result +} + +// Int16 将当前值转换为 int16 类型 +func (r *rfx) Int16() int16 { + result, err := cast.ToInt16E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int16: %v", err)) + } + return result +} + +// Int8 将当前值转换为 int8 类型 +func (r *rfx) Int8() int8 { + result, err := cast.ToInt8E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int8: %v", err)) + } + return result +} + +// Int 将当前值转换为 int 类型 +func (r *rfx) Int() int { + result, err := cast.ToIntE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int: %v", err)) + } + return result +} + +// Uint 将当前值转换为 uint 类型 +func (r *rfx) Uint() uint { + result, err := cast.ToUintE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint: %v", err)) + } + return result +} + +// Uint64 将当前值转换为 uint64 类型 +func (r *rfx) Uint64() uint64 { + result, err := cast.ToUint64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint64: %v", err)) + } + return result +} + +// Uint32 将当前值转换为 uint32 类型 +func (r *rfx) Uint32() uint32 { + result, err := cast.ToUint32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint32: %v", err)) + } + return result +} + +// Uint16 将当前值转换为 uint16 类型 +func (r *rfx) Uint16() uint16 { + result, err := cast.ToUint16E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint16: %v", err)) + } + return result +} + +// Uint8 将当前值转换为 uint8 类型 +func (r *rfx) Uint8() uint8 { + result, err := cast.ToUint8E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint8: %v", err)) + } + return result +} + +// String 将当前值转换为 string 类型 +func (r *rfx) String() string { + result, err := cast.ToStringE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to string: %v", err)) + } + return result +} + +// StringMapString 将当前值转换为 map[string]string 类型 +func (r *rfx) StringMapString() map[string]string { + result, err := cast.ToStringMapStringE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]string: %v", err)) + } + return result +} + +// StringMapStringSlice 将当前值转换为 map[string][]string 类型 +func (r *rfx) StringMapStringSlice() map[string][]string { + result, err := cast.ToStringMapStringSliceE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string][]string: %v", err)) + } + return result +} + +// StringMapBool 将当前值转换为 map[string]bool 类型 +func (r *rfx) StringMapBool() map[string]bool { + result, err := cast.ToStringMapBoolE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]bool: %v", err)) + } + return result +} + +// StringMapInt 将当前值转换为 map[string]int 类型 +func (r *rfx) StringMapInt() map[string]int { + result, err := cast.ToStringMapIntE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]int: %v", err)) + } + return result +} + +// StringMapInt64 将当前值转换为 map[string]int64 类型 +func (r *rfx) StringMapInt64() map[string]int64 { + result, err := cast.ToStringMapInt64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]int64: %v", err)) + } + return result +} + +// StringMap 将当前值转换为 map[string]any 类型 +func (r *rfx) StringMap() map[string]any { + result, err := cast.ToStringMapE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]any: %v", err)) + } + return result +} + +// Slice 将当前值转换为 []any 切片 +func (r *rfx) Slice() []any { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) { + return nil + } + + result := make([]any, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + for elem.Kind() == reflect.Ptr || elem.Kind() == reflect.Interface { + if elem.IsNil() { + break + } + elem = elem.Elem() + } + if elem.IsValid() { + result[i] = elem.Interface() + } + } + return result +} + +// BoolSlice 将当前值转换为 []bool 切片 +func (r *rfx) BoolSlice() []bool { + result, err := cast.ToBoolSliceE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []bool: %v", err)) + } + return result +} + +// StringSlice 将当前值转换为 []string 切片 +func (r *rfx) StringSlice() []string { + result, err := cast.ToStringSliceE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []string: %v", err)) + } + return result +} + +// IntSlice 将当前值转换为 []int 切片 +func (r *rfx) IntSlice() []int { + result, err := cast.ToIntSliceE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []int: %v", err)) + } + return result +} diff --git a/rfx_example_test.go b/rfx_example_test.go new file mode 100644 index 0000000..a0ac3e7 --- /dev/null +++ b/rfx_example_test.go @@ -0,0 +1,268 @@ +package reflux + +import "fmt" + +// ExampleNew_withMap 演示如何使用 New 函数传入 map +func ExampleNew_withMap() { + // 创建一个 map + config := map[string]any{ + "host": "localhost", + "port": 8080, + "ssl": true, + } + + // 使用 New 创建 StructMap (必须传入指针) + rfx := New(&config) + + // 获取值 + host := rfx.Get("host").String() + port := rfx.Get("port").Int() + ssl := rfx.Get("ssl").Bool() + + fmt.Printf("Host: %s\n", host) + fmt.Printf("Port: %d\n", port) + fmt.Printf("SSL: %t\n", ssl) + + // 修改值 + rfx.Set("host", "127.0.0.1") + rfx.Set("port", 9090) + + // 添加新键 + rfx.Set("timeout", 30) + + fmt.Printf("Updated host: %s\n", config["host"]) + fmt.Printf("Updated port: %v\n", config["port"]) + fmt.Printf("New timeout: %v\n", config["timeout"]) + + // Output: + // Host: localhost + // Port: 8080 + // SSL: true + // Updated host: 127.0.0.1 + // Updated port: 9090 + // New timeout: 30 +} + +// ExampleStructMap_Scope_noArgs 演示如何使用 Scope() 不传参数来复制整个对象 +func ExampleStructMap_Scope_noArgs() { + type Config struct { + Host string + Port int + } + + cfg := Config{ + Host: "localhost", + Port: 8080, + } + + rfx := New(&cfg) + + // Scope 不传参数,创建整个对象的深度克隆 + clone := rfx.Scope() + + // 在克隆上修改值 + clone.Set("Host", "127.0.0.1") + clone.Set("Port", 9090) + + // 原对象不受影响 + fmt.Printf("Original - Host: %s, Port: %d\n", cfg.Host, cfg.Port) + + // 克隆对象已修改 + fmt.Printf("Clone - Host: %s, Port: %d\n", + clone.Get("Host").String(), + clone.Get("Port").Int()) + + // Output: + // Original - Host: localhost, Port: 8080 + // Clone - Host: 127.0.0.1, Port: 9090 +} + +// ExampleStructMap_Scope_withPath 演示如何使用 Scope() 传入路径来复制嵌套对象 +func ExampleStructMap_Scope_withPath() { + type Address struct { + City string + Street string + } + + type Person struct { + Name string + Address Address + } + + person := Person{ + Name: "Alice", + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + } + + rfx := New(&person) + + // 创建 Address 字段的 Scope (深度克隆) + addressScope := rfx.Scope("Address") + + // 在 Scope 上修改值 + addressScope.Set("City", "Shanghai") + addressScope.Set("Street", "New St") + + // 原对象不受影响 + fmt.Printf("Original Address - City: %s, Street: %s\n", + person.Address.City, person.Address.Street) + + // Scope 对象已修改 + fmt.Printf("Scope Address - City: %s, Street: %s\n", + addressScope.Get("City").String(), + addressScope.Get("Street").String()) + + // Output: + // Original Address - City: Beijing, Street: Main St + // Scope Address - City: Shanghai, Street: New St +} + +// ExampleNew_withNestedMap 演示如何使用嵌套的 map +func ExampleNew_withNestedMap() { + data := map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 30, + }, + "tags": []string{"developer", "gopher"}, + } + + rfx := New(&data) + + // 获取嵌套的值 + userName := rfx.Get("user", "name").String() + userAge := rfx.Get("user", "age").Int() + firstTag := rfx.Get("tags", "0").String() + + fmt.Printf("User: %s, Age: %d\n", userName, userAge) + fmt.Printf("First tag: %s\n", firstTag) + + // 使用点号语法访问嵌套值 + userName2 := rfx.Get("user.name").String() + fmt.Printf("User (dot notation): %s\n", userName2) + + // Output: + // User: Alice, Age: 30 + // First tag: developer + // User (dot notation): Alice +} + +// ExampleStructMap_lowercaseFields 演示如何使用小写字段名访问 struct +func ExampleStructMap_lowercaseFields() { + type Person struct { + Name string + Age int + } + + person := Person{ + Name: "Alice", + Age: 30, + } + + rfx := New(&person) + + // 使用小写字段名获取值(自动转换为大写) + name := rfx.Get("name").String() + age := rfx.Get("age").Int() + + fmt.Printf("Name: %s, Age: %d\n", name, age) + + // 使用小写字段名设置值 + rfx.Set("name", "Bob") + rfx.Set("age", 35) + + fmt.Printf("Updated - Name: %s, Age: %d\n", person.Name, person.Age) + + // Output: + // Name: Alice, Age: 30 + // Updated - Name: Bob, Age: 35 +} + +// ExampleStructMap_lowercaseNestedFields 演示如何使用小写字段名访问嵌套 struct +func ExampleStructMap_lowercaseNestedFields() { + type Address struct { + City string + Street string + } + + type Person struct { + Name string + Address Address + } + + person := Person{ + Name: "Alice", + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + } + + rfx := New(&person) + + // 使用小写字段名访问嵌套字段 + city := rfx.Get("address", "city").String() + street := rfx.Get("address.street").String() + + fmt.Printf("City: %s, Street: %s\n", city, street) + + // 使用小写字段名设置嵌套字段 + rfx.Set("address.city", "Shanghai") + rfx.Set("address.street", "New Street") + + fmt.Printf("Updated City: %s, Street: %s\n", + person.Address.City, person.Address.Street) + + // Output: + // City: Beijing, Street: Main St + // Updated City: Shanghai, Street: New Street +} + +// ExampleStructMap_castConversion 演示使用 cast 进行智能类型转换 +func ExampleStructMap_castConversion() { + type Config struct { + Port int + Host string + Enabled bool + Timeout float64 + } + + config := Config{} + rfx := New(&config) + + // 字符串转数字 + rfx.Set("Port", "8080") + fmt.Printf("Port (string->int): %d\n", config.Port) + + // 数字转字符串 + rfx.Set("Host", 12345) + fmt.Printf("Host (int->string): %s\n", config.Host) + + // 字符串转布尔 + rfx.Set("Enabled", "true") + fmt.Printf("Enabled (string->bool): %t\n", config.Enabled) + + // 字符串转浮点数 + rfx.Set("Timeout", "30.5") + fmt.Printf("Timeout (string->float): %.1f\n", config.Timeout) + + // 链式调用不同类型转换 + rfx.Set("Port", "9090"). + Set("Host", "localhost"). + Set("Enabled", "1"). + Set("Timeout", "60") + + fmt.Printf("\nAfter chain: Port=%d, Host=%s, Enabled=%t, Timeout=%.0f\n", + config.Port, config.Host, config.Enabled, config.Timeout) + + // Output: + // Port (string->int): 8080 + // Host (int->string): 12345 + // Enabled (string->bool): true + // Timeout (string->float): 30.5 + // + // After chain: Port=9090, Host=localhost, Enabled=true, Timeout=60 +} diff --git a/rfx_test.go b/rfx_test.go new file mode 100644 index 0000000..0fd184f --- /dev/null +++ b/rfx_test.go @@ -0,0 +1,1284 @@ +package reflux + +import ( + "encoding/json" + "testing" +) + +// 测试用的结构体 +type Person struct { + Name string + Age int + Email string + Address Address + Tags []string + Meta map[string]string +} + +type Address struct { + City string + Street string + ZipCode int +} + +// TestNew 测试 New 函数 +func TestNew(t *testing.T) { + p := Person{Name: "Alice", Age: 30} + + // 测试传入非指针值应该 panic + defer func() { + if r := recover(); r == nil { + t.Error("New() with non-pointer should panic") + } + }() + New(p) // 应该 panic +} + +// TestNewWithPointer 测试传入指针的情况 +func TestNewWithPointer(t *testing.T) { + p := Person{Name: "Alice", Age: 30} + + // 测试传入指针值 + rfx := New(&p) + if rfx == nil { + t.Fatal("New() with pointer returned nil") + } +} + +// TestGet 测试 Get 方法 +func TestGet(t *testing.T) { + p := Person{ + Name: "Bob", + Age: 25, + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + Tags: []string{"developer", "gopher"}, + } + + rfx := New(&p) + + // 测试获取顶层字段 + name := rfx.Get("Name").String() + if name != "Bob" { + t.Errorf("Expected 'Bob', got '%s'", name) + } + + // 测试获取嵌套字段 + city := rfx.Get("Address", "City").String() + if city != "Beijing" { + t.Errorf("Expected 'Beijing', got '%s'", city) + } + + // 测试获取切片元素 + tag := rfx.Get("Tags", "0").String() + if tag != "developer" { + t.Errorf("Expected 'developer', got '%s'", tag) + } +} + +// TestScope 测试 Scope 方法 +func TestScope(t *testing.T) { + p := Person{ + Address: Address{ + City: "Shanghai", + Street: "Park Ave", + ZipCode: 200000, + }, + } + + rfx := New(&p) + addressScope := rfx.Scope("Address") + + // 测试通过 Scope 获取值 + city := addressScope.Get("City").String() + if city != "Shanghai" { + t.Errorf("Expected 'Shanghai', got '%s'", city) + } + + // 测试通过 Scope 设置值后,原对象不应该被修改(因为是深度克隆) + addressScope.Set("City", "Beijing") + + // 验证原对象的值没有改变(深度克隆) + if p.Address.City != "Shanghai" { + t.Errorf("Expected original object's City to remain 'Shanghai', got '%s'", p.Address.City) + } + + // 验证 Scope 内的值已改变 + scopeCity := addressScope.Get("City").String() + if scopeCity != "Beijing" { + t.Errorf("Expected scope's City to be 'Beijing', got '%s'", scopeCity) + } + + // 验证通过原 rfx 对象获取的值仍然是原值 + cityFromOriginal := rfx.Get("Address", "City").String() + if cityFromOriginal != "Shanghai" { + t.Errorf("Expected 'Shanghai' from original rfx, got '%s'", cityFromOriginal) + } + + // 测试在 Scope 上设置多个字段 + addressScope.Set("Street", "New Street").Set("ZipCode", 100000) + + // 验证原对象的值都没有改变 + if p.Address.Street != "Park Ave" { + t.Errorf("Expected original Street to remain 'Park Ave', got '%s'", p.Address.Street) + } + if p.Address.ZipCode != 200000 { + t.Errorf("Expected original ZipCode to remain 200000, got %d", p.Address.ZipCode) + } + + // 验证 Scope 内的值已经改变 + scopeStreet := addressScope.Get("Street").String() + scopeZipCode := addressScope.Get("ZipCode").Int() + if scopeStreet != "New Street" { + t.Errorf("Expected scope's Street to be 'New Street', got '%s'", scopeStreet) + } + if scopeZipCode != 100000 { + t.Errorf("Expected scope's ZipCode to be 100000, got %d", scopeZipCode) + } + + // 再次验证 Scope 的值与原对象不同(证明是深度克隆) + if scopeCity == p.Address.City { + t.Error("Scope should have independent copy, not reference to original") + } +} + +// TestSet 测试 Set 方法 +func TestSet(t *testing.T) { + p := Person{ + Name: "Charlie", + Age: 30, + Address: Address{ + City: "Guangzhou", + }, + Meta: make(map[string]string), + } + + rfx := New(&p) + + // 测试设置顶层字段 + rfx.Set("Name", "David") + if p.Name != "David" { + t.Errorf("Expected 'David', got '%s'", p.Name) + } + + // 测试设置嵌套字段 + rfx.Set("Address.City", "Shenzhen") + if p.Address.City != "Shenzhen" { + t.Errorf("Expected 'Shenzhen', got '%s'", p.Address.City) + } + + // 测试设置 map 值 + rfx.Set("Meta.key1", "value1") + if p.Meta["key1"] != "value1" { + t.Errorf("Expected 'value1', got '%s'", p.Meta["key1"]) + } + + // 测试类型转换 + rfx.Set("Age", int32(35)) + if p.Age != 35 { + t.Errorf("Expected 35, got %d", p.Age) + } + + // 测试设置整个 Address 结构体 + newAddress := Address{ + City: "Shanghai", + Street: "Nanjing Road", + ZipCode: 200000, + } + rfx.Set("Address", newAddress) + if p.Address.City != "Shanghai" { + t.Errorf("Expected City 'Shanghai', got '%s'", p.Address.City) + } + if p.Address.Street != "Nanjing Road" { + t.Errorf("Expected Street 'Nanjing Road', got '%s'", p.Address.Street) + } + if p.Address.ZipCode != 200000 { + t.Errorf("Expected ZipCode 200000, got %d", p.Address.ZipCode) + } + + // 测试设置 Address 结构体后再修改其字段 + rfx.Set("Address.ZipCode", 200001) + if p.Address.ZipCode != 200001 { + t.Errorf("Expected ZipCode 200001, got %d", p.Address.ZipCode) + } +} + +// TestDelete 测试 Delete 方法 +func TestDelete(t *testing.T) { + meta := map[string]string{ + "key1": "value1", + "key2": "value2", + } + p := Person{ + Meta: meta, + Tags: []string{"tag1", "tag2", "tag3"}, + } + + rfx := New(&p) + + // 测试删除 map 键 + rfx.Delete("Meta", "key1") + if _, exists := p.Meta["key1"]; exists { + t.Error("key1 should have been deleted") + } + + // 测试删除切片元素 + rfx.Delete("Tags", "1") + if len(p.Tags) != 2 || p.Tags[1] != "tag3" { + t.Errorf("Expected Tags to be ['tag1', 'tag3'], got %v", p.Tags) + } +} + +// TestExists 测试 Exists 方法 +func TestExists(t *testing.T) { + p := Person{ + Name: "Eve", + Address: Address{ + City: "Hangzhou", + }, + } + + rfx := New(&p) + + // 测试存在的字段 + if !rfx.Exists("Name") { + t.Error("Name should exist") + } + + // 测试嵌套字段 + if !rfx.Exists("Address", "City") { + t.Error("Address.City should exist") + } + + // 测试不存在的字段 + if rfx.Exists("NonExistent") { + t.Error("NonExistent should not exist") + } + + // 测试空字段 + if rfx.Exists("Email") { + // Email 是空字符串,但字段存在 + email := rfx.Get("Email").String() + if email != "" { + t.Error("Email should be empty string") + } + } +} + +// TestArray 测试 Array 方法 +func TestArray(t *testing.T) { + tags := []string{"go", "rust", "python"} + p := Person{ + Tags: tags, + } + + rfx := New(&p) + arr := rfx.Get("Tags").Array() + + if len(arr) != 3 { + t.Errorf("Expected array length 3, got %d", len(arr)) + } + + if arr[0].String() != "go" { + t.Errorf("Expected 'go', got '%s'", arr[0].String()) + } +} + +// TestKeys 测试 keys 方法 +func TestKeys(t *testing.T) { + meta := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + p := Person{ + Meta: meta, + } + + rfx := New(&p) + keys := rfx.Get("Meta").Keys() + + if len(keys) != 3 { + t.Errorf("Expected 3 keys, got %d", len(keys)) + } + + // 检查键是否存在 + keyMap := make(map[string]bool) + for _, k := range keys { + keyMap[k] = true + } + if !keyMap["key1"] || !keyMap["key2"] || !keyMap["key3"] { + t.Error("Missing expected keys") + } +} + +// TestTypeConversions 测试类型转换方法 +func TestTypeConversions(t *testing.T) { + // 测试整数转换 + type Numbers struct { + IntVal int + Int64Val int64 + UintVal uint + FloatVal float64 + BoolVal bool + StringVal string + } + + n := Numbers{ + IntVal: 42, + Int64Val: 9223372036854775807, + UintVal: 100, + FloatVal: 3.14, + BoolVal: true, + StringVal: "hello", + } + + rfx := New(&n) + + // Int + if rfx.Get("IntVal").Int() != 42 { + t.Error("Int() conversion failed") + } + + // Int64 + if rfx.Get("Int64Val").Int64() != 9223372036854775807 { + t.Error("Int64() conversion failed") + } + + // Uint + if rfx.Get("UintVal").Uint() != 100 { + t.Error("Uint() conversion failed") + } + + // Float64 + if rfx.Get("FloatVal").Float64() != 3.14 { + t.Error("Float64() conversion failed") + } + + // Bool + if !rfx.Get("BoolVal").Bool() { + t.Error("Bool() conversion failed") + } + + // String + if rfx.Get("StringVal").String() != "hello" { + t.Error("String() conversion failed") + } + + // 跨类型转换 + if rfx.Get("IntVal").Float64() != 42.0 { + t.Error("Int to Float64 conversion failed") + } + + if rfx.Get("FloatVal").Int64() != 3 { + t.Error("Float64 to Int64 conversion failed") + } +} + +// TestStringMapConversions 测试 map 类型转换 +func TestStringMapConversions(t *testing.T) { + type Maps struct { + StringMap map[string]string + IntMap map[string]int + BoolMap map[string]bool + StringSliceMap map[string][]string + } + + m := Maps{ + StringMap: map[string]string{ + "name": "Alice", + "city": "NYC", + }, + IntMap: map[string]int{ + "age": 30, + "score": 100, + }, + BoolMap: map[string]bool{ + "active": true, + "enabled": false, + }, + StringSliceMap: map[string][]string{ + "tags": {"go", "rust"}, + }, + } + + rfx := New(&m) + + // StringMapString + strMap := rfx.Get("StringMap").StringMapString() + if strMap["name"] != "Alice" { + t.Error("StringMapString conversion failed") + } + + // StringMapInt + intMap := rfx.Get("IntMap").StringMapInt() + if intMap["age"] != 30 { + t.Error("StringMapInt conversion failed") + } + + // StringMapBool + boolMap := rfx.Get("BoolMap").StringMapBool() + if !boolMap["active"] { + t.Error("StringMapBool conversion failed") + } + + // StringMapStringSlice + sliceMap := rfx.Get("StringSliceMap").StringMapStringSlice() + if len(sliceMap["tags"]) != 2 || sliceMap["tags"][0] != "go" { + t.Error("StringMapStringSlice conversion failed") + } +} + +// TestSliceConversions 测试切片类型转换 +func TestSliceConversions(t *testing.T) { + type Slices struct { + IntSlice []int + StringSlice []string + BoolSlice []bool + } + + s := Slices{ + IntSlice: []int{1, 2, 3}, + StringSlice: []string{"a", "b", "c"}, + BoolSlice: []bool{true, false, true}, + } + + rfx := New(&s) + + // IntSlice + intSlice := rfx.Get("IntSlice").IntSlice() + if len(intSlice) != 3 || intSlice[1] != 2 { + t.Error("IntSlice conversion failed") + } + + // StringSlice + strSlice := rfx.Get("StringSlice").StringSlice() + if len(strSlice) != 3 || strSlice[1] != "b" { + t.Error("StringSlice conversion failed") + } + + // BoolSlice + boolSlice := rfx.Get("BoolSlice").BoolSlice() + if len(boolSlice) != 3 || boolSlice[1] != false { + t.Error("BoolSlice conversion failed") + } + + // Slice (any) + anySlice := rfx.Get("IntSlice").Slice() + if len(anySlice) != 3 { + t.Error("Slice conversion failed") + } +} + +// TestPtr 测试 Ptr 方法 +func TestPtr(t *testing.T) { + p := Person{Name: "Frank"} + rfx := New(&p) + + ptr := rfx.Ptr() + if ptr == nil { + t.Error("Ptr() returned nil") + } + + // 验证指针指向正确的对象 + if pPtr, ok := ptr.(*Person); ok { + if pPtr.Name != "Frank" { + t.Error("Ptr() returned incorrect pointer") + } + } else { + t.Error("Ptr() returned wrong type") + } +} + +// TestGetPtrWithJSONUnmarshal 测试 Get().Ptr() 与 json.Unmarshal() 的配合 +func TestGetPtrWithJSONUnmarshal(t *testing.T) { + t.Run("Unmarshal to nested struct field", func(t *testing.T) { + p := Person{ + Name: "Alice", + Age: 30, + Address: Address{ + City: "OldCity", + }, + } + + rfx := New(&p) + + // 准备 JSON 数据 + jsonData := []byte(`{ + "City": "Shanghai", + "Street": "Nanjing Road", + "ZipCode": 200000 + }`) + + // 通过 Get().Ptr() 获取 Address 字段的指针 + addressPtr := rfx.Get("Address").Ptr() + if addressPtr == nil { + t.Fatal("Get('Address').Ptr() returned nil") + } + + // 使用 json.Unmarshal 直接赋值 + err := json.Unmarshal(jsonData, addressPtr) + if err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // 验证赋值成功 + if p.Address.City != "Shanghai" { + t.Errorf("Expected City 'Shanghai', got '%s'", p.Address.City) + } + if p.Address.Street != "Nanjing Road" { + t.Errorf("Expected Street 'Nanjing Road', got '%s'", p.Address.Street) + } + if p.Address.ZipCode != 200000 { + t.Errorf("Expected ZipCode 200000, got %d", p.Address.ZipCode) + } + }) + + t.Run("Unmarshal to top-level struct", func(t *testing.T) { + p := Person{ + Name: "Bob", + Age: 25, + } + + rfx := New(&p) + + // 准备 JSON 数据 + jsonData := []byte(`{ + "Name": "Charlie", + "Age": 35, + "Email": "charlie@example.com" + }`) + + // 通过 Ptr() 获取整个 Person 的指针 + personPtr := rfx.Ptr() + if personPtr == nil { + t.Fatal("Ptr() returned nil") + } + + // 使用 json.Unmarshal 直接赋值 + err := json.Unmarshal(jsonData, personPtr) + if err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // 验证赋值成功 + if p.Name != "Charlie" { + t.Errorf("Expected Name 'Charlie', got '%s'", p.Name) + } + if p.Age != 35 { + t.Errorf("Expected Age 35, got %d", p.Age) + } + if p.Email != "charlie@example.com" { + t.Errorf("Expected Email 'charlie@example.com', got '%s'", p.Email) + } + }) + + t.Run("Unmarshal to map field", func(t *testing.T) { + p := Person{ + Name: "Dave", + Meta: make(map[string]string), + } + + rfx := New(&p) + + // 准备 JSON 数据 + jsonData := []byte(`{ + "key1": "value1", + "key2": "value2", + "key3": "value3" + }`) + + // 通过 Get().Ptr() 获取 Meta 字段的指针 + metaPtr := rfx.Get("Meta").Ptr() + if metaPtr == nil { + t.Fatal("Get('Meta').Ptr() returned nil") + } + + // 使用 json.Unmarshal 直接赋值 + err := json.Unmarshal(jsonData, metaPtr) + if err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // 验证赋值成功 + if p.Meta["key1"] != "value1" { + t.Errorf("Expected Meta['key1'] 'value1', got '%s'", p.Meta["key1"]) + } + if p.Meta["key2"] != "value2" { + t.Errorf("Expected Meta['key2'] 'value2', got '%s'", p.Meta["key2"]) + } + if p.Meta["key3"] != "value3" { + t.Errorf("Expected Meta['key3'] 'value3', got '%s'", p.Meta["key3"]) + } + }) + + t.Run("Unmarshal to slice field", func(t *testing.T) { + p := Person{ + Name: "Eve", + Tags: []string{"old1", "old2"}, + } + + rfx := New(&p) + + // 准备 JSON 数据 + jsonData := []byte(`["tag1", "tag2", "tag3"]`) + + // 通过 Get().Ptr() 获取 Tags 字段的指针 + tagsPtr := rfx.Get("Tags").Ptr() + if tagsPtr == nil { + t.Fatal("Get('Tags').Ptr() returned nil") + } + + // 使用 json.Unmarshal 直接赋值 + err := json.Unmarshal(jsonData, tagsPtr) + if err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // 验证赋值成功 + if len(p.Tags) != 3 { + t.Errorf("Expected Tags length 3, got %d", len(p.Tags)) + } + if p.Tags[0] != "tag1" || p.Tags[1] != "tag2" || p.Tags[2] != "tag3" { + t.Errorf("Expected Tags ['tag1', 'tag2', 'tag3'], got %v", p.Tags) + } + }) +} + +// TestAny 测试 Any 方法 +func TestAny(t *testing.T) { + p := Person{Name: "Grace", Age: 28} + rfx := New(&p) + + anyVal := rfx.Any() + if person, ok := anyVal.(Person); ok { + if person.Name != "Grace" || person.Age != 28 { + t.Error("Any() returned incorrect value") + } + } else { + t.Error("Any() returned wrong type") + } +} + +// TestValue 测试 Value 方法 +func TestValue(t *testing.T) { + p := Person{Name: "Henry"} + rfx := New(&p) + + val := rfx.Value() + if !val.IsValid() { + t.Error("Value() returned invalid value") + } + + if val.Kind() != 22 { // reflect.Ptr + t.Errorf("Expected pointer kind, got %v", val.Kind()) + } +} + +// TestComplexNesting 测试复杂嵌套场景 +func TestComplexNesting(t *testing.T) { + type Company struct { + Name string + Employees []Person + } + + company := Company{ + Name: "TechCorp", + Employees: []Person{ + {Name: "Alice", Age: 30}, + {Name: "Bob", Age: 25}, + }, + } + + rfx := New(&company) + + // 获取嵌套数组中的值 + empName := rfx.Get("Employees", "0", "Name").String() + if empName != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", empName) + } + + // 设置嵌套数组中的值 + rfx.Set("Employees.0.Age", 31) + if company.Employees[0].Age != 31 { + t.Errorf("Expected 31, got %d", company.Employees[0].Age) + } +} + +// TestEdgeCases 测试边界情况 +func TestEdgeCases(t *testing.T) { + p := Person{} + rfx := New(&p) + + // 测试空值 + emptyStr := rfx.Get("Name").String() + if emptyStr != "" { + t.Error("Empty string should be returned") + } + + // 测试不存在的路径 + nonExistent := rfx.Get("NonExistent", "Field").String() + if nonExistent != "" { + t.Error("Non-existent path should return empty string") + } + + // 测试初始化的 nil map(结构体字段默认为 nil) + metaMap := rfx.Get("Meta").StringMapString() + // nil map 会返回 nil,这是正确的行为 + _ = metaMap + + // 测试 nil 切片(结构体字段默认为 nil) + tags := rfx.Get("Tags").StringSlice() + // nil slice 会返回 nil,这是正确的行为 + _ = tags + + // 测试设置到不存在的路径应该 panic + defer func() { + if r := recover(); r == nil { + t.Error("Setting non-existent field should panic") + } + }() + rfx.Set("NonExistent", "value") // 应该 panic +} + +// TestMapOperations 测试 map 操作 +func TestMapOperations(t *testing.T) { + m := map[string]any{ + "name": "Alice", + "age": 30, + "tags": []string{"go", "rust"}, + } + + rfx := New(&m) + + // Get from map + name := rfx.Get("name").String() + if name != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", name) + } + + // Set to map + rfx.Set("name", "Bob") + if m["name"] != "Bob" { + t.Errorf("Expected 'Bob', got '%v'", m["name"]) + } + + // Add new key + rfx.Set("age", 35) + + // Delete from map + rfx.Delete("age") + if _, exists := m["age"]; exists { + t.Error("Key should have been deleted") + } +} + +// BenchmarkGet 性能测试 Get 方法 +func BenchmarkGet(b *testing.B) { + p := Person{ + Name: "Benchmark", + Address: Address{ + City: "TestCity", + }, + } + rfx := New(&p) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rfx.Get("Address", "City").String() + } +} + +// BenchmarkSet 性能测试 Set 方法 +func BenchmarkSet(b *testing.B) { + p := Person{} + rfx := New(&p) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + rfx.Set("Name", "TestName") + } +} + +// TestDotNotationPath 测试点号分割路径 +func TestDotNotationPath(t *testing.T) { + p := Person{ + Name: "Alice", + Age: 30, + Address: Address{ + City: "Beijing", + Street: "Main St", + ZipCode: 100000, + }, + Tags: []string{"go", "rust", "python"}, + Meta: map[string]string{ + "level": "senior", + }, + } + + rfx := New(&p) + + // 测试点号分割路径获取 + t.Run("Get with dot notation", func(t *testing.T) { + // 使用点号访问嵌套字段 + city := rfx.Get("Address.City").String() + if city != "Beijing" { + t.Errorf("Expected 'Beijing', got '%s'", city) + } + + zipCode := rfx.Get("Address.ZipCode").Int() + if zipCode != 100000 { + t.Errorf("Expected 100000, got %d", zipCode) + } + }) + + // 测试混合使用点号和分隔参数 + t.Run("Get with mixed notation", func(t *testing.T) { + city := rfx.Get("Address.City").String() + if city != "Beijing" { + t.Errorf("Expected 'Beijing', got '%s'", city) + } + + // 混合使用 + metaLevel := rfx.Get("Meta", "level").String() + if metaLevel != "senior" { + t.Errorf("Expected 'senior', got '%s'", metaLevel) + } + + // 也可以用点号访问 Map + metaLevel2 := rfx.Get("Meta.level").String() + if metaLevel2 != "senior" { + t.Errorf("Expected 'senior', got '%s'", metaLevel2) + } + }) + + // 测试访问切片元素 + t.Run("Get array with dot notation", func(t *testing.T) { + tag := rfx.Get("Tags.0").String() + if tag != "go" { + t.Errorf("Expected 'go', got '%s'", tag) + } + + tag2 := rfx.Get("Tags.1").String() + if tag2 != "rust" { + t.Errorf("Expected 'rust', got '%s'", tag2) + } + }) + + // 测试 Set 使用点号路径 + t.Run("Set with dot notation", func(t *testing.T) { + rfx.Set("Address.City", "Shanghai") + if p.Address.City != "Shanghai" { + t.Errorf("Expected 'Shanghai', got '%s'", p.Address.City) + } + + rfx.Set("Address.ZipCode", 200000) + if p.Address.ZipCode != 200000 { + t.Errorf("Expected 200000, got %d", p.Address.ZipCode) + } + }) + + // 测试 Exists 使用点号路径 + t.Run("Exists with dot notation", func(t *testing.T) { + if !rfx.Exists("Address.City") { + t.Error("Address.City should exist") + } + + if rfx.Exists("Address.NonExistent") { + t.Error("Address.NonExistent should not exist") + } + }) + + // 测试 Delete 使用点号路径 + t.Run("Delete with dot notation", func(t *testing.T) { + rfx.Delete("Meta.level") + if _, exists := p.Meta["level"]; exists { + t.Error("Meta.level should have been deleted") + } + }) + + // 测试 Scope 使用点号路径 + t.Run("Scope with dot notation", func(t *testing.T) { + addressScope := rfx.Scope("Address") + city := addressScope.Get("City").String() + if city != "Shanghai" { + t.Errorf("Expected 'Shanghai', got '%s'", city) + } + }) +} + +// TestExpandPath 测试路径展开功能 +func TestExpandPath(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "Simple path", + input: []string{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "Dot notation", + input: []string{"a.b.c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "Mixed notation", + input: []string{"a.b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "Multiple dots", + input: []string{"a.b", "c.d", "e"}, + expected: []string{"a", "b", "c", "d", "e"}, + }, + { + name: "Empty segments", + input: []string{"a..b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "Leading/trailing dots", + input: []string{".a.b.", "c"}, + expected: []string{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := expandPath(tt.input...) + if len(result) != len(tt.expected) { + t.Errorf("Expected length %d, got %d", len(tt.expected), len(result)) + return + } + for i := range result { + if result[i] != tt.expected[i] { + t.Errorf("At index %d: expected '%s', got '%s'", i, tt.expected[i], result[i]) + } + } + }) + } +} + +// TestComplexDotNotation 测试复杂的点号路径场景 +func TestComplexDotNotation(t *testing.T) { + type Company struct { + Name string + Address Address + CEO Person + } + + company := Company{ + Name: "TechCorp", + Address: Address{ + City: "Beijing", + Street: "Tech Street", + }, + CEO: Person{ + Name: "John Doe", + Age: 45, + Address: Address{ + City: "Shanghai", + }, + }, + } + + rfx := New(&company) + + // 深层嵌套访问 + ceoCity := rfx.Get("CEO.Address.City").String() + if ceoCity != "Shanghai" { + t.Errorf("Expected 'Shanghai', got '%s'", ceoCity) + } + + // 设置深层嵌套值 + rfx.Set("CEO.Address.City", "Guangzhou") + if company.CEO.Address.City != "Guangzhou" { + t.Errorf("Expected 'Guangzhou', got '%s'", company.CEO.Address.City) + } + + // 检查深层路径存在性 + if !rfx.Exists("CEO.Address.City") { + t.Error("CEO.Address.City should exist") + } +} + +// TestScopeSetIssue 测试 Scope 深度克隆的特性 +func TestScopeSetIssue(t *testing.T) { + p := Person{ + Name: "Alice", + Address: Address{ + City: "Beijing", + }, + } + + rfx := New(&p) + + // 测试直接通过路径设置值 - 这应该可以工作 + t.Run("Direct path set", func(t *testing.T) { + rfx.Set("Address.City", "Shanghai") + if p.Address.City != "Shanghai" { + t.Errorf("Expected 'Shanghai', got '%s'", p.Address.City) + } + }) + + // 测试通过 Scope 设置值 - 由于是深度克隆,不会修改原始值 + t.Run("Scope set - deep clone behavior", func(t *testing.T) { + addressScope := rfx.Scope("Address") + originalCity := p.Address.City // "Shanghai" + + // 在 Scope 上设置值 + addressScope.Set("City", "Guangzhou") + + // 原始值不应该被修改(因为 Scope 返回深度克隆) + if p.Address.City != originalCity { + t.Errorf("Original value should remain '%s', got '%s'", originalCity, p.Address.City) + } + + // Scope 内的值应该已经改变 + scopeCity := addressScope.Get("City").String() + if scopeCity != "Guangzhou" { + t.Errorf("Scope value should be 'Guangzhou', got '%s'", scopeCity) + } + }) + + // 测试获取 Scope 的值是独立副本 + t.Run("Scope returns independent copy", func(t *testing.T) { + // 获取 Address 的 Scope + addressScope := rfx.Scope("Address") + + // 获取 Scope 的 Any 值 + addr := addressScope.Any() + if address, ok := addr.(Address); ok { + originalCity := p.Address.City + + // 修改 Scope 返回的值 + addressScope.Set("City", "TestCity") + + // 原始值不应该被修改 + if p.Address.City != originalCity { + t.Errorf("Modifying scope should not affect original, expected '%s', got '%s'", + originalCity, p.Address.City) + } + + // 确保 address 变量本身也是副本 + _ = address + } + }) + + // 测试嵌套 Scope 的深度克隆 + t.Run("Nested scope deep clone", func(t *testing.T) { + p2 := Person{ + Name: "Bob", + Age: 25, + Address: Address{ + City: "Beijing", + Street: "Main St", + ZipCode: 100000, + }, + } + + sm2 := New(&p2) + + // 创建 Scope + scope := sm2.Scope("Address") + + // 在 Scope 上修改多个字段 + scope.Set("City", "Shenzhen") + scope.Set("Street", "New Street") + scope.Set("ZipCode", 518000) + + // 验证原始对象没有被修改 + if p2.Address.City != "Beijing" { + t.Errorf("Original City should be 'Beijing', got '%s'", p2.Address.City) + } + if p2.Address.Street != "Main St" { + t.Errorf("Original Street should be 'Main St', got '%s'", p2.Address.Street) + } + if p2.Address.ZipCode != 100000 { + t.Errorf("Original ZipCode should be 100000, got %d", p2.Address.ZipCode) + } + + // 验证 Scope 内的值已改变 + if scope.Get("City").String() != "Shenzhen" { + t.Errorf("Scope City should be 'Shenzhen', got '%s'", scope.Get("City").String()) + } + }) +} + +// TestScopeDeepClone 测试 Scope 深度克隆复杂嵌套结构 +func TestScopeDeepClone(t *testing.T) { + type Company struct { + Name string + Employees []Person + Locations map[string]Address + } + + company := Company{ + Name: "TechCorp", + Employees: []Person{ + { + Name: "Alice", + Age: 30, + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + Tags: []string{"go", "rust"}, + }, + { + Name: "Bob", + Age: 25, + Address: Address{ + City: "Shanghai", + }, + }, + }, + Locations: map[string]Address{ + "hq": { + City: "Beijing", + Street: "Tech Park", + ZipCode: 100000, + }, + }, + } + + rfx := New(&company) + + t.Run("Clone and modify nested slice", func(t *testing.T) { + // 创建 Employees 的 Scope + empScope := rfx.Scope("Employees") + + // 修改 Scope 中的值 + empScope.Set("0.Name", "Charlie") + empScope.Set("0.Age", 35) + + // 验证原始数据没有被修改 + if company.Employees[0].Name != "Alice" { + t.Errorf("Original employee name should be 'Alice', got '%s'", company.Employees[0].Name) + } + if company.Employees[0].Age != 30 { + t.Errorf("Original employee age should be 30, got %d", company.Employees[0].Age) + } + + // 验证 Scope 中的值已修改 + if empScope.Get("0", "Name").String() != "Charlie" { + t.Error("Scope employee name should be 'Charlie'") + } + }) + + t.Run("Clone and modify nested map", func(t *testing.T) { + // 创建 Locations 的 Scope + locScope := rfx.Scope("Locations") + + // 修改 Scope 中的值 + locScope.Set("hq.City", "Shanghai").Set("hq.ZipCode", 200000) + + // 验证原始数据没有被修改 + if company.Locations["hq"].City != "Beijing" { + t.Errorf("Original location city should be 'Beijing', got '%s'", company.Locations["hq"].City) + } + if company.Locations["hq"].ZipCode != 100000 { + t.Errorf("Original location zipcode should be 100000, got %d", company.Locations["hq"].ZipCode) + } + + // 验证 Scope 中的值已修改 + if locScope.Get("hq", "City").String() != "Shanghai" { + t.Error("Scope location city should be 'Shanghai'") + } + if locScope.Get("hq", "ZipCode").Int() != 200000 { + t.Error("Scope location zipcode should be 200000") + } + }) + + t.Run("Clone nested struct with slices", func(t *testing.T) { + // 创建第一个员工的 Scope + emp0Scope := rfx.Scope("Employees", "0") + + // 修改嵌套的切片 + emp0Scope.Set("Tags.0", "python") + + // 验证原始数据没有被修改 + if company.Employees[0].Tags[0] != "go" { + t.Errorf("Original tag should be 'go', got '%s'", company.Employees[0].Tags[0]) + } + + // 验证 Scope 中的值已修改 + if emp0Scope.Get("Tags", "0").String() != "python" { + t.Error("Scope tag should be 'python'") + } + }) +} + +// TestSetValueWithCastInStruct 测试在实际结构体中使用 cast 转换 +func TestSetValueWithCastInStruct(t *testing.T) { + type Config struct { + Port int + Host string + Enabled bool + Timeout float64 + } + + config := Config{} + rfx := New(&config) + + // 字符串转 int + rfx.Set("Port", "8080") + if config.Port != 8080 { + t.Errorf("Expected Port to be 8080, got %d", config.Port) + } + + // int 转 string + rfx.Set("Host", 12345) + if config.Host != "12345" { + t.Errorf("Expected Host to be '12345', got '%s'", config.Host) + } + + // 字符串转 bool + rfx.Set("Enabled", "true") + if !config.Enabled { + t.Errorf("Expected Enabled to be true, got %v", config.Enabled) + } + + // 字符串转 float + rfx.Set("Timeout", "30.5") + if config.Timeout != 30.5 { + t.Errorf("Expected Timeout to be 30.5, got %f", config.Timeout) + } +} + +// TestSetValueChainWithCast 测试链式调用中的 cast 转换 +func TestSetValueChainWithCast(t *testing.T) { + type User struct { + Name string + Age int + Active bool + Score float64 + } + + user := User{} + rfx := New(&user) + + // 链式设置,使用不同类型的值 + rfx.Set("Name", 12345). // int -> string + Set("Age", "30"). // string -> int + Set("Active", "1"). // string -> bool + Set("Score", "95.5") // string -> float64 + + if user.Name != "12345" { + t.Errorf("Expected Name to be '12345', got '%s'", user.Name) + } + if user.Age != 30 { + t.Errorf("Expected Age to be 30, got %d", user.Age) + } + if !user.Active { + t.Errorf("Expected Active to be true, got %v", user.Active) + } + if user.Score != 95.5 { + t.Errorf("Expected Score to be 95.5, got %f", user.Score) + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..af54cd3 --- /dev/null +++ b/util.go @@ -0,0 +1,141 @@ +package reflux + +import ( + "reflect" + "strings" + "unicode" +) + +// DeepClone 深度克隆一个 reflect.Value +func DeepClone(v reflect.Value) reflect.Value { + // 解引用指针和接口以获取实际值 + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + + if !v.IsValid() { + return reflect.Value{} + } + + // 创建新值的指针以便修改 + cloned := reflect.New(v.Type()) + CloneValue(cloned.Elem(), v) + return cloned +} + +// CloneValue 递归克隆值 +func CloneValue(dst, src reflect.Value) { + if !src.IsValid() { + return + } + + switch src.Kind() { + case reflect.Ptr: + if src.IsNil() { + return + } + dst.Set(reflect.New(src.Type().Elem())) + CloneValue(dst.Elem(), src.Elem()) + + case reflect.Interface: + if src.IsNil() { + return + } + cloned := DeepClone(src.Elem()) + if cloned.IsValid() { + dst.Set(cloned) + } + + case reflect.Struct: + for i := 0; i < src.NumField(); i++ { + if dst.Field(i).CanSet() { + CloneValue(dst.Field(i), src.Field(i)) + } + } + + case reflect.Slice: + if src.IsNil() { + return + } + dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap())) + for i := 0; i < src.Len(); i++ { + CloneValue(dst.Index(i), src.Index(i)) + } + + case reflect.Array: + for i := 0; i < src.Len(); i++ { + CloneValue(dst.Index(i), src.Index(i)) + } + + case reflect.Map: + if src.IsNil() { + return + } + dst.Set(reflect.MakeMap(src.Type())) + for _, key := range src.MapKeys() { + val := src.MapIndex(key) + // Map 的值需要特殊处理 + newVal := reflect.New(val.Type()).Elem() + CloneValue(newVal, val) + dst.SetMapIndex(key, newVal) + } + + default: + // 对于基本类型(int, string, bool, float等),直接赋值 + if dst.CanSet() { + dst.Set(src) + } + } +} + +// expandPath 展开路径,支持点号分割 +// 例如: expandPath("a.b", "c") -> []string{"a", "b", "c"} +func expandPath(p ...string) []string { + var result []string + for _, segment := range p { + // 按点号分割 + parts := strings.Split(segment, ".") + for _, part := range parts { + if part != "" { // 忽略空字符串 + result = append(result, part) + } + } + } + return result +} + +// capitalizeFirst 将字符串首字母转换为大写 +// 用于处理 struct 字段名,使其符合 Go 的公开字段命名规范 +func capitalizeFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + +// tryStructField 尝试获取 struct 字段,支持小写字段名自动转大写 +// 1. 首先尝试原始字段名 +// 2. 如果失败且首字母是小写,尝试首字母大写的版本 +func tryStructField(v reflect.Value, fieldName string) reflect.Value { + // 首先尝试原始字段名 + field := v.FieldByName(fieldName) + if field.IsValid() { + return field + } + + // 如果字段名首字母是小写,尝试首字母大写 + if len(fieldName) > 0 && unicode.IsLower(rune(fieldName[0])) { + capitalizedName := capitalizeFirst(fieldName) + field = v.FieldByName(capitalizedName) + if field.IsValid() { + return field + } + } + + return reflect.Value{} +}