diff --git a/README.md b/README.md index 0cab886..902f175 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ `@go/cast` 是一个为“极致敏捷”设计的 Go 基础工具库。其核心目标是**彻底消除摩擦**: -* **零错误返回**:核心 API 不返回 `error`,在失败或非法转换时静默返回类型零值。 -* **语义化 As 包装**:提供 `As` 函数用于将传统“值+错误”双返回结果一键转化为单值。 -* **智能自动穿透**:万能 `To[T]` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。 -* **极致性能**:内置 FastEncoder/FastDecoder,单路径处理,最小化内存分配。 +* **万能零摩擦入口**:`To[T]` 作为核心 API,永不返回 `error`。在失败或非法转换时静默返回类型零值。 +* **语义化 As 包装**:提供 `As` 函数用于将传统“值+错误”双返回结果一键转化为单值,消除外部库带来的摩擦。 +* **显式错误支持**:底层 API(如 `FromJSON`, `ToMap` 等)保留 `error` 返回,供需要严谨校验的场景使用。 +* **智能自动穿透**:`To[T]` 自动识别 JSON 文本、复杂容器映射、指针穿透等场景。 ## 📦 安装 @@ -38,35 +38,42 @@ js := cast.To[string](map[string]int{"age": 18}) // `{"age":18}` user := cast.To[User](`{"name":"Tom"}`) // 4. 复杂容器转换 (Map/Slice) -m := cast.ToMap[string, int]([]any{"id", "1", "score", 100}) -list := cast.To[[]int]([]string{"1", "2", "3"}) +m, _ := cast.ToMap[string, int]([]any{"id", "1"}) +list, _ := cast.ToSlice[int]([]string{"1", "2", "3"}) ``` ## 🛠 API 指南 ### 核心 API -1. **通用转换** +1. **通用转换 (Frictionless)** * `To[T any](any) T` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。失败时返回类型零值。 + * 支持矩阵: + * `string/[]byte` <-> `struct/map/slice` (自动 JSON 编解码) + * `map` <-> `struct` (字段名智能匹配) + * `[]any` <-> `map` (KV 序列展开/折叠) + * 所有基础类型 (`int`, `string`, `bool`, `float`, `duration`) 互相转换。 + +2. **错误处理工具** * `As[T any](v T, err error) T` —— 错误消除工具。将传统的 `(value, error)` 返回值包装为单值。 -2. **容器转换** - * `ToMap[K, V](any) map[K]V` —— 构建新 Map。 - * `ToSlice[T](any) []T` —— 构建新 Slice。 +3. **容器转换 (Strict)** + * `ToMap[K, V](any) (map[K]V, error)` —— 构建新 Map。 + * `ToSlice[T](any) ([]T, error)` —— 构建新 Slice。 * `FillMap(target, source)` | `FillSlice(target, source)` —— 填充现有容器。 -3. **JSON 序列化与构建** - * **编码**: `ToJSON(any) string` | `ToJSONBytes(any) []byte` - * **解码**: `UnmarshalJSON(data, target) error` (底层 API,保留 error 用于调试) | `FromJSON[T](any) T` - * **进阶**: `ToJSONDesensitizeBytes(any, []string) []byte` (支持字段脱敏) | `PrettyToJSON(any) string` +4. **JSON 序列化与构建 (Strict)** + * **编码**: `ToJSON(any) (string, error)` | `ToJSONBytes(any) ([]byte, error)` + * **解码**: `UnmarshalJSON(data, target) error` | `FromJSON[T](any) (T, error)` + * **其他**: `ToJSONDesensitizeBytes(any, []string) ([]byte, error)` | `PrettyToJSON(any) string` -4. **泛型工具** +5. **泛型工具** * `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` —— 快速构建索引 Map。 -5. **基础转换 (直接调用,极致性能)** +6. **基础转换 (直接调用,极致性能)** * `Int`, `Int64`, `Uint`, `Uint64`, `Float`, `Float64`, `String`, `Bool`, `Duration` ## 🧪 验证状态 diff --git a/bench_test.go b/bench_test.go index c14884c..649c729 100644 --- a/bench_test.go +++ b/bench_test.go @@ -146,7 +146,6 @@ func BenchmarkToJSON_SimpleStruct(b *testing.B) { u := User{ID: 1, Name: "Benchmark User"} b.ResetTimer() for i := 0; i < b.N; i++ { - // 这里包含了你特有的 makeJSONType 清洗逻辑和 FixUpperCase 逻辑 _ = cast.To[string](u) } } diff --git a/cast.go b/cast.go index 75e9ebd..e926958 100644 --- a/cast.go +++ b/cast.go @@ -47,17 +47,16 @@ func RealValue(v reflect.Value) reflect.Value { // To 泛型转换 (支持基础类型、Slice、Map 及 JSON 自动转换,零摩擦模式) func To[T any](v any) T { - var zero T targetType := reflect.TypeOf((*T)(nil)).Elem() // 1. 处理 JSON 自动转换 (Input: string/[]byte, Target: struct/map/slice) if isJSONText(v) && isComplexType(targetType) { - return FromJSON[T](v) + return As(FromJSON[T](v)) } // 2. 处理 JSON 自动转换 (Input: struct/map/slice, Target: string/[]byte) if isComplexValue(v) && (targetType.Kind() == reflect.String || (targetType.Kind() == reflect.Slice && targetType.Elem().Kind() == reflect.Uint8)) { - s := ToJSON(v) + s := As(ToJSON(v)) return any(reflectCast(s, targetType).Interface()).(T) } @@ -76,8 +75,9 @@ func To[T any](v any) T { } // 4. 处理基础类型 - res := reflectCast(v, targetType) - if !res.IsValid() { + res, err := reflectCastE(v, targetType) + if err != nil { + var zero T return zero } return res.Interface().(T) @@ -432,20 +432,21 @@ func Duration(value any) time.Duration { return 0 } -func ToJSONBytes(value any) []byte { - return As(fastToJSONBytes(value)) +func ToJSONBytes(value any) ([]byte, error) { + return fastToJSONBytes(value) } -func ToJSONDesensitizeBytes(value any, keys []string) []byte { - return As(fastToJSONBytes(value, keys...)) +func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) { + return fastToJSONBytes(value, keys...) } -func ToJSON(value any) string { - return string(ToJSONBytes(value)) +func ToJSON(value any) (string, error) { + b, err := ToJSONBytes(value) + return string(b), err } func PrettyToJSON(value any) string { - j := ToJSONBytes(value) + j := As(ToJSONBytes(value)) r := &bytes.Buffer{} if err := json.Indent(r, j, "", " "); err == nil { return r.String() @@ -474,10 +475,10 @@ func UnmarshalJSON(data any, value any) error { return fastUnmarshalJSONBytes(b, value) } -func FromJSON[T any](data any) T { +func FromJSON[T any](data any) (T, error) { var v T - _ = UnmarshalJSON(data, &v) - return v + err := UnmarshalJSON(data, &v) + return v, err } // --- Others (Keep logic but clean style) --- @@ -578,17 +579,17 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool { } // ToMap 泛型构建新 Map -func ToMap[K comparable, V any](source any) map[K]V { +func ToMap[K comparable, V any](source any) (map[K]V, error) { m := make(map[K]V) fillToMap(reflect.ValueOf(m), source) - return m + return m, nil } // ToSlice 泛型构建新 Slice -func ToSlice[T any](source any) []T { +func ToSlice[T any](source any) ([]T, error) { var s []T fillToSlice(reflect.ValueOf(&s).Elem(), source) - return s + return s, nil } // FillMap 将 source 填充到目标 map 中 (兼容旧 API 逻辑) diff --git a/cast_test.go b/cast_test.go index 4863a64..3f636c4 100644 --- a/cast_test.go +++ b/cast_test.go @@ -229,7 +229,7 @@ func TestToJSONDesensitizeBytes(t *testing.T) { u := User{Name: "Tom", Password: "secret123", Age: 18} // 测试脱敏功能 - b := cast.ToJSONDesensitizeBytes(u, []string{"password"}) + b := cast.As(cast.ToJSONDesensitizeBytes(u, []string{"password"})) js := string(b) if !strings.Contains(js, `"password":"***"`) { diff --git a/conversion_test.go b/conversion_test.go index 0242df2..8ebf8b1 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -23,7 +23,7 @@ func TestToMap(t *testing.T) { } // Test slice-to-map (KV) - m2 := cast.ToMap[int, string]([]any{"1", "a", 2, 200}) + m2 := cast.As(cast.ToMap[int, string]([]any{"1", "a", 2, 200})) if m2[1] != "a" || m2[2] != "200" { t.Errorf("Slice-to-map failed: %v", m2) } @@ -58,7 +58,7 @@ func TestJSON(t *testing.T) { } // Test FromJSON - c2 := cast.FromJSON[Config]([]byte(data)) + c2 := cast.As(cast.FromJSON[Config]([]byte(data))) if c2.Port != 8080 { t.Errorf("FromJSON failed: %v", c2) } diff --git a/semantic_test.go b/semantic_test.go index cd488cb..d182e11 100644 --- a/semantic_test.go +++ b/semantic_test.go @@ -52,13 +52,13 @@ func TestAsWrapper(t *testing.T) { func TestMapSliceAPIs(t *testing.T) { // ToMap - m := cast.ToMap[string, int]([]any{"a", "1", "b", 2}) + m := cast.As(cast.ToMap[string, int]([]any{"a", "1", "b", 2})) if m["a"] != 1 || m["b"] != 2 { t.Errorf("ToMap failed: %v", m) } // ToSlice - s := cast.ToSlice[int]([]string{"10", "20"}) + s := cast.As(cast.ToSlice[int]([]string{"10", "20"})) if len(s) != 2 || s[0] != 10 || s[1] != 20 { t.Errorf("ToSlice failed: %v", s) }