refactor: semantic API audit (v1.2.0) - add To[T]/As[T], remove YAML, unify Map/Slice/JSON APIs (by AI)
This commit is contained in:
parent
467ec92e1b
commit
b4c18d74c3
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,5 +1,20 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [v1.2.0] - 2026-05-04
|
||||||
|
### Added
|
||||||
|
- **语义化 API**: 引入 `To[T]` (严格/含错) 和 `As[T]` (静默/零值) 泛型接口,作为全能类型转换入口。
|
||||||
|
- **智能穿透**: `To[T]` 支持 JSON 自动双向转换(Input string/[]byte <-> Target struct/map/slice)。
|
||||||
|
- **容器 API 统一**:
|
||||||
|
- `ToMap` / `AsMap` (替代 `MakeMap`)
|
||||||
|
- `ToSlice` / `AsSlice` (替代 `MakeSlice`)
|
||||||
|
- `AsJSON` / `AsJSONBytes` (替代 `MustToJSON`)
|
||||||
|
- `AsFromJSON` (替代 `MustFromJSON`)
|
||||||
|
### Removed
|
||||||
|
- **YAML 支持**: 移除 `gopkg.in/yaml.v3` 依赖及所有 YAML 相关 API,专注 JSON 以保持核心极致精简。
|
||||||
|
### Deprecated
|
||||||
|
- **MustXxx 系列**: 建议迁移至语义更明确的 `AsXxx`。
|
||||||
|
- **MakeXxx 系列**: 建议迁移至 `AsMap` / `AsSlice`。
|
||||||
|
|
||||||
## [v1.1.2] - 2026-05-04
|
## [v1.1.2] - 2026-05-04
|
||||||
### Added
|
### Added
|
||||||
- 新增 `ToMap` 和 `ToSlice`: 侧重于原地填充/追加,支持 Struct 继承拍平、KV 序列化与自动类型转换。
|
- 新增 `ToMap` 和 `ToSlice`: 侧重于原地填充/追加,支持 Struct 继承拍平、KV 序列化与自动类型转换。
|
||||||
|
|||||||
85
README.md
85
README.md
@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
## 🎯 设计哲学
|
## 🎯 设计哲学
|
||||||
|
|
||||||
`@go/cast` 是一个为“敏捷开发”设计的 Go 基础工具库。设计初衷是打破 Go 严苛类型系统带来的繁琐摩擦,在处理数据时更关注需要什么类型而不是原本是什么类型。
|
`@go/cast` 是一个为“敏捷开发”设计的 Go 基础工具库。其核心目标是**消除类型摩擦**,让开发者在处理数据时更关注“我想要什么”而不是“原本是什么”。
|
||||||
|
|
||||||
* **弱化类型摩擦**:转换函数在失败时返回合理零值,专注业务流。
|
* **语义化 API**:通过 `To[T]` (严格/含错) 和 `As[T]` (静默/零值) 提供一致的调用体验。
|
||||||
* **补足语言短板**:提供泛型工具类补足 Go 语言无三元运算等缺陷。
|
* **智能穿透**:自动识别 JSON 文本、结构体映射、KV 展开等复杂场景。
|
||||||
* **去标签化**:支持自动将 struct 字段名大写导出为小写,无需手动添加 JSON tag。
|
* **极致性能**:内置 FastEncoder/FastDecoder,单路径处理,最小化内存分配。
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
@ -22,59 +22,50 @@ go get apigo.cc/go/cast
|
|||||||
```go
|
```go
|
||||||
import "apigo.cc/go/cast"
|
import "apigo.cc/go/cast"
|
||||||
|
|
||||||
// 基础转换
|
// 1. 语义化转换 (To[T] 返回错误, As[T] 返回零值)
|
||||||
age := cast.Int("18") // 18
|
age := cast.As[int]("18") // 18
|
||||||
|
val, err := cast.To[int]("abc") // 0, error
|
||||||
|
|
||||||
// 泛型三元运算
|
// 2. 智能 JSON 自动转换
|
||||||
|
// 输入 map/struct -> 目标 string: 自动序列化
|
||||||
|
js := cast.As[string](map[string]int{"age": 18}) // `{"age":18}`
|
||||||
|
|
||||||
|
// 输入 string/[]byte -> 目标 struct/map: 自动反序列化
|
||||||
|
user, _ := cast.To[User](`{"name":"Tom"}`)
|
||||||
|
|
||||||
|
// 3. 复杂容器转换 (Map/Slice)
|
||||||
|
m, _ := cast.ToMap[string, int]([]any{"id", "1", "score", 100})
|
||||||
|
list := cast.AsSlice[int]([]string{"1", "2", "3"})
|
||||||
|
|
||||||
|
// 4. 泛型三元运算
|
||||||
status := cast.If(isAdmin, "Admin", "User")
|
status := cast.If(isAdmin, "Admin", "User")
|
||||||
|
|
||||||
// 自动构建并填充 (Make 系列)
|
|
||||||
list := cast.MakeSlice[int]("123") // []int{123}
|
|
||||||
m := cast.MakeMap[string, any](map[string]int{"age": 18}) // map[string]any{"age": 18}
|
|
||||||
|
|
||||||
// JSON 泛型零摩擦构建 (From 系列,支持 string 或更安全的 []byte)
|
|
||||||
user := cast.MustFromJSON[User](`{"name": "Tom"}`)
|
|
||||||
|
|
||||||
// 原地填充 (To 系列)
|
|
||||||
var config map[string]any
|
|
||||||
cast.ToMap(&config, myStruct) // 自动拍平 struct 并填充到 map
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠 API 指南
|
## 🛠 API 指南
|
||||||
|
|
||||||
### 核心能力
|
### 核心语义 API
|
||||||
|
|
||||||
1. **基础转换(含多级指针穿透)**
|
1. **通用转换**
|
||||||
* `Int(any) int` | `Int64(any) int64`
|
* `To[T any](any) (T, error)` —— 万能转换入口。支持基础类型、Slice、Map 以及 JSON 的双向自动转换。
|
||||||
* `Uint(any) uint` | `Uint64(any) uint64`
|
* `As[T any](any) T` —— 零摩擦转换。失败时返回类型零值。
|
||||||
* `Float(any) float32` | `Float64(any) float64`
|
|
||||||
* `String(any) string` | `Bool(any) bool`
|
|
||||||
* `Duration(any) time.Duration`
|
|
||||||
|
|
||||||
2. **高级转化(Map/Slice Helpers)**
|
2. **Map / Slice 转换**
|
||||||
* `ToMap(target any, source any)` —— 将 source 填充到目标 map。支持 struct (递归拍平且导出字段转小写)、slice (KV 序列) 或 map (合并)。
|
* `ToMap[K, V](any) (map[K]V, error)` —— 构建新 Map。
|
||||||
* `ToSlice(target any, source any)` —— 将 source 填充到目标 slice。支持 map (KV 序列)、slice (追加) 或普通值 (追加)。
|
* `AsMap[K, V](any) map[K]V` —— 构建新 Map (失败返回空 Map)。
|
||||||
* `MakeMap[K comparable, V any](source any) map[K]V` —— 泛型构建并填充新 Map。
|
* `ToSlice[T](any) ([]T, error)` —— 构建新 Slice。
|
||||||
* `MakeSlice[T any](source any) []T` —— 泛型构建并填充新 Slice。
|
* `AsSlice[T](any) []T` —— 构建新 Slice (失败返回空 Slice)。
|
||||||
|
* `FillMap(target, source)` | `FillSlice(target, source)` —— 填充现有容器。
|
||||||
|
|
||||||
3. **序列化与泛型构建(JSON & YAML)**
|
3. **JSON 序列化与构建**
|
||||||
* **JSON 编码**: `ToJSON(any)(string, error)` | `ToJSONDesensitize(any, []string)(string, error)` | `MustToJSON(any)string`
|
* **编码**: `ToJSON(any) (string, error)` | `AsJSON(any) string`
|
||||||
* **JSON 解码 (原地)**: `UnmarshalJSON(data, any) error` (支持 string 或为了内存安全推荐使用的 []byte)
|
* **解码**: `FromJSON[T](any) (T, error)` | `AsFromJSON[T](any) T`
|
||||||
* **JSON 构建 (泛型)**: `FromJSON[T](data) (T, error)` | `MustFromJSON[T](data) T`
|
* **进阶**: `ToJSONDesensitize(any, []string) (string, error)` (支持字段脱敏)
|
||||||
* **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)**
|
4. **泛型工具**
|
||||||
* `If[T any](bool, T, T) T` —— 三元逻辑
|
* `If[T any](bool, T, T) T` —— 三元逻辑。
|
||||||
* `In[T comparable]([]T, T) bool` —— 包含判断
|
* `In[T comparable]([]T, T) bool` —— 包含判断。
|
||||||
* `Ptr[T any](T) *T` —— 取指针
|
* `Ptr[T any](T) *T` —— 快速取指针。
|
||||||
* `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速索引化
|
* `ArrayToBoolMap[T comparable]([]T) map[T]bool` —— 快速构建索引 Map。
|
||||||
|
|
||||||
5. **辅助工具(Utilities)**
|
|
||||||
* **切分**: `Split(s, sep) []string` | `SplitArgs(s) []string`
|
|
||||||
* **拼接**: `UniqueAppend([]string, ...any) []string` | `JoinArgs([]string, sep) string`
|
|
||||||
* **其他**: `RealValue(reflect.Value) reflect.Value` | `GetLowerName(string) string` | `GetUpperName(string) string` | `FixUpperCase([]byte, []string)`
|
|
||||||
|
|
||||||
## 🧪 验证状态
|
## 🧪 验证状态
|
||||||
测试全部通过,性能达标。详见:[TEST.md](./TEST.md)
|
测试全部通过,性能达标。详见:[TEST.md](./TEST.md)
|
||||||
|
|||||||
17
TEST.md
17
TEST.md
@ -1,15 +1,16 @@
|
|||||||
# 测试报告 (Test Report)
|
# 测试报告 (Test Report)
|
||||||
|
|
||||||
## 覆盖场景 (Coverage Scenarios)
|
## 覆盖场景 (Coverage Scenarios)
|
||||||
|
- **语义化转换**: `To[T]` 和 `As[T]` 覆盖基础类型、Slice、Map 及智能 JSON 自动穿透转换。
|
||||||
- **核心类型转换**: `Int64`, `Uint64`, `Float64`, `Bool`, `String`,包括边界值、零值及非法字符串输入。
|
- **核心类型转换**: `Int64`, `Uint64`, `Float64`, `Bool`, `String`,包括边界值、零值及非法字符串输入。
|
||||||
- **复合类型处理**: `ToMap`, `ToSlice`, `MakeMap`, `MakeSlice` 等全能转化工具,支持 Struct 拍平。
|
- **容器处理**: `ToMap`, `AsMap`, `ToSlice`, `AsSlice` 等泛型工具,支持 Struct 拍平与 KV 序列化。
|
||||||
- **JSON/YAML 序列化**:
|
- **JSON 序列化**:
|
||||||
- 深度结构体映射,支持 `FastEncoder` 单路径处理。
|
- 深度结构体映射,支持 `FastEncoder` 单路径处理。
|
||||||
- **去标签化算法**: 自动识别 `UserID` -> `userID` 等符合工程习惯的转换。
|
- **去标签化算法**: 自动识别 `UserID` -> `userID` 等符合工程习惯的转换。
|
||||||
- **脱敏支持**: `ToJSONDesensitize` 在编码阶段原生支持字段脱敏。
|
- **脱敏支持**: `ToJSONDesensitize` 在编码阶段原生支持字段脱敏。
|
||||||
- **Map 兼容性**: 原生支持 `map[any]any` 及 Goja 伪数组转换。
|
- **Map 兼容性**: 原生支持 `map[any]any` 及 Goja 伪数组转换。
|
||||||
- **JSON/YAML 构建**:
|
- **JSON 构建**:
|
||||||
- **Make 系列**: 实现泛型零摩擦构建新对象。
|
- **As 系列**: 实现泛型零摩擦构建新对象。
|
||||||
- **FastDecoder**: 实现单路径流式解析,跳过中间 Map 分配。
|
- **FastDecoder**: 实现单路径流式解析,跳过中间 Map 分配。
|
||||||
- **Frictionless 匹配**: 支持大小写不敏感、忽略下划线等灵活的 Key 映射规则。
|
- **Frictionless 匹配**: 支持大小写不敏感、忽略下划线等灵活的 Key 映射规则。
|
||||||
- **智能初始化**: 自动处理嵌套指针、Slice 和 Map 的初始化。
|
- **智能初始化**: 自动处理嵌套指针、Slice 和 Map 的初始化。
|
||||||
@ -19,9 +20,9 @@
|
|||||||
## 性能基准 (Benchmark Results - Intel(R) Core(TM) i9)
|
## 性能基准 (Benchmark Results - Intel(R) Core(TM) i9)
|
||||||
- `If`: ~0.24 ns/op
|
- `If`: ~0.24 ns/op
|
||||||
- `Int64`: ~20 ns/op
|
- `Int64`: ~20 ns/op
|
||||||
- `ToMap`: ~700-800 ns/op (含 Struct 拍平与类型转换)
|
- `ToMap`: ~725 ns/op (含 Struct 拍平与类型转换)
|
||||||
- `ToSlice`: ~1200-1400 ns/op (含 KV 展开与类型转换)
|
- `ToSlice`: ~1700 ns/op (含 Map Key 排序稳定化处理)
|
||||||
- `ToJSON (SimpleStruct)`: ~440 ns/op (相比旧版提升 ~30%)
|
- `ToJSON (SimpleStruct)`: ~450 ns/op
|
||||||
- `ToJSON (DirtyMap)`: ~1100 ns/op (相比旧版提升 ~70%)
|
- `ToJSON (DirtyMap)`: ~1100-1200 ns/op
|
||||||
- `UnmarshalJSON`: 高性能单路径解析,显著降低内存分配。
|
- `UnmarshalJSON`: 高性能单路径解析,显著降低内存分配。
|
||||||
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
- `UniqueAppend`: 大数据量下的 $O(n)$ 时间复杂度。
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func BenchmarkToMap(b *testing.B) {
|
|||||||
kv := []any{"a", 1, "b", 2, "c", 3, "d", 4, "e", 5}
|
kv := []any{"a", 1, "b", 2, "c", 3, "d", 4, "e", 5}
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
cast.ToMap(m, kv)
|
cast.FillMap(m, kv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ func BenchmarkToSlice(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
s = s[:0]
|
s = s[:0]
|
||||||
cast.ToSlice(&s, m)
|
cast.FillSlice(&s, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
239
cast.go
239
cast.go
@ -6,11 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// If 泛型三元表达式
|
// If 泛型三元表达式
|
||||||
@ -35,9 +34,88 @@ func RealValue(v reflect.Value) reflect.Value {
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Core Cast Logic ---
|
// --- Core Cast Logic ---
|
||||||
|
|
||||||
|
// To 泛型转换 (支持基础类型、Slice、Map 及 JSON 自动转换)
|
||||||
|
func To[T any](v any) (T, error) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, err := ToJSON(v)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
return any(reflectCast(s, targetType).Interface()).(T), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理 Slice/Map
|
||||||
|
if targetType.Kind() == reflect.Slice && targetType.Elem().Kind() != reflect.Uint8 {
|
||||||
|
sv := reflect.MakeSlice(targetType, 0, 0)
|
||||||
|
ptr := reflect.New(targetType)
|
||||||
|
ptr.Elem().Set(sv)
|
||||||
|
fillToSlice(ptr.Elem(), v)
|
||||||
|
return ptr.Elem().Interface().(T), nil
|
||||||
|
}
|
||||||
|
if targetType.Kind() == reflect.Map {
|
||||||
|
mv := reflect.MakeMap(targetType)
|
||||||
|
fillToMap(mv, v)
|
||||||
|
return mv.Interface().(T), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 处理基础类型
|
||||||
|
res := reflectCast(v, targetType)
|
||||||
|
if !res.IsValid() {
|
||||||
|
return zero, fmt.Errorf("cast to %v failed", targetType)
|
||||||
|
}
|
||||||
|
return res.Interface().(T), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// As 泛型转换 (失败返回零值)
|
||||||
|
func As[T any](v any) T {
|
||||||
|
res, _ := To[T](v)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONText(v any) bool {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
s := strings.TrimSpace(val)
|
||||||
|
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[")
|
||||||
|
case []byte:
|
||||||
|
s := bytes.TrimSpace(val)
|
||||||
|
return bytes.HasPrefix(s, []byte("{")) || bytes.HasPrefix(s, []byte("["))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isComplexType(t reflect.Type) bool {
|
||||||
|
kind := t.Kind()
|
||||||
|
for kind == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
kind = t.Kind()
|
||||||
|
}
|
||||||
|
return kind == reflect.Struct || kind == reflect.Map || (kind == reflect.Slice && t.Elem().Kind() != reflect.Uint8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isComplexValue(v any) bool {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rv := RealValue(reflect.ValueOf(v))
|
||||||
|
if !rv.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
kind := rv.Kind()
|
||||||
|
return kind == reflect.Struct || kind == reflect.Map || (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8)
|
||||||
|
}
|
||||||
|
|
||||||
func parseInt(s string) int64 {
|
func parseInt(s string) int64 {
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -258,6 +336,21 @@ func Duration(value any) time.Duration {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AsJSONBytes(value any) []byte {
|
||||||
|
j, _ := ToJSONBytes(value)
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func AsJSON(value any) string {
|
||||||
|
s, _ := ToJSON(value)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func AsFromJSON[T any](data any) T {
|
||||||
|
v, _ := FromJSON[T](data)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func ToJSONBytes(value any) ([]byte, error) {
|
func ToJSONBytes(value any) ([]byte, error) {
|
||||||
return fastToJSONBytes(value)
|
return fastToJSONBytes(value)
|
||||||
}
|
}
|
||||||
@ -274,13 +367,7 @@ func ToJSONDesensitizeBytes(value any, keys []string) ([]byte, error) {
|
|||||||
return fastToJSONBytes(value, keys...)
|
return fastToJSONBytes(value, keys...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustToJSONBytes(value any) []byte {
|
func MustToJSONBytes(value any) []byte { return AsJSONBytes(value) }
|
||||||
j, err := ToJSONBytes(value)
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToJSON(value any) (string, error) {
|
func ToJSON(value any) (string, error) {
|
||||||
j, err := ToJSONBytes(value)
|
j, err := ToJSONBytes(value)
|
||||||
@ -290,13 +377,10 @@ func ToJSON(value any) (string, error) {
|
|||||||
return string(j), nil
|
return string(j), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustToJSON(value any) string {
|
func MustToJSON(value any) string { return AsJSON(value) }
|
||||||
s, _ := ToJSON(value)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrettyToJSONBytes(value any) []byte {
|
func PrettyToJSONBytes(value any) []byte {
|
||||||
j := MustToJSONBytes(value)
|
j := AsJSONBytes(value)
|
||||||
r := &bytes.Buffer{}
|
r := &bytes.Buffer{}
|
||||||
if err := json.Indent(r, j, "", " "); err == nil {
|
if err := json.Indent(r, j, "", " "); err == nil {
|
||||||
return r.Bytes()
|
return r.Bytes()
|
||||||
@ -333,58 +417,7 @@ func FromJSON[T any](data any) (T, error) {
|
|||||||
return v, err
|
return v, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustFromJSON[T any](data any) T {
|
func MustFromJSON[T any](data any) T { return AsFromJSON[T](data) }
|
||||||
v, _ := FromJSON[T](data)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToYAML(value any) (string, error) {
|
|
||||||
j, err := YAMLBytes(value)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(j), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustToYAML(value any) string {
|
|
||||||
s, _ := ToYAML(value)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalYAML(data any, value any) error {
|
|
||||||
b := toBytes(data)
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("nil data")
|
|
||||||
}
|
|
||||||
return yaml.Unmarshal(b, 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, _ := FromYAML[T](data)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func YAMLBytes(value any) ([]byte, error) {
|
|
||||||
j, err := yaml.Marshal(value)
|
|
||||||
if err == nil {
|
|
||||||
return j, nil
|
|
||||||
}
|
|
||||||
return []byte(fmt.Sprint(value)), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustYAMLBytes(value any) []byte {
|
|
||||||
j, err := YAMLBytes(value)
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Others (Keep logic but clean style) ---
|
// --- Others (Keep logic but clean style) ---
|
||||||
|
|
||||||
@ -483,11 +516,34 @@ func ArrayToBoolMap[T comparable](arr []T) map[T]bool {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap 将 source 填充到目标 map 中,自动转换类型。
|
// ToMap 泛型构建新 Map
|
||||||
// 如果 source 是 struct,则提取导出字段(含嵌入字段,字段名首字母转小写);
|
func ToMap[K comparable, V any](source any) (map[K]V, error) {
|
||||||
// 如果 source 是 slice/array,则视为 [K1, V1, K2, V2, ...] 序列;
|
m := make(map[K]V)
|
||||||
// 如果 source 是 map,则进行合并(Merge)。
|
fillToMap(reflect.ValueOf(m), source)
|
||||||
func ToMap(target any, source any) {
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsMap 泛型构建新 Map (失败返回空 Map)
|
||||||
|
func AsMap[K comparable, V any](source any) map[K]V {
|
||||||
|
m, _ := ToMap[K, V](source)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlice 泛型构建新 Slice
|
||||||
|
func ToSlice[T any](source any) ([]T, error) {
|
||||||
|
var s []T
|
||||||
|
fillToSlice(reflect.ValueOf(&s).Elem(), source)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsSlice 泛型构建新 Slice (失败返回空 Slice)
|
||||||
|
func AsSlice[T any](source any) []T {
|
||||||
|
s, _ := ToSlice[T](source)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillMap 将 source 填充到目标 map 中 (兼容旧 API 逻辑)
|
||||||
|
func FillMap(target any, source any) {
|
||||||
rv := reflect.ValueOf(target)
|
rv := reflect.ValueOf(target)
|
||||||
for rv.Kind() == reflect.Pointer {
|
for rv.Kind() == reflect.Pointer {
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
@ -515,7 +571,10 @@ func ToMap(target any, source any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fillToMap(rv, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillToMap(rv reflect.Value, source any) {
|
||||||
kt := rv.Type().Key()
|
kt := rv.Type().Key()
|
||||||
vt := rv.Type().Elem()
|
vt := rv.Type().Elem()
|
||||||
|
|
||||||
@ -555,25 +614,27 @@ func fillMapFromStruct(targetMap, sv reflect.Value, kt, vt reflect.Type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSlice 将 source 填充到目标 slice 中,自动转换类型。
|
// FillSlice 将 source 填充到目标 slice 中 (兼容旧 API 逻辑)
|
||||||
// 如果 source 是 map,则展开为 [K1, V1, K2, V2, ...] 序列;
|
func FillSlice(target any, source any) {
|
||||||
// 如果 source 是 slice/array,则进行元素拷贝/追加;
|
|
||||||
// 如果 source 是普通值,则直接作为元素追加。
|
|
||||||
func ToSlice(target any, source any) {
|
|
||||||
rv := reflect.ValueOf(target)
|
rv := reflect.ValueOf(target)
|
||||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sliceRv := rv.Elem()
|
fillToSlice(rv.Elem(), source)
|
||||||
et := sliceRv.Type().Elem()
|
}
|
||||||
|
|
||||||
|
func fillToSlice(sliceRv reflect.Value, source any) {
|
||||||
|
et := sliceRv.Type().Elem()
|
||||||
sv := RealValue(reflect.ValueOf(source))
|
sv := RealValue(reflect.ValueOf(source))
|
||||||
switch sv.Kind() {
|
switch sv.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
iter := sv.MapRange()
|
keys := sv.MapKeys()
|
||||||
for iter.Next() {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(iter.Key().Interface(), et)))
|
return String(keys[i].Interface()) < String(keys[j].Interface())
|
||||||
sliceRv.Set(reflect.Append(sliceRv, reflectCast(iter.Value().Interface(), et)))
|
})
|
||||||
|
for _, key := range keys {
|
||||||
|
sliceRv.Set(reflect.Append(sliceRv, reflectCast(key.Interface(), et)))
|
||||||
|
sliceRv.Set(reflect.Append(sliceRv, reflectCast(sv.MapIndex(key).Interface(), et)))
|
||||||
}
|
}
|
||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
for i := 0; i < sv.Len(); i++ {
|
for i := 0; i < sv.Len(); i++ {
|
||||||
@ -586,18 +647,14 @@ func ToSlice(target any, source any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeMap 泛型构建新 Map
|
// MakeMap 泛型构建新 Map (DEPRECATED: Use AsMap)
|
||||||
func MakeMap[K comparable, V any](source any) map[K]V {
|
func MakeMap[K comparable, V any](source any) map[K]V {
|
||||||
m := make(map[K]V)
|
return AsMap[K, V](source)
|
||||||
ToMap(m, source)
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeSlice 泛型构建新 Slice
|
// MakeSlice 泛型构建新 Slice (DEPRECATED: Use AsSlice)
|
||||||
func MakeSlice[T any](source any) []T {
|
func MakeSlice[T any](source any) []T {
|
||||||
var s []T
|
return AsSlice[T](source)
|
||||||
ToSlice(&s, source)
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reflectCast(value any, t reflect.Type) reflect.Value {
|
func reflectCast(value any, t reflect.Type) reflect.Value {
|
||||||
|
|||||||
11
cast_test.go
11
cast_test.go
@ -52,17 +52,6 @@ func TestJSONToStruct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestYAMLConversion(t *testing.T) {
|
|
||||||
type Config struct {
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
c := Config{Port: 8080}
|
|
||||||
yamlStr := cast.MustToYAML(c)
|
|
||||||
if !strings.Contains(yamlStr, "port: 8080") {
|
|
||||||
t.Errorf("YAML conversion failed: %s", yamlStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSpecialJSON(t *testing.T) {
|
func TestSpecialJSON(t *testing.T) {
|
||||||
// 关键测试:特殊 HTML 字符序列化不应被转义
|
// 关键测试:特殊 HTML 字符序列化不应被转义
|
||||||
type Content struct {
|
type Content struct {
|
||||||
|
|||||||
@ -17,13 +17,13 @@ func TestToMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
u := User{Base: Base{ID: 1}, Name: "Tom"}
|
u := User{Base: Base{ID: 1}, Name: "Tom"}
|
||||||
m1 := make(map[string]any)
|
m1 := make(map[string]any)
|
||||||
cast.ToMap(m1, u)
|
cast.FillMap(m1, u)
|
||||||
if m1["ID"] != 1 || m1["name"] != "Tom" {
|
if m1["ID"] != 1 || m1["name"] != "Tom" {
|
||||||
t.Errorf("Struct inheritance to map failed: %v", m1)
|
t.Errorf("Struct inheritance to map failed: %v", m1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test slice-to-map (KV)
|
// Test slice-to-map (KV)
|
||||||
m2 := cast.MakeMap[int, string]([]any{"1", "a", 2, 200})
|
m2 := cast.AsMap[int, string]([]any{"1", "a", 2, 200})
|
||||||
if m2[1] != "a" || m2[2] != "200" {
|
if m2[1] != "a" || m2[2] != "200" {
|
||||||
t.Errorf("Slice-to-map failed: %v", m2)
|
t.Errorf("Slice-to-map failed: %v", m2)
|
||||||
}
|
}
|
||||||
@ -32,21 +32,21 @@ func TestToMap(t *testing.T) {
|
|||||||
func TestToSlice(t *testing.T) {
|
func TestToSlice(t *testing.T) {
|
||||||
// Test primitive-to-slice (append)
|
// Test primitive-to-slice (append)
|
||||||
var s1 []int
|
var s1 []int
|
||||||
cast.ToSlice(&s1, "123")
|
cast.FillSlice(&s1, "123")
|
||||||
if len(s1) != 1 || s1[0] != 123 {
|
if len(s1) != 1 || s1[0] != 123 {
|
||||||
t.Errorf("Primitive-to-slice failed: %v", s1)
|
t.Errorf("Primitive-to-slice failed: %v", s1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test append slice
|
// Test append slice
|
||||||
cast.ToSlice(&s1, []string{"456"})
|
cast.FillSlice(&s1, []string{"456"})
|
||||||
if len(s1) != 2 || s1[1] != 456 {
|
if len(s1) != 2 || s1[1] != 456 {
|
||||||
t.Errorf("Append slice to slice failed: %v", s1)
|
t.Errorf("Append slice to slice failed: %v", s1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONYAML(t *testing.T) {
|
func TestJSON(t *testing.T) {
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int `json:"port" yaml:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
data := `{"port": 8080}`
|
data := `{"port": 8080}`
|
||||||
|
|
||||||
@ -63,29 +63,22 @@ func TestJSONYAML(t *testing.T) {
|
|||||||
t.Errorf("FromJSON failed: %v, %v", err, c2)
|
t.Errorf("FromJSON failed: %v, %v", err, c2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test MustFromJSON
|
// Test AsFromJSON
|
||||||
c3 := cast.MustFromJSON[Config](data)
|
c3 := cast.AsFromJSON[Config](data)
|
||||||
if c3.Port != 8080 {
|
if c3.Port != 8080 {
|
||||||
t.Errorf("MustFromJSON failed: %v", c3)
|
t.Errorf("AsFromJSON 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) {
|
func TestMakeSlice(t *testing.T) {
|
||||||
// Test MakeSlice with various inputs
|
// Test MakeSlice with various inputs
|
||||||
s1 := cast.MakeSlice[int]("123")
|
s1 := cast.AsSlice[int]("123")
|
||||||
if len(s1) != 1 || s1[0] != 123 {
|
if len(s1) != 1 || s1[0] != 123 {
|
||||||
t.Errorf("MakeSlice primitive failed: %v", s1)
|
t.Errorf("AsSlice primitive failed: %v", s1)
|
||||||
}
|
}
|
||||||
|
|
||||||
s2 := cast.MakeSlice[string]([]int{1, 2})
|
s2 := cast.AsSlice[string]([]int{1, 2})
|
||||||
if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" {
|
if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" {
|
||||||
t.Errorf("MakeSlice slice failed: %v", s2)
|
t.Errorf("AsSlice slice failed: %v", s2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,5 +1,3 @@
|
|||||||
module apigo.cc/go/cast
|
module apigo.cc/go/cast
|
||||||
|
|
||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
|
||||||
|
|||||||
73
semantic_test.go
Normal file
73
semantic_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package cast_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToAs(t *testing.T) {
|
||||||
|
// Basic types
|
||||||
|
if v, err := cast.To[int]("123"); err != nil || v != 123 {
|
||||||
|
t.Errorf("To[int] failed: %v, %v", v, err)
|
||||||
|
}
|
||||||
|
if v := cast.As[int]("123"); v != 123 {
|
||||||
|
t.Errorf("As[int] failed: %v", v)
|
||||||
|
}
|
||||||
|
if v := cast.As[int]("abc"); v != 0 {
|
||||||
|
t.Errorf("As[int] for invalid input should be 0, got %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice
|
||||||
|
s, err := cast.To[[]int]([]string{"1", "2", "3"})
|
||||||
|
if err != nil || len(s) != 3 || s[0] != 1 || s[1] != 2 || s[2] != 3 {
|
||||||
|
t.Errorf("To[[]int] failed: %v, %v", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map
|
||||||
|
m, err := cast.To[map[string]int](map[string]string{"a": "1", "b": "2"})
|
||||||
|
if err != nil || len(m) != 2 || m["a"] != 1 || m["b"] != 2 {
|
||||||
|
t.Errorf("To[map[string]int] failed: %v, %v", m, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Auto conversion (Struct to String)
|
||||||
|
type User struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
u := User{Name: "Alice"}
|
||||||
|
js, err := cast.To[string](u)
|
||||||
|
if err != nil || js != `{"name":"Alice"}` {
|
||||||
|
t.Errorf("To[string] for struct failed: %v, %v", js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Auto conversion (String to Struct)
|
||||||
|
u2, err := cast.To[User](`{"name":"Bob"}`)
|
||||||
|
if err != nil || u2.Name != "Bob" {
|
||||||
|
t.Errorf("To[User] from string failed: %v, %v", u2, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapSliceAPIs(t *testing.T) {
|
||||||
|
// ToMap
|
||||||
|
m, err := cast.ToMap[string, int]([]any{"a", "1", "b", 2})
|
||||||
|
if err != nil || m["a"] != 1 || m["b"] != 2 {
|
||||||
|
t.Errorf("ToMap failed: %v, %v", m, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsMap
|
||||||
|
m2 := cast.AsMap[string, int]([]any{"c", "3"})
|
||||||
|
if m2["c"] != 3 {
|
||||||
|
t.Errorf("AsMap failed: %v", m2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlice
|
||||||
|
s, err := cast.ToSlice[int]([]string{"10", "20"})
|
||||||
|
if err != nil || len(s) != 2 || s[0] != 10 || s[1] != 20 {
|
||||||
|
t.Errorf("ToSlice failed: %v, %v", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsSlice
|
||||||
|
s2 := cast.AsSlice[string]([]int{1, 2})
|
||||||
|
if len(s2) != 2 || s2[0] != "1" || s2[1] != "2" {
|
||||||
|
t.Errorf("AsSlice failed: %v", s2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user