diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3275a..14b5c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # CHANGELOG -## [v1.1.1] - 2026-05-02 +## [v1.1.2] - 2026-05-04 +### Added +- 新增 `ToMap` 和 `ToSlice`: 侧重于原地填充/追加,支持 Struct 继承拍平、KV 序列化与自动类型转换。 +- 新增 `MakeMap`, `MakeSlice`, `FromJSON`, `FromYAML` 系列: 泛型构建新对象的零摩擦 API。 +- 增强 `UnmarshalJSON` / `UnmarshalYAML`: 重构为副作用模式(返回 error),支持多种输入类型。 +### Removed +- 移除 `Ints` 和 `Strings`: 由更通用的 `MakeSlice` 替代,保持 API 简洁。 + +## [v1.1.1] - 2026-04-12 + - **修复**: 解决 `FastEncoder` 在编码结构体时忽略匿名嵌入(Embedded)字段的问题,确保组合对象的完整序列化。 ## [v1.1.0] - 2026-05-02 diff --git a/README.md b/README.md index 20ca39d..cee7f7d 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,22 @@ go get apigo.cc/go/cast ```go import "apigo.cc/go/cast" -// 像 JS 一样转换 +// 基础转换 age := cast.Int("18") // 18 // 泛型三元运算 status := cast.If(isAdmin, "Admin", "User") -// JSON 序列化增强 -var u struct { UserID int } -cast.UnmarshalJSON("userId: 1001", &u) // u.UserID = 1001 -cast.MustToJSON(u) // "userId": 1001,首字母自动小写 +// 自动构建并填充 (Make 系列) +list := cast.MakeSlice[int]("123") // []int{123} +m := cast.MakeMap[string, any](map[string]int{"age": 18}) // map[string]any{"age": 18} -// 字符串切分增强 -cast.Split(",", ",") // [],忽略无效切片 -cast.Split("a, b, ", ",") // ["a", "b"],去除空白字符 +// JSON 泛型零摩擦构建 (From 系列,支持 string 或更安全的 []byte) +user := cast.MustFromJSON[User](`{"name": "Tom"}`) +// 原地填充 (To 系列) +var config map[string]any +cast.ToMap(&config, myStruct) // 自动拍平 struct 并填充到 map ``` ## 🛠 API 指南 @@ -50,23 +51,26 @@ cast.Split("a, b, ", ",") // ["a", "b"],去除空白字符 * `String(any) string` | `Bool(any) bool` * `Duration(any) time.Duration` -2. **批量转换(Slice Casting)** - * `Ints(any) []int64` - * `Strings(any) []string` +2. **高级转化(Map/Slice Helpers)** + * `ToMap(target any, source any)` —— 将 source 填充到目标 map。支持 struct (递归拍平且导出字段转小写)、slice (KV 序列) 或 map (合并)。 + * `ToSlice(target any, source any)` —— 将 source 填充到目标 slice。支持 map (KV 序列)、slice (追加) 或普通值 (追加)。 + * `MakeMap[K comparable, V any](source any) map[K]V` —— 泛型构建并填充新 Map。 + * `MakeSlice[T any](source any) []T` —— 泛型构建并填充新 Slice。 -3. **泛型工具(Type Helpers)** +3. **序列化与泛型构建(JSON & YAML)** + * **JSON 编码**: `ToJSON(any)(string, error)` | `ToJSONDesensitize(any, []string)(string, error)` | `MustToJSON(any)string` + * **JSON 解码 (原地)**: `UnmarshalJSON(data, any) error` (支持 string 或为了内存安全推荐使用的 []byte) + * **JSON 构建 (泛型)**: `FromJSON[T](data) (T, error)` | `MustFromJSON[T](data) T` + * **YAML 编码**: `ToYAML(any)(string, error)` | `MustToYAML(any)string` + * **YAML 解码 (原地)**: `UnmarshalYAML(data, any) error` + * **YAML 构建 (泛型)**: `FromYAML[T](data) (T, error)` | `MustFromYAML[T](data) T` + +4. **泛型工具(Type Helpers)** * `If[T any](bool, T, T) T` —— 三元逻辑 * `In[T comparable]([]T, T) bool` —— 包含判断 * `Ptr[T any](T) *T` —— 取指针 * `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速索引化 -4. **序列化(JSON & YAML)** - * **JSON 编码**: `ToJSON(any)(string, error)` | `ToJSONDesensitize(any, []string)(string, error)` | `MustToJSON(any)string` | `PrettyToJSON(any)string` - * **JSON 字节**: `ToJSONBytes(any)([]byte, error)` | `ToJSONDesensitizeBytes(any, []string)([]byte, error)` | `MustJSONBytes(any)[]byte` | `PrettyToJSONBytes(any)[]byte` - * **JSON 解码**: `UnmarshalJSON(string, any)(any, error)` | `MustUnmarshalJSON(string, any)any` | `UnmarshalJSONBytes([]byte, any)(any, error)` | `MustUnmarshalJSONBytes([]byte, any)any` - * **YAML 编码**: `ToYAML(any)(string, error)` | `MustToYAML(any)string` | `YAMLBytes(any)([]byte, error)` | `MustYAMLBytes(any)[]byte` - * **YAML 解码**: `UnmarshalYAML(string, any)(any, error)` | `MustUnmarshalYAML(string, any)any` | `UnmarshalYAMLBytes([]byte, any)(any, error)` | `MustUnmarshalYAMLBytes([]byte, any)any` - 5. **辅助工具(Utilities)** * **切分**: `Split(s, sep) []string` | `SplitArgs(s) []string` * **拼接**: `UniqueAppend([]string, ...any) []string` | `JoinArgs([]string, sep) string` diff --git a/TEST.md b/TEST.md index c424413..f7fba6c 100644 --- a/TEST.md +++ b/TEST.md @@ -2,13 +2,14 @@ ## 覆盖场景 (Coverage Scenarios) - **核心类型转换**: `Int64`, `Uint64`, `Float64`, `Bool`, `String`,包括边界值、零值及非法字符串输入。 -- **复合类型处理**: `Ints`, `Strings` 自动解析 JSON 字符串或直接转换。 +- **复合类型处理**: `ToMap`, `ToSlice`, `MakeMap`, `MakeSlice` 等全能转化工具,支持 Struct 拍平。 - **JSON/YAML 序列化**: - 深度结构体映射,支持 `FastEncoder` 单路径处理。 - **去标签化算法**: 自动识别 `UserID` -> `userID` 等符合工程习惯的转换。 - **脱敏支持**: `ToJSONDesensitize` 在编码阶段原生支持字段脱敏。 - **Map 兼容性**: 原生支持 `map[any]any` 及 Goja 伪数组转换。 -- **JSON 反序列化**: +- **JSON/YAML 构建**: + - **Make 系列**: 实现泛型零摩擦构建新对象。 - **FastDecoder**: 实现单路径流式解析,跳过中间 Map 分配。 - **Frictionless 匹配**: 支持大小写不敏感、忽略下划线等灵活的 Key 映射规则。 - **智能初始化**: 自动处理嵌套指针、Slice 和 Map 的初始化。 @@ -17,8 +18,10 @@ ## 性能基准 (Benchmark Results - Intel(R) Core(TM) i9) - `If`: ~0.24 ns/op -- `Int64`: ~20.4 ns/op -- `ToJSON (SimpleStruct)`: ~448.5 ns/op (相比旧版提升 ~30%) -- `ToJSON (DirtyMap)`: ~1126 ns/op (相比旧版提升 ~70%) +- `Int64`: ~20 ns/op +- `ToMap`: ~700-800 ns/op (含 Struct 拍平与类型转换) +- `ToSlice`: ~1200-1400 ns/op (含 KV 展开与类型转换) +- `ToJSON (SimpleStruct)`: ~440 ns/op (相比旧版提升 ~30%) +- `ToJSON (DirtyMap)`: ~1100 ns/op (相比旧版提升 ~70%) - `UnmarshalJSON`: 高性能单路径解析,显著降低内存分配。 - `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。 diff --git a/bench_test.go b/bench_test.go index 8db0929..9d9ff87 100644 --- a/bench_test.go +++ b/bench_test.go @@ -36,7 +36,7 @@ func TestEdgeCases(t *testing.T) { } // 4. JSON 异常输入 - if _, err := cast.UnmarshalJSON("{invalid_json}", nil); err == nil { + if err := cast.UnmarshalJSON("{invalid_json}", nil); err == nil { t.Log("UnmarshalJSON handled invalid input") } } @@ -88,6 +88,25 @@ func BenchmarkSplit(b *testing.B) { } } +func BenchmarkToMap(b *testing.B) { + m := make(map[string]int) + kv := []any{"a", 1, "b", 2, "c", 3, "d", 4, "e", 5} + b.ResetTimer() + for i := 0; i < b.N; i++ { + cast.ToMap(m, kv) + } +} + +func BenchmarkToSlice(b *testing.B) { + m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5} + var s []any + b.ResetTimer() + for i := 0; i < b.N; i++ { + s = s[:0] + cast.ToSlice(&s, m) + } +} + // 1. 测试 Int 转换的快速路径 (命中 switch case) func BenchmarkInt_FastPath(b *testing.B) { val := "123456" diff --git a/cast.go b/cast.go index 7d7c94d..0d501ec 100644 --- a/cast.go +++ b/cast.go @@ -235,57 +235,6 @@ func Bool(value any) bool { return false } -func Ints(value any) []int64 { - switch realValue := value.(type) { - case []any: - result := make([]int64, len(realValue)) - for i, v := range realValue { - result[i] = Int64(v) - } - return result - case string: - if strings.HasPrefix(realValue, "[") { - result := make([]int64, 0) - UnmarshalJSON(realValue, &result) - return result - } - return []int64{Int64(value)} - } - rv := reflect.ValueOf(value) - if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface { - if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer { - return Ints(rv.Interface()) - } - } - return []int64{Int64(value)} -} - -func Strings(value any) []string { - switch realValue := value.(type) { - case []any: - result := make([]string, len(realValue)) - for i, v := range realValue { - result[i] = String(v) - } - return result - case string: - if strings.HasPrefix(realValue, "[") { - result := make([]string, 0) - UnmarshalJSON(realValue, &result) - return result - } - return []string{String(value)} - } - - rv := reflect.ValueOf(value) - if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface { - if rv = RealValue(rv); rv.IsValid() && rv.CanInterface() && rv.Kind() != reflect.Pointer { - return Strings(rv.Interface()) - } - } - return []string{String(value)} -} - func Duration(value any) time.Duration { if value == nil { return 0 @@ -353,27 +302,33 @@ func PrettyToJSONBytes(value any) []byte { func PrettyToJSON(value any) string { return string(PrettyToJSONBytes(value)) } -func UnmarshalJSONBytes(data []byte, value any) (any, error) { - if value == nil { - var v any - value = &v +func UnmarshalJSON(data any, value any) error { + var b []byte + switch v := data.(type) { + case string: + b = []byte(v) + case []byte: + b = v + default: + return fmt.Errorf("unsupported data type: %T", data) } - err := fastUnmarshalJSONBytes(data, value) - return value, err + return fastUnmarshalJSONBytes(b, value) } -func MustUnmarshalJSONBytes(data []byte, value any) any { - v, err := UnmarshalJSONBytes(data, value) +func FromJSON[T any](data any) (T, error) { + var v T + err := UnmarshalJSON(data, &v) + return v, err +} + +func MustFromJSON[T any](data any) T { + v, err := FromJSON[T](data) if err != nil { - return nil + panic(err) } return v } -func UnmarshalJSON(str string, value any) (any, error) { return UnmarshalJSONBytes([]byte(str), value) } - -func MustUnmarshalJSON(str string, value any) any { return MustUnmarshalJSONBytes([]byte(str), value) } - func ToYAML(value any) (string, error) { j, err := YAMLBytes(value) if err != nil { @@ -384,11 +339,32 @@ func ToYAML(value any) (string, error) { func MustToYAML(value any) string { return string(MustYAMLBytes(value)) } -func UnmarshalYAML(data string, value any) (any, error) { - return UnmarshalYAMLBytes([]byte(data), value) +func UnmarshalYAML(data any, value any) error { + var b []byte + switch v := data.(type) { + case string: + b = []byte(v) + case []byte: + b = v + default: + return fmt.Errorf("unsupported data type: %T", data) + } + return yaml.Unmarshal(b, value) } -func MustUnmarshalYAML(data string, value any) any { return MustUnmarshalYAMLBytes([]byte(data), value) } +func FromYAML[T any](data any) (T, error) { + var v T + err := UnmarshalYAML(data, &v) + return v, err +} + +func MustFromYAML[T any](data any) T { + v, err := FromYAML[T](data) + if err != nil { + panic(err) + } + return v +} func YAMLBytes(value any) ([]byte, error) { j, err := yaml.Marshal(value) @@ -406,19 +382,6 @@ func MustYAMLBytes(value any) []byte { return j } -func UnmarshalYAMLBytes(data []byte, value any) (any, error) { - err := yaml.Unmarshal(data, value) - return value, err -} - -func MustUnmarshalYAMLBytes(data []byte, value any) any { - v, err := UnmarshalYAMLBytes(data, value) - if err != nil { - return nil - } - return v -} - // --- Others (Keep logic but clean style) --- func Split(s, sep string) []string { @@ -516,12 +479,164 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool { return r } +// ToMap 将 source 填充到目标 map 中,自动转换类型。 +// 如果 source 是 struct,则提取导出字段(含嵌入字段,字段名首字母转小写); +// 如果 source 是 slice/array,则视为 [K1, V1, K2, V2, ...] 序列; +// 如果 source 是 map,则进行合并(Merge)。 +func ToMap(target any, source any) { + rv := reflect.ValueOf(target) + for rv.Kind() == reflect.Pointer { + if rv.IsNil() { + if !rv.CanSet() { + return + } + elemType := rv.Type().Elem() + if elemType.Kind() == reflect.Map || elemType.Kind() == reflect.Pointer || elemType.Kind() == reflect.Struct { + rv.Set(reflect.New(elemType)) + } else { + return + } + } + rv = rv.Elem() + } + + if rv.Kind() != reflect.Map { + return + } + + if rv.IsNil() { + if rv.CanSet() { + rv.Set(reflect.MakeMap(rv.Type())) + } else { + return + } + } + + kt := rv.Type().Key() + vt := rv.Type().Elem() + + sv := RealValue(reflect.ValueOf(source)) + switch sv.Kind() { + case reflect.Struct: + fillMapFromStruct(rv, sv, kt, vt) + case reflect.Slice, reflect.Array: + for i := 0; i < sv.Len(); i += 2 { + k := sv.Index(i).Interface() + var v any + if i+1 < sv.Len() { + v = sv.Index(i+1).Interface() + } + rv.SetMapIndex(reflectCast(k, kt), reflectCast(v, vt)) + } + case reflect.Map: + iter := sv.MapRange() + for iter.Next() { + rv.SetMapIndex(reflectCast(iter.Key().Interface(), kt), reflectCast(iter.Value().Interface(), vt)) + } + } +} + +func fillMapFromStruct(targetMap, sv reflect.Value, kt, vt reflect.Type) { + st := sv.Type() + for i := 0; i < sv.NumField(); i++ { + field := st.Field(i) + if !field.IsExported() { + continue + } + if field.Anonymous { + fillMapFromStruct(targetMap, sv.Field(i), kt, vt) + continue + } + targetMap.SetMapIndex(reflectCast(GetLowerName(field.Name), kt), reflectCast(sv.Field(i).Interface(), vt)) + } +} + +// ToSlice 将 source 填充到目标 slice 中,自动转换类型。 +// 如果 source 是 map,则展开为 [K1, V1, K2, V2, ...] 序列; +// 如果 source 是 slice/array,则进行元素拷贝/追加; +// 如果 source 是普通值,则直接作为元素追加。 +func ToSlice(target any, source any) { + rv := reflect.ValueOf(target) + if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice { + return + } + sliceRv := rv.Elem() + et := sliceRv.Type().Elem() + + sv := RealValue(reflect.ValueOf(source)) + switch sv.Kind() { + case reflect.Map: + iter := sv.MapRange() + for iter.Next() { + sliceRv.Set(reflect.Append(sliceRv, reflectCast(iter.Key().Interface(), et))) + sliceRv.Set(reflect.Append(sliceRv, reflectCast(iter.Value().Interface(), et))) + } + case reflect.Slice, reflect.Array: + for i := 0; i < sv.Len(); i++ { + sliceRv.Set(reflect.Append(sliceRv, reflectCast(sv.Index(i).Interface(), et))) + } + case reflect.Invalid: + // Nil source, do nothing + default: + sliceRv.Set(reflect.Append(sliceRv, reflectCast(source, et))) + } +} + +// MakeMap 泛型构建新 Map +func MakeMap[K comparable, V any](source any) map[K]V { + m := make(map[K]V) + ToMap(m, source) + return m +} + +// MakeSlice 泛型构建新 Slice +func MakeSlice[T any](source any) []T { + var s []T + ToSlice(&s, source) + return s +} + +func reflectCast(value any, t reflect.Type) reflect.Value { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return reflect.ValueOf(Int64(value)).Convert(t) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return reflect.ValueOf(Uint64(value)).Convert(t) + case reflect.Float32, reflect.Float64: + return reflect.ValueOf(Float64(value)).Convert(t) + case reflect.String: + return reflect.ValueOf(String(value)).Convert(t) + case reflect.Bool: + return reflect.ValueOf(Bool(value)).Convert(t) + case reflect.Interface: + if value == nil { + return reflect.Zero(t) + } + return reflect.ValueOf(value) + } + + if t == reflect.TypeOf(time.Duration(0)) { + return reflect.ValueOf(Duration(value)) + } + + return reflect.Zero(t) +} + // 补充缺失的 Key 转换工具 func GetLowerName(s string) string { - if s == "" { - return "" + if len(s) > 0 && s[0] >= 'A' && s[0] <= 'Z' { + hasLower := false + for i := 0; i < len(s); i++ { + if s[i] >= 'a' && s[i] <= 'z' { + hasLower = true + break + } + } + if hasLower { + return strings.ToLower(s[:1]) + s[1:] + } } - return strings.ToLower(s[:1]) + s[1:] + return s } func GetUpperName(s string) string { diff --git a/conversion_test.go b/conversion_test.go new file mode 100644 index 0000000..1e0f964 --- /dev/null +++ b/conversion_test.go @@ -0,0 +1,91 @@ +package cast_test + +import ( + "testing" + + "apigo.cc/go/cast" +) + +func TestToMap(t *testing.T) { + // Test struct-to-map with inheritance + type Base struct { + ID int + } + type User struct { + Base + Name string + } + u := User{Base: Base{ID: 1}, Name: "Tom"} + m1 := make(map[string]any) + cast.ToMap(m1, u) + if m1["ID"] != 1 || m1["name"] != "Tom" { + t.Errorf("Struct inheritance to map failed: %v", m1) + } + + // Test slice-to-map (KV) + m2 := cast.MakeMap[int, string]([]any{"1", "a", 2, 200}) + if m2[1] != "a" || m2[2] != "200" { + t.Errorf("Slice-to-map failed: %v", m2) + } +} + +func TestToSlice(t *testing.T) { + // Test primitive-to-slice (append) + var s1 []int + cast.ToSlice(&s1, "123") + if len(s1) != 1 || s1[0] != 123 { + t.Errorf("Primitive-to-slice failed: %v", s1) + } + + // Test append slice + cast.ToSlice(&s1, []string{"456"}) + if len(s1) != 2 || s1[1] != 456 { + t.Errorf("Append slice to slice failed: %v", s1) + } +} + +func TestJSONYAML(t *testing.T) { + type Config struct { + Port int `json:"port" yaml:"port"` + } + data := `{"port": 8080}` + + // Test UnmarshalJSON + var c1 Config + err := cast.UnmarshalJSON(data, &c1) + if err != nil || c1.Port != 8080 { + t.Errorf("UnmarshalJSON failed: %v, %v", err, c1) + } + + // Test FromJSON + c2, err := cast.FromJSON[Config]([]byte(data)) + if err != nil || c2.Port != 8080 { + t.Errorf("FromJSON failed: %v, %v", err, c2) + } + + // Test MustFromJSON + c3 := cast.MustFromJSON[Config](data) + if c3.Port != 8080 { + t.Errorf("MustFromJSON failed: %v", c3) + } + + // YAML + yData := "port: 9090" + c4 := cast.MustFromYAML[Config](yData) + if c4.Port != 9090 { + t.Errorf("MustFromYAML failed: %v", c4) + } +} + +func TestMakeSlice(t *testing.T) { + // Test MakeSlice with various inputs + s1 := cast.MakeSlice[int]("123") + if len(s1) != 1 || s1[0] != 123 { + t.Errorf("MakeSlice primitive failed: %v", s1) + } + + s2 := cast.MakeSlice[string]([]int{1, 2}) + if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" { + t.Errorf("MakeSlice slice failed: %v", s2) + } +} diff --git a/decoder_test.go b/decoder_test.go index a211d13..45fa35b 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -15,7 +15,7 @@ func TestFastUnmarshal_Frictionless(t *testing.T) { // 测试各种 Key 格式的匹配 data := `{"user_id": 1001, "UserName": "Tom", "isadmin": "true"}` var u User - _, err := cast.UnmarshalJSON(data, &u) + err := cast.UnmarshalJSON(data, &u) if err != nil { t.Fatalf("UnmarshalJSON failed: %v", err) }