Compare commits
No commits in common. "main" and "v1.0.3" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
go.sum
|
|
||||||
33
AI.md
Normal file
33
AI.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# AI Coding Context: @go/convert
|
||||||
|
|
||||||
|
本索引供 AI 模型理解 `@go/convert` 的逻辑,以生成符合本项目“意图优先、零摩擦”哲学的代码。
|
||||||
|
|
||||||
|
## 🤖 AI 行为准则
|
||||||
|
|
||||||
|
1. **优先使用 convert.To 进行对象映射**:在需要进行结构体拷贝、Map 转 Struct 或复杂嵌套转换时,直接调用 `convert.To(from, &to)`。
|
||||||
|
2. **理解“解包”与“包装”逻辑**:生成代码时可以放心将单值传给切片目标,或将切片传给单值目标。
|
||||||
|
3. **利用键名容忍度**:在处理不可控的外部数据时,无需担心键名格式(驼峰、蛇形等),`convert.To` 会自动匹配。
|
||||||
|
4. **无视指针层级**:在编写调用代码时,无需手动解引用或取地址以匹配类型,`convert` 内部会自动穿透处理。
|
||||||
|
|
||||||
|
## 🛠 API Reference
|
||||||
|
|
||||||
|
### 核心转换函数
|
||||||
|
- `func To(from, to any)`:支持任意可能的类型深度转换,`to` 必须为指针。
|
||||||
|
|
||||||
|
### 结构体分析
|
||||||
|
- `func FlatStruct(data any) *StructInfo`:平展结构体(仅导出字段/方法)。
|
||||||
|
- `func FlatStructWithUnexported(data any) *StructInfo`:平展结构体(包含未导出字段/方法)。
|
||||||
|
|
||||||
|
## 🧩 典型模式 (Best Practices)
|
||||||
|
|
||||||
|
* **✅ 推荐 (@go/convert)**:
|
||||||
|
```go
|
||||||
|
// 自动匹配任何格式的键名
|
||||||
|
convert.To(from, &u)
|
||||||
|
```
|
||||||
|
|
||||||
|
* **✅ 推荐 (@go/convert)**:
|
||||||
|
```go
|
||||||
|
// 自动包装单值至切片
|
||||||
|
convert.To(src, &dest)
|
||||||
|
```
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,18 +1,5 @@
|
|||||||
# Changelog: @go/convert
|
# Changelog: @go/convert
|
||||||
|
|
||||||
## [v1.0.5] - 2026-05-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **兼容性修复**:修复了由于 `cast` 库 API 变更导致的 `MustJSONBytes` 未定义错误,更新为 `MustToJSONBytes`。
|
|
||||||
|
|
||||||
## [v1.0.4] - 2026-04-30
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **性能重构**:重构了反射字段匹配逻辑,引入 `sync.Map` 缓存结构体元数据,性能从 $O(N)$ 优化至 $O(1)$,Benchmark 结果约为 `1020 ns/op`。
|
|
||||||
- **匹配策略优化**:实现了“分层模糊匹配”,确保在保留“零摩擦”模糊匹配哲学的同时,适配更多不规范命名风格(如下划线、中划线、大小写混用)。
|
|
||||||
- **防御性强化**:重构了 `To` 函数中的指针解引用与初始化逻辑,移除了由于 `nil` 指针或非法嵌套引起的 Panic 风险,支持多级指针自动分配内存。
|
|
||||||
- **辅助函数优化**:将 `FlattenStruct` 的内部递归逻辑重构为高性能 Visitor 模式,增加了深度递归防护。
|
|
||||||
|
|
||||||
## [v1.0.0] - 2026-04-22
|
## [v1.0.0] - 2026-04-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
71
README.md
71
README.md
@ -1,29 +1,60 @@
|
|||||||
|
# 关于本项目
|
||||||
|
|
||||||
|
本项目完全由 AI 维护。代码源自 github.com/ssgo/u 的重构。
|
||||||
|
|
||||||
# @go/convert
|
# @go/convert
|
||||||
|
|
||||||
基于“意图优先”哲学的零摩擦数据转换库。
|
`@go/convert` 是一个为“零摩擦”数据映射设计的深度转换库。它的核心哲学是**意图优先**:通过目标对象的类型推断用户的需求,并尽力抹平输入数据与目标形状之间的鸿沟。
|
||||||
|
|
||||||
> **AI 维护声明**: 本仓库代码逻辑已通过高并发性能测试与边界防御审查。由 Gemini CLI AI 协同维护。请勿手动修改核心算法逻辑,如有变更需求请通过 AI 工具链进行。
|
## 🎯 设计哲学
|
||||||
|
|
||||||
## 特性
|
* **消除类型摩擦**:在业务代码中,我们经常遇到单值与切片、字符串与结构体之间的转换。`convert` 能够自动处理这些“形状”差异。
|
||||||
- **零摩擦映射**:基于目标类型的深度转换,自动消除类型与格式鸿沟。
|
* **极致容忍 Key 名**:忽略所有非字母数字字符及大小写,确保不同来源的数据都能精准映射。
|
||||||
- **高性能映射**:内置高性能字段索引缓存,满足生产级吞吐要求。
|
* **指针透明化**:自动处理深层指针的穿透与分配。
|
||||||
- **防 Panic 设计**:内置完善的指针解引用与非法内存操作防御。
|
* **可定制转换**:通过方法钩子实现特定的解析逻辑。
|
||||||
|
|
||||||
|
## 🛠 API Reference
|
||||||
|
|
||||||
|
### 核心函数
|
||||||
|
|
||||||
|
#### `func To(from, to any)`
|
||||||
|
将 `from` 中的数据深度映射到 `to` 中。`to` 必须是一个**指针**类型。
|
||||||
|
- **支持类型**:基础类型互转、Struct 互转、Map 转 Struct、Struct 转 Map、Slice 互转等。
|
||||||
|
- **去摩擦特性**:支持单值与切片的互转(包装/解包)、CSV 字符串转切片。
|
||||||
|
|
||||||
|
#### `func Convert(from, to any)`
|
||||||
|
`To` 的别名,用于保持向前兼容。
|
||||||
|
|
||||||
|
### 结构体分析
|
||||||
|
|
||||||
|
#### `func FlatStruct(data any) *StructInfo`
|
||||||
|
平展结构体。返回导出字段、导出方法及其对应的 `reflect.Value` 映射。
|
||||||
|
|
||||||
|
#### `func FlatStructWithUnexported(data any) *StructInfo`
|
||||||
|
平展结构体,包含未导出的字段和方法。
|
||||||
|
|
||||||
|
### 定制转换钩子
|
||||||
|
|
||||||
|
如果目标结构体定义了 `func (p *T) ParseXxx(v any) FieldType` 方法(其中 `Xxx` 为字段名),`convert` 将优先调用该方法来决定字段的值。
|
||||||
|
|
||||||
|
## 📦 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get apigo.cc/go/convert
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 快速开始
|
||||||
|
|
||||||
## 快速使用
|
|
||||||
```go
|
```go
|
||||||
import "apigo.cc/go/convert"
|
import "apigo.cc/go/convert"
|
||||||
|
|
||||||
// 使用 To 方法进行转换
|
// 1. 模糊键名匹配
|
||||||
convert.To(source, &destination)
|
from := map[string]any{"user-id": 1001}
|
||||||
|
var u struct { UserID int }
|
||||||
|
convert.To(from, &u) // u.UserID = 1001
|
||||||
|
|
||||||
|
// 2. 切片自动解包
|
||||||
|
nums := []int{100, 200}
|
||||||
|
var n int
|
||||||
|
convert.To(nums, &n) // n = 100
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 指南
|
|
||||||
|
|
||||||
- **`func To(source any, destination any)`**
|
|
||||||
深度映射与数据转换的核心入口。它会根据 `destination` 的类型推断意图,将 `source` 的数据转换为目标类型并赋值。`destination` 必须为可写的指针。
|
|
||||||
|
|
||||||
- **`func FlatStruct(data any) *StructInfo`**
|
|
||||||
解析结构体元数据,返回包含所有导出字段定义、字段值映射及方法集的 `StructInfo`。
|
|
||||||
|
|
||||||
- **`func FlatStructWithUnexported(data any) *StructInfo`**
|
|
||||||
与 `FlatStruct` 类似,但包含结构体中未导出的(私有)字段。
|
|
||||||
|
|||||||
35
TEST.md
35
TEST.md
@ -1,17 +1,24 @@
|
|||||||
# 测试说明 (@go/convert)
|
# Test Report: @go/convert
|
||||||
|
|
||||||
本模块采用基于类型映射的测试策略,涵盖了从基础类型转换到高并发下结构体映射的全面场景。
|
## 📋 测试概览
|
||||||
|
- **测试时间**: 2026-04-22
|
||||||
|
- **测试环境**: darwin/amd64 (Intel i9-9980HK)
|
||||||
|
- **Go 版本**: 1.25.0
|
||||||
|
|
||||||
## 覆盖场景
|
## ✅ 功能测试 (Functional Tests)
|
||||||
1. **类型矩阵测试**:覆盖 int, string, float, bool 等基础类型在不同容器间的互转。
|
| 场景 | 状态 | 描述 |
|
||||||
2. **零摩擦映射**:测试单值与切片的自动包装与解包、CSV/JSON 字符串到切片的转换。
|
| :--- | :--- | :--- |
|
||||||
3. **复杂指针处理**:验证多级指针到值的深度穿透及自动初始化。
|
| `TestConvertMatrix` | PASS | 包含 13 个子测试,涵盖基础全互转、自动包装/解包、CSV/JSON 切分、深层指针穿透。 |
|
||||||
4. **模糊匹配逻辑**:涵盖各种不规范命名风格(下划线、大小写混用、特殊符号)到 Struct 字段的成功匹配。
|
| `TestComplexFrictionlessMapping` | PASS | 验证 Map 到 Struct 的极致去摩擦映射(忽略大小写、忽略下划线/中划线、类型自动纠正)。 |
|
||||||
5. **函数代理**:验证 Func to Func 映射的准确性。
|
| `TestFuncConversion` | PASS | 验证函数类型的动态映射与参数/返回值自动转换。 |
|
||||||
6. **扩展钩子**:验证 `ParseXxx` 的自定义逻辑挂载。
|
| `TestParseHook` | PASS | 验证 `ParseXxx` 定制化转换钩子。 |
|
||||||
7. **防御性编程**:模拟 `nil` 指针解引用、只读目标地址等非法调用,确保不发生 Panic。
|
|
||||||
|
|
||||||
## 性能表现
|
## 🛡️ 鲁棒性防御 (Robustness)
|
||||||
基准测试 (`BenchmarkMatrixConvert`) 在典型的 Struct 映射场景下表现为:
|
- **单值与切片自动适配**:输入 `[]int{200}` 到 `int` 目标,自动取首值;输入 `100` 到 `[]int` 目标,自动包装。
|
||||||
- 平均耗时:**~1020 ns/op**
|
- **极致 Key 容忍**:键名 `user_id`、`USER-NAME` 等均能精准匹配。
|
||||||
- 吞吐量:高并发环境下 O(1) 查找速度,无锁热点竞争。
|
- **零值安全性**:转换失败或输入 nil 时,目标被重置为干净的零值,不保留脏数据。
|
||||||
|
|
||||||
|
## ⚡ 性能基准 (Benchmarks)
|
||||||
|
| 函数 | 平均耗时 | 性能分析 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `MatrixConvert` | **1226 ns/op** | 包含反射解析、Map 键名归一化及嵌套映射,性能处于工业级水准。 |
|
||||||
|
|||||||
621
convert.go
621
convert.go
@ -4,423 +4,353 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// To 将 source 中的数据深度映射到 destination 中。
|
// To 将 from 中的数据深度映射到 to 中。
|
||||||
// 核心哲学:意图优先。根据 destination 的类型推断用户的意图,并尽力转化数据。
|
// 核心哲学:意图优先。根据 to 的类型推断用户的意图,并尽力转化数据。
|
||||||
func To(source, destination any) {
|
func To(from, to any) {
|
||||||
if destination == nil {
|
r := convert(from, to)
|
||||||
return
|
if r != nil {
|
||||||
}
|
toValue := reflect.ValueOf(to)
|
||||||
convertedResult := performConversion(source, destination)
|
var prevValue reflect.Value
|
||||||
if convertedResult != nil {
|
for toValue.Kind() == reflect.Ptr {
|
||||||
destinationValue := reflect.ValueOf(destination)
|
prevValue = toValue
|
||||||
|
toValue = toValue.Elem()
|
||||||
// 循环解开指针直到目标值,在此过程中自动初始化 nil 指针
|
|
||||||
for destinationValue.Kind() == reflect.Ptr {
|
|
||||||
if destinationValue.IsNil() && destinationValue.CanSet() {
|
|
||||||
destinationValue.Set(reflect.New(destinationValue.Type().Elem()))
|
|
||||||
}
|
|
||||||
destinationValue = destinationValue.Elem()
|
|
||||||
}
|
}
|
||||||
|
if prevValue.IsValid() {
|
||||||
if destinationValue.CanSet() {
|
prevValue.Elem().Set(*r)
|
||||||
destinationValue.Set(*convertedResult)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func performConversion(source, destination any) *reflect.Value {
|
// Convert 是 To 的别名,保持向前兼容。
|
||||||
var sourceValue reflect.Value
|
func Convert(from, to any) { To(from, to) }
|
||||||
var destinationValue reflect.Value
|
|
||||||
|
|
||||||
if val, ok := source.(reflect.Value); ok {
|
func convert(from, to any) *reflect.Value {
|
||||||
sourceValue = val
|
var fromValue reflect.Value
|
||||||
|
var toValue reflect.Value
|
||||||
|
|
||||||
|
if v, ok := from.(reflect.Value); ok {
|
||||||
|
fromValue = v
|
||||||
} else {
|
} else {
|
||||||
sourceValue = reflect.ValueOf(source)
|
fromValue = reflect.ValueOf(from)
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, ok := destination.(reflect.Value); ok {
|
if v, ok := to.(reflect.Value); ok {
|
||||||
destinationValue = val
|
toValue = v
|
||||||
} else {
|
} else {
|
||||||
destinationValue = reflect.ValueOf(destination)
|
toValue = reflect.ValueOf(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 初始化目标容器
|
// 1. 初始化目标容器
|
||||||
ensureInitialized(destinationValue)
|
fixNilValue(toValue)
|
||||||
|
|
||||||
// 2. 获取底层业务数据
|
// 2. 获取底层业务数据
|
||||||
realSource := cast.RealValue(sourceValue)
|
rawFrom := cast.FinalValue(fromValue)
|
||||||
realDestination := cast.RealValue(destinationValue)
|
destValue := cast.RealValue(toValue)
|
||||||
|
|
||||||
if !realDestination.IsValid() {
|
if !destValue.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理 Unmarshaler 接口
|
// 3. 处理 Unmarshaler 接口
|
||||||
if realDestination.CanAddr() {
|
if destValue.CanAddr() {
|
||||||
address := realDestination.Addr().Interface()
|
addr := destValue.Addr().Interface()
|
||||||
if unmarshaler, isJSONUnmarshaler := address.(json.Unmarshaler); isJSONUnmarshaler {
|
if um, ok := addr.(json.Unmarshaler); ok {
|
||||||
_ = unmarshaler.UnmarshalJSON(cast.MustToJSONBytes(realSource.Interface()))
|
_ = um.UnmarshalJSON(cast.JsonBytes(rawFrom.Interface()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if unmarshaler, isYAMLUnmarshaler := address.(yaml.Unmarshaler); isYAMLUnmarshaler {
|
if um, ok := addr.(yaml.Unmarshaler); ok {
|
||||||
_ = unmarshaler.UnmarshalYAML(&yaml.Node{Value: cast.String(realSource.Interface())})
|
_ = um.UnmarshalYAML(&yaml.Node{Value: cast.String(rawFrom.Interface())})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 核心转换逻辑
|
// 4. 核心转换逻辑
|
||||||
sourceType := getActualType(realSource)
|
fromType := finalType(rawFrom)
|
||||||
destinationType := realDestination.Type()
|
destType := destValue.Type()
|
||||||
|
|
||||||
// 兼容 interface{} 目标
|
// 兼容 interface{} 目标
|
||||||
if destinationType.Kind() == reflect.Interface {
|
if destType.Kind() == reflect.Interface {
|
||||||
if realDestination.CanSet() {
|
if destValue.CanSet() {
|
||||||
realDestination.Set(reflect.ValueOf(realSource.Interface()))
|
destValue.Set(reflect.ValueOf(rawFrom.Interface()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 极致去摩擦:如果目标是单值,但输入是切片,自动取第一个元素进行后续处理
|
// 极致去摩擦:如果目标是单值,但输入是切片,自动取第一个元素进行后续处理
|
||||||
effectiveSource := realSource
|
effectiveFrom := rawFrom
|
||||||
if sourceType.Kind() == reflect.Slice && realSource.Len() > 0 && destinationType.Kind() != reflect.Slice && destinationType.Kind() != reflect.Array {
|
if fromType.Kind() == reflect.Slice && rawFrom.Len() > 0 && destType.Kind() != reflect.Slice && destType.Kind() != reflect.Array {
|
||||||
effectiveSource = cast.RealValue(realSource.Index(0))
|
effectiveFrom = cast.FinalValue(rawFrom.Index(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
var allocatedValue *reflect.Value
|
var newValue *reflect.Value
|
||||||
|
|
||||||
switch destinationType.Kind() {
|
switch destType.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.Bool(effectiveSource.Interface())), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.Bool(effectiveFrom.Interface())), &newValue)
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.String(effectiveSource.Interface())), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.String(effectiveFrom.Interface())), &newValue)
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.Int64(effectiveSource.Interface())).Convert(destinationType), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.Int64(effectiveFrom.Interface())).Convert(destType), &newValue)
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.Uint64(effectiveSource.Interface())).Convert(destinationType), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.Uint64(effectiveFrom.Interface())).Convert(destType), &newValue)
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.Float64(effectiveSource.Interface())).Convert(destinationType), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.Float64(effectiveFrom.Interface())).Convert(destType), &newValue)
|
||||||
|
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if destinationType.Elem().Kind() == reflect.Uint8 {
|
if destType.Elem().Kind() == reflect.Uint8 {
|
||||||
applyValue(realDestination, reflect.ValueOf(cast.MustToJSONBytes(realSource.Interface())), &allocatedValue)
|
setOrNew(destValue, reflect.ValueOf(cast.JsonBytes(rawFrom.Interface())), &newValue)
|
||||||
} else {
|
} else {
|
||||||
normalizedSource := realSource
|
workFrom := rawFrom
|
||||||
if sourceType.Kind() == reflect.String {
|
if fromType.Kind() == reflect.String {
|
||||||
sourceStr := realSource.String()
|
str := rawFrom.String()
|
||||||
if !strings.HasPrefix(sourceStr, "[") && strings.Contains(sourceStr, ",") {
|
if !strings.HasPrefix(str, "[") && strings.Contains(str, ",") {
|
||||||
normalizedSource = reflect.ValueOf(cast.Split(sourceStr, ","))
|
workFrom = reflect.ValueOf(cast.SplitTrim(str, ","))
|
||||||
} else if !strings.HasPrefix(sourceStr, "[") {
|
} else if !strings.HasPrefix(str, "[") {
|
||||||
tempSlice := reflect.MakeSlice(reflect.SliceOf(sourceType), 1, 1)
|
tmp := reflect.MakeSlice(reflect.SliceOf(fromType), 1, 1)
|
||||||
tempSlice.Index(0).Set(realSource)
|
tmp.Index(0).Set(rawFrom)
|
||||||
normalizedSource = tempSlice
|
workFrom = tmp
|
||||||
} else {
|
} else {
|
||||||
var rawList []any
|
var arr []any
|
||||||
cast.UnmarshalJSON(sourceStr, &rawList)
|
cast.UnJson(str, &arr)
|
||||||
normalizedSource = reflect.ValueOf(rawList)
|
workFrom = reflect.ValueOf(arr)
|
||||||
}
|
}
|
||||||
} else if sourceType.Kind() != reflect.Slice {
|
} else if fromType.Kind() != reflect.Slice {
|
||||||
tempSlice := reflect.MakeSlice(reflect.SliceOf(sourceType), 1, 1)
|
tmp := reflect.MakeSlice(reflect.SliceOf(fromType), 1, 1)
|
||||||
tempSlice.Index(0).Set(realSource)
|
tmp.Index(0).Set(rawFrom)
|
||||||
normalizedSource = tempSlice
|
workFrom = tmp
|
||||||
}
|
}
|
||||||
return convertSliceToSlice(normalizedSource, realDestination)
|
return convertSliceToSlice(workFrom, destValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
switch effectiveSource.Kind() {
|
switch effectiveFrom.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
convertMapToStruct(effectiveSource, realDestination)
|
convertMapToStruct(effectiveFrom, destValue)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
convertStructToStruct(effectiveSource, realDestination)
|
convertStructToStruct(effectiveFrom, destValue)
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
var rawMap map[string]any
|
var m map[string]any
|
||||||
cast.UnmarshalJSON(effectiveSource.String(), &rawMap)
|
cast.UnJson(effectiveFrom.String(), &m)
|
||||||
convertMapToStruct(reflect.ValueOf(rawMap), realDestination)
|
convertMapToStruct(reflect.ValueOf(m), destValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if realDestination.IsNil() {
|
if destValue.IsNil() {
|
||||||
realDestination = reflect.MakeMap(destinationType)
|
destValue = reflect.MakeMap(destType)
|
||||||
allocatedValue = &realDestination
|
newValue = &destValue
|
||||||
}
|
}
|
||||||
switch realSource.Kind() {
|
switch rawFrom.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
convertMapToMap(realSource, realDestination)
|
convertMapToMap(rawFrom, destValue)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
convertStructToMap(realSource, realDestination)
|
convertStructToMap(rawFrom, destValue)
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
var rawMap map[string]any
|
var m map[string]any
|
||||||
cast.UnmarshalJSON(realSource.String(), &rawMap)
|
cast.UnJson(rawFrom.String(), &m)
|
||||||
convertMapToMap(reflect.ValueOf(rawMap), realDestination)
|
convertMapToMap(reflect.ValueOf(m), destValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
case reflect.Func:
|
case reflect.Func:
|
||||||
if realSource.Kind() == reflect.Func {
|
if rawFrom.Kind() == reflect.Func {
|
||||||
realDestination.Set(reflect.MakeFunc(destinationType, func(args []reflect.Value) []reflect.Value {
|
destValue.Set(reflect.MakeFunc(destType, func(goArgs []reflect.Value) []reflect.Value {
|
||||||
inParameters := make([]reflect.Value, 0)
|
ins := make([]reflect.Value, 0)
|
||||||
for i := 0; i < destinationType.NumIn(); i++ {
|
for i := 0; i < destType.NumIn(); i++ {
|
||||||
if i < realSource.Type().NumIn() {
|
if i < rawFrom.Type().NumIn() {
|
||||||
paramPtr := reflect.New(realSource.Type().In(i))
|
argP := reflect.New(rawFrom.Type().In(i))
|
||||||
performConversion(args[i].Interface(), paramPtr)
|
convert(goArgs[i].Interface(), argP)
|
||||||
inParameters = append(inParameters, paramPtr.Elem())
|
ins = append(ins, argP.Elem())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results := realSource.Call(inParameters)
|
out := rawFrom.Call(ins)
|
||||||
outParameters := make([]reflect.Value, 0)
|
outs := make([]reflect.Value, 0)
|
||||||
for i := 0; i < destinationType.NumOut(); i++ {
|
for i := 0; i < destType.NumOut(); i++ {
|
||||||
resultPtr := reflect.New(destinationType.Out(i))
|
outP := reflect.New(destType.Out(i))
|
||||||
if i < len(results) {
|
if i < len(out) {
|
||||||
performConversion(results[i].Interface(), resultPtr)
|
convert(out[i].Interface(), outP)
|
||||||
}
|
}
|
||||||
outParameters = append(outParameters, resultPtr.Elem())
|
outs = append(outs, outP.Elem())
|
||||||
}
|
}
|
||||||
return outParameters
|
return outs
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allocatedValue
|
return newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyValue(destination, value reflect.Value, allocatedValue **reflect.Value) {
|
func setOrNew(dest, val reflect.Value, newValue **reflect.Value) {
|
||||||
if destination.CanSet() {
|
if dest.CanSet() {
|
||||||
destination.Set(value)
|
dest.Set(val)
|
||||||
} else {
|
} else {
|
||||||
*allocatedValue = &value
|
*newValue = &val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureInitialized(value reflect.Value) {
|
func fixNilValue(v reflect.Value) {
|
||||||
currentType := value.Type()
|
t := v.Type()
|
||||||
for currentType.Kind() == reflect.Ptr {
|
for t.Kind() == reflect.Ptr {
|
||||||
if value.IsNil() {
|
if v.IsNil() {
|
||||||
value.Set(reflect.New(value.Type().Elem()))
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
}
|
}
|
||||||
value = value.Elem()
|
v = v.Elem()
|
||||||
currentType = currentType.Elem()
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
if currentType.Kind() == reflect.Slice && value.IsNil() {
|
if t.Kind() == reflect.Slice && v.IsNil() {
|
||||||
value.Set(reflect.MakeSlice(value.Type(), 0, 0))
|
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
|
||||||
}
|
}
|
||||||
if currentType.Kind() == reflect.Map && value.IsNil() {
|
if t.Kind() == reflect.Map && v.IsNil() {
|
||||||
value.Set(reflect.MakeMap(value.Type()))
|
v.Set(reflect.MakeMap(v.Type()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getActualType(value reflect.Value) reflect.Type {
|
func finalType(v reflect.Value) reflect.Type {
|
||||||
if !value.IsValid() {
|
if !v.IsValid() {
|
||||||
return reflect.TypeOf(nil)
|
return reflect.TypeOf(nil)
|
||||||
}
|
}
|
||||||
actualType := value.Type()
|
t := v.Type()
|
||||||
for actualType.Kind() == reflect.Ptr {
|
for t.Kind() == reflect.Ptr {
|
||||||
actualType = actualType.Elem()
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
return actualType
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func normalizeKey(s string) string {
|
||||||
structFieldCache sync.Map
|
return strings.Map(func(r rune) rune {
|
||||||
structCacheCount int32
|
|
||||||
maxStructCacheSize = int32(10000)
|
|
||||||
)
|
|
||||||
|
|
||||||
func toLowerCamelCase(s string) string {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] >= 'a' && s[i] <= 'z' {
|
|
||||||
if i == 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return strings.ToLower(s[:i]) + s[i:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.ToLower(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeKey(key string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.Grow(len(key))
|
|
||||||
for _, r := range key {
|
|
||||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||||
b.WriteRune(r)
|
return r
|
||||||
} else if r >= 'A' && r <= 'Z' {
|
|
||||||
b.WriteRune(r + 32)
|
|
||||||
}
|
}
|
||||||
}
|
if r >= 'A' && r <= 'Z' {
|
||||||
return b.String()
|
return r + 32
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFieldMap(t reflect.Type) map[string]int {
|
func convertMapToStruct(from, to reflect.Value) {
|
||||||
m := make(map[string]int, t.NumField()*3)
|
keys := from.MapKeys()
|
||||||
for i := 0; i < t.NumField(); i++ {
|
keyMap := make(map[string]*reflect.Value)
|
||||||
field := t.Field(i)
|
|
||||||
if field.Anonymous {
|
for i := range keys {
|
||||||
|
keyMap[normalizeKey(cast.String(keys[i].Interface()))] = &keys[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
toType := to.Type()
|
||||||
|
for i := 0; i < toType.NumField(); i++ {
|
||||||
|
f := toType.Field(i)
|
||||||
|
if f.Anonymous {
|
||||||
|
convertMapToStruct(from, to.Field(i))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.Name[0] < 'A' || f.Name[0] > 'Z' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx := i + 1
|
|
||||||
m[field.Name] = idx
|
|
||||||
m[toLowerCamelCase(field.Name)] = idx
|
|
||||||
m[normalizeKey(field.Name)] = idx
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFieldMap(t reflect.Type) map[string]int {
|
k := keyMap[normalizeKey(f.Name)]
|
||||||
if val, ok := structFieldCache.Load(t); ok {
|
if k != nil {
|
||||||
return val.(map[string]int)
|
val := from.MapIndex(*k)
|
||||||
}
|
if val.IsValid() {
|
||||||
|
if to.CanAddr() {
|
||||||
m := buildFieldMap(t)
|
if m, ok := to.Addr().Type().MethodByName("Parse" + f.Name); ok {
|
||||||
|
argP := reflect.New(m.Type.In(1))
|
||||||
if atomic.LoadInt32(&structCacheCount) < maxStructCacheSize {
|
convert(val, argP)
|
||||||
structFieldCache.Store(t, m)
|
out := m.Func.Call([]reflect.Value{to.Addr(), argP.Elem()})
|
||||||
atomic.AddInt32(&structCacheCount, 1)
|
to.Field(i).Set(out[0])
|
||||||
}
|
continue
|
||||||
return m
|
}
|
||||||
}
|
}
|
||||||
|
r := convert(val, to.Field(i))
|
||||||
func convertMapToStruct(sourceMap, destinationStruct reflect.Value) {
|
if r != nil {
|
||||||
fieldMap := getFieldMap(destinationStruct.Type())
|
to.Field(i).Set(*r)
|
||||||
for _, key := range sourceMap.MapKeys() {
|
|
||||||
sourceKeyName := cast.String(key.Interface())
|
|
||||||
|
|
||||||
normalizedKey := sourceKeyName
|
|
||||||
idx, ok := fieldMap[normalizedKey]
|
|
||||||
if !ok {
|
|
||||||
normalizedKey = toLowerCamelCase(sourceKeyName)
|
|
||||||
idx, ok = fieldMap[normalizedKey]
|
|
||||||
if !ok {
|
|
||||||
normalizedKey = normalizeKey(sourceKeyName)
|
|
||||||
idx, ok = fieldMap[normalizedKey]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldIdx := idx - 1
|
|
||||||
fieldValue := destinationStruct.Field(fieldIdx)
|
|
||||||
sourceValue := sourceMap.MapIndex(key)
|
|
||||||
|
|
||||||
// 检查是否存在 ParseHook
|
|
||||||
structType := destinationStruct.Type()
|
|
||||||
fieldInfo := structType.Field(fieldIdx)
|
|
||||||
if destinationStruct.CanAddr() {
|
|
||||||
if method, exists := destinationStruct.Addr().Type().MethodByName("Parse" + fieldInfo.Name); exists {
|
|
||||||
argumentPointer := reflect.New(method.Type.In(1))
|
|
||||||
performConversion(sourceValue.Interface(), argumentPointer)
|
|
||||||
hookResults := method.Func.Call([]reflect.Value{destinationStruct.Addr(), argumentPointer.Elem()})
|
|
||||||
fieldValue.Set(hookResults[0])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedValue := performConversion(sourceValue.Interface(), fieldValue)
|
|
||||||
if convertedValue != nil {
|
|
||||||
fieldValue.Set(*convertedValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertStructToStruct(sourceStruct, destinationStruct reflect.Value) {
|
func convertStructToStruct(from, to reflect.Value) {
|
||||||
destFieldMap := getFieldMap(destinationStruct.Type())
|
fromType := from.Type()
|
||||||
|
keyMap := make(map[string]int)
|
||||||
sourceType := sourceStruct.Type()
|
|
||||||
for i := 0; i < sourceType.NumField(); i++ {
|
for i := 0; i < fromType.NumField(); i++ {
|
||||||
field := sourceType.Field(i)
|
f := fromType.Field(i)
|
||||||
if field.Anonymous {
|
if f.Name[0] >= 'A' && f.Name[0] <= 'Z' {
|
||||||
convertStructToStruct(sourceStruct.Field(i), destinationStruct)
|
keyMap[normalizeKey(f.Name)] = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toType := to.Type()
|
||||||
|
for i := 0; i < toType.NumField(); i++ {
|
||||||
|
f := toType.Field(i)
|
||||||
|
if f.Anonymous {
|
||||||
|
convertStructToStruct(from, to.Field(i))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if f.Name[0] < 'A' || f.Name[0] > 'Z' {
|
||||||
sourceName := field.Name
|
|
||||||
|
|
||||||
normalizedKey := sourceName
|
|
||||||
var fieldIdx int
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
if fieldIdx, ok = destFieldMap[normalizedKey]; !ok {
|
|
||||||
normalizedKey = toLowerCamelCase(sourceName)
|
|
||||||
if fieldIdx, ok = destFieldMap[normalizedKey]; !ok {
|
|
||||||
normalizedKey = normalizeKey(sourceName)
|
|
||||||
if fieldIdx, ok = destFieldMap[normalizedKey]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destFieldValue := destinationStruct.Field(fieldIdx - 1)
|
|
||||||
convertedValue := performConversion(sourceStruct.Field(i).Interface(), destFieldValue)
|
|
||||||
if convertedValue != nil {
|
|
||||||
destFieldValue.Set(*convertedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertMapToMap(sourceMap, destinationMap reflect.Value) {
|
|
||||||
destinationType := destinationMap.Type()
|
|
||||||
for _, key := range sourceMap.MapKeys() {
|
|
||||||
newKey := reflect.New(destinationType.Key()).Elem()
|
|
||||||
convertKey := performConversion(key, newKey)
|
|
||||||
if convertKey != nil {
|
|
||||||
newKey = *convertKey
|
|
||||||
}
|
|
||||||
newValue := reflect.New(destinationType.Elem()).Elem()
|
|
||||||
convertedValue := performConversion(sourceMap.MapIndex(key).Interface(), newValue)
|
|
||||||
if convertedValue != nil {
|
|
||||||
destinationMap.SetMapIndex(newKey, *convertedValue)
|
|
||||||
} else {
|
|
||||||
destinationMap.SetMapIndex(newKey, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertStructToMap(sourceStruct, destinationMap reflect.Value) {
|
|
||||||
destinationType := destinationMap.Type()
|
|
||||||
sourceType := sourceStruct.Type()
|
|
||||||
fieldCount := sourceStruct.NumField()
|
|
||||||
for i := 0; i < fieldCount; i++ {
|
|
||||||
fieldInfo := sourceType.Field(i)
|
|
||||||
if fieldInfo.Name[0] < 'A' || fieldInfo.Name[0] > 'Z' {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newKey := reflect.New(destinationType.Key()).Elem()
|
|
||||||
convertKey := performConversion(cast.GetLowerName(fieldInfo.Name), newKey)
|
k := keyMap[normalizeKey(f.Name)]
|
||||||
if convertKey != nil {
|
if k != 0 {
|
||||||
newKey = *convertKey
|
r := convert(from.Field(k-1), to.Field(i))
|
||||||
}
|
if r != nil {
|
||||||
newValue := reflect.New(destinationType.Elem()).Elem()
|
to.Field(i).Set(*r)
|
||||||
convertedValue := performConversion(sourceStruct.Field(i).Interface(), newValue)
|
}
|
||||||
if convertedValue != nil {
|
|
||||||
destinationMap.SetMapIndex(newKey, *convertedValue)
|
|
||||||
} else {
|
|
||||||
destinationMap.SetMapIndex(newKey, newValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertSliceToSlice(sourceSlice, destinationSlice reflect.Value) *reflect.Value {
|
func convertMapToMap(from, to reflect.Value) {
|
||||||
destinationType := destinationSlice.Type()
|
toType := to.Type()
|
||||||
sourceLen := sourceSlice.Len()
|
for _, k := range from.MapKeys() {
|
||||||
for i := 0; i < sourceLen; i++ {
|
keyItem := reflect.New(toType.Key()).Elem()
|
||||||
newItem := reflect.New(destinationType.Elem()).Elem()
|
convert(k, keyItem)
|
||||||
convertedValue := performConversion(sourceSlice.Index(i).Interface(), newItem)
|
valueItem := reflect.New(toType.Elem()).Elem()
|
||||||
if convertedValue != nil {
|
r := convert(from.MapIndex(k), valueItem)
|
||||||
destinationSlice = reflect.Append(destinationSlice, *convertedValue)
|
if r != nil {
|
||||||
|
to.SetMapIndex(keyItem, *r)
|
||||||
} else {
|
} else {
|
||||||
destinationSlice = reflect.Append(destinationSlice, newItem)
|
to.SetMapIndex(keyItem, valueItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &destinationSlice
|
}
|
||||||
|
|
||||||
|
func convertStructToMap(from, to reflect.Value) {
|
||||||
|
toType := to.Type()
|
||||||
|
fromType := from.Type()
|
||||||
|
for i := 0; i < from.NumField(); i++ {
|
||||||
|
f := fromType.Field(i)
|
||||||
|
if f.Name[0] < 'A' || f.Name[0] > 'Z' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyItem := reflect.New(toType.Key()).Elem()
|
||||||
|
convert(cast.GetLowerName(f.Name), keyItem)
|
||||||
|
valueItem := reflect.New(toType.Elem()).Elem()
|
||||||
|
r := convert(from.Field(i), valueItem)
|
||||||
|
if r != nil {
|
||||||
|
to.SetMapIndex(keyItem, *r)
|
||||||
|
} else {
|
||||||
|
to.SetMapIndex(keyItem, valueItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertSliceToSlice(from, to reflect.Value) *reflect.Value {
|
||||||
|
toType := to.Type()
|
||||||
|
for i := 0; i < from.Len(); i++ {
|
||||||
|
valueItem := reflect.New(toType.Elem()).Elem()
|
||||||
|
r := convert(from.Index(i), valueItem)
|
||||||
|
if r != nil {
|
||||||
|
to = reflect.Append(to, *r)
|
||||||
|
} else {
|
||||||
|
to = reflect.Append(to, valueItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &to
|
||||||
}
|
}
|
||||||
|
|
||||||
type StructInfo struct {
|
type StructInfo struct {
|
||||||
@ -430,69 +360,46 @@ type StructInfo struct {
|
|||||||
MethodValues map[string]reflect.Value
|
MethodValues map[string]reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func FlatStruct(data any) *StructInfo { return FlattenStruct(data, true) }
|
func FlatStruct(data any) *StructInfo { return flat(data, true) }
|
||||||
func FlatStructWithUnexported(data any) *StructInfo { return FlattenStruct(data, false) }
|
func FlatStructWithUnexported(data any) *StructInfo { return flat(data, false) }
|
||||||
|
|
||||||
func FlattenStruct(data any, exportOnly bool) *StructInfo {
|
func flat(data any, onlyExported bool) *StructInfo {
|
||||||
info := &StructInfo{
|
out := &StructInfo{
|
||||||
Fields: []reflect.StructField{},
|
Fields: []reflect.StructField{},
|
||||||
Values: make(map[string]reflect.Value),
|
Values: make(map[string]reflect.Value),
|
||||||
Methods: []reflect.Method{},
|
Methods: []reflect.Method{},
|
||||||
MethodValues: make(map[string]reflect.Value),
|
MethodValues: make(map[string]reflect.Value),
|
||||||
}
|
}
|
||||||
|
var v reflect.Value
|
||||||
reflectValue := reflect.ValueOf(data)
|
if rv, ok := data.(reflect.Value); ok { v = rv } else { v = reflect.ValueOf(data) }
|
||||||
for reflectValue.Kind() == reflect.Ptr {
|
makeStructInfo(v, out, onlyExported)
|
||||||
reflectValue = reflectValue.Elem()
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集方法 (仅当为指针类型时)
|
func makeStructInfo(v reflect.Value, out *StructInfo, onlyExported bool) {
|
||||||
if reflect.TypeOf(data).Kind() == reflect.Ptr {
|
for v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Ptr { v = v.Elem() }
|
||||||
methodCount := reflectValue.NumMethod()
|
fv := v
|
||||||
for i := 0; i < methodCount; i++ {
|
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { fv = v.Elem() }
|
||||||
method := reflectValue.Type().Method(i)
|
if fv.Kind() == reflect.Struct {
|
||||||
if !exportOnly || method.IsExported() {
|
t := v.Type()
|
||||||
info.Methods = append(info.Methods, method)
|
if v.Kind() == reflect.Ptr {
|
||||||
info.MethodValues[method.Name] = reflectValue.Method(i)
|
for i := 0; i < v.NumMethod(); i++ {
|
||||||
|
m := t.Method(i)
|
||||||
|
if onlyExported && !m.IsExported() { continue }
|
||||||
|
out.Methods = append(out.Methods, m)
|
||||||
|
out.MethodValues[m.Name] = v.Method(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ft := fv.Type()
|
||||||
|
for i := 0; i < ft.NumField(); i++ {
|
||||||
|
f := ft.Field(i)
|
||||||
|
if onlyExported && !f.IsExported() { continue }
|
||||||
|
if f.Anonymous {
|
||||||
|
makeStructInfo(fv.Field(i), out, onlyExported)
|
||||||
|
} else {
|
||||||
|
out.Fields = append(out.Fields, f)
|
||||||
|
out.Values[f.Name] = fv.Field(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
walkStruct(reflectValue, exportOnly, 0, func(field reflect.StructField, value reflect.Value) {
|
|
||||||
info.Fields = append(info.Fields, field)
|
|
||||||
info.Values[field.Name] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
type fieldVisitor func(field reflect.StructField, value reflect.Value)
|
|
||||||
|
|
||||||
func walkStruct(value reflect.Value, exportOnly bool, depth int, visitor fieldVisitor) {
|
|
||||||
if depth > 10 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for value.Kind() == reflect.Ptr {
|
|
||||||
value = value.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Kind() != reflect.Struct {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
structType := value.Type()
|
|
||||||
for i := 0; i < value.NumField(); i++ {
|
|
||||||
field := structType.Field(i)
|
|
||||||
if exportOnly && !field.IsExported() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val := value.Field(i)
|
|
||||||
if field.Anonymous {
|
|
||||||
walkStruct(val, exportOnly, depth+1, visitor)
|
|
||||||
} else {
|
|
||||||
visitor(field, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,67 +107,27 @@ func TestFuncConversion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 命名风格模糊匹配测试
|
// 7. Parse 钩子测试
|
||||||
func TestFuzzyNamingMapping(t *testing.T) {
|
type HookStruct struct {
|
||||||
type TestStruct struct {
|
Status int
|
||||||
JSONTag string
|
|
||||||
UserID int
|
|
||||||
UserName string
|
|
||||||
IsActiveFlag bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试用例:各种不规范输入到标准结构体的映射
|
|
||||||
from := map[string]any{
|
|
||||||
"jsontag": "val1", // 小写
|
|
||||||
"JSONTag": "val2", // 原名
|
|
||||||
"JSON_TAG": "val3", // 下划线大写
|
|
||||||
"user_id": 123, // 下划线小写
|
|
||||||
"USERID": 456, // 全大写
|
|
||||||
"userName": "Andy", // 首字母小写
|
|
||||||
"user-name": "Bob", // 中划线
|
|
||||||
"is_active_flag": "true", // 模糊匹配 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var to TestStruct
|
|
||||||
convert.To(from, &to)
|
|
||||||
|
|
||||||
if to.UserID == 0 {
|
|
||||||
t.Errorf("UserID should be matched and set")
|
|
||||||
}
|
|
||||||
if to.UserName == "" {
|
|
||||||
t.Errorf("UserName should be matched and set")
|
|
||||||
}
|
|
||||||
if !to.IsActiveFlag {
|
|
||||||
t.Errorf("IsActiveFlag should be set to true")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Conversion result: %+v", to)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 异常防御测试 (Panic Prevention)
|
func (h *HookStruct) ParseStatus(v any) int {
|
||||||
func TestPanicPrevention(t *testing.T) {
|
s := reflect.ValueOf(v).String()
|
||||||
t.Run("nil destination", func(t *testing.T) {
|
if s == "active" {
|
||||||
defer func() {
|
return 1
|
||||||
if r := recover(); r != nil {
|
}
|
||||||
t.Errorf("Panic occurred with nil destination: %v", r)
|
return 0
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
convert.To("data", nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nil pointer destination", func(t *testing.T) {
|
func TestParseHook(t *testing.T) {
|
||||||
// nil 指针目标应该是可以接受的,库应当忽略或优雅处理,不应 panic
|
from := map[string]any{"status": "active"}
|
||||||
var p *int = nil
|
var to HookStruct
|
||||||
convert.To(123, &p) // 这里传入的是指针的指针
|
convert.To(from, &to)
|
||||||
if p == nil || *p != 123 {
|
|
||||||
t.Errorf("Expected p to be initialized and set to 123, got %v", p)
|
if to.Status != 1 {
|
||||||
}
|
t.Errorf("Parse hook failed, got %d", to.Status)
|
||||||
})
|
}
|
||||||
|
|
||||||
t.Run("read-only destination", func(t *testing.T) {
|
|
||||||
const i = 1
|
|
||||||
convert.To(123, i)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
// 辅助函数
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -3,6 +3,6 @@ module apigo.cc/go/convert
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.0.4
|
apigo.cc/go/cast v1.0.3
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
Normal file
6
go.sum
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
apigo.cc/go/cast v1.0.2 h1:L9cbD6gOxQdUTwLJz8aJlV7L1df7CH11N80qKhb1QTk=
|
||||||
|
apigo.cc/go/cast v1.0.2/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
Loading…
x
Reference in New Issue
Block a user