2024-10-28 21:37:58 +08:00
|
|
|
package structx_test
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-27 22:02:17 +08:00
|
|
|
"encoding/json"
|
2024-10-28 21:37:58 +08:00
|
|
|
"fmt"
|
2026-05-27 22:02:17 +08:00
|
|
|
"math"
|
2025-09-20 20:28:59 +08:00
|
|
|
"strings"
|
2026-05-27 22:02:17 +08:00
|
|
|
"sync"
|
2024-10-28 21:37:58 +08:00
|
|
|
"testing"
|
2025-09-20 20:28:59 +08:00
|
|
|
"time"
|
2024-10-28 21:37:58 +08:00
|
|
|
|
|
|
|
|
"code.yun.ink/pkg/structx"
|
2025-09-20 20:28:59 +08:00
|
|
|
"github.com/shopspring/decimal"
|
2024-10-28 21:37:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestAttactToStructMap(t *testing.T) {
|
|
|
|
|
|
|
|
|
|
m := make(map[string]interface{})
|
|
|
|
|
m["name"] = "Tom"
|
|
|
|
|
m["age"] = 18
|
|
|
|
|
m["is_man"] = true
|
|
|
|
|
fmt.Println(m)
|
|
|
|
|
|
|
|
|
|
d := Data{}
|
|
|
|
|
|
|
|
|
|
r, err := structx.AttactToStructAny(&d, m)
|
|
|
|
|
|
|
|
|
|
fmt.Printf("%+v %+v %+v", d, r, err)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Data struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Age int `json:"age"`
|
|
|
|
|
IsMan bool `json:"is_man"`
|
|
|
|
|
Addr string `json:"addr"`
|
|
|
|
|
}
|
2025-09-20 20:28:59 +08:00
|
|
|
|
|
|
|
|
// 基础测试结构体
|
|
|
|
|
type BasicStruct struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Age int `json:"age"`
|
|
|
|
|
Salary float64 `json:"salary"`
|
|
|
|
|
IsActive bool `json:"is_active"`
|
|
|
|
|
Count uint `json:"count"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 嵌套结构体
|
|
|
|
|
type NestedStruct struct {
|
|
|
|
|
Basic BasicStruct `json:"basic"`
|
|
|
|
|
Comment string `json:"comment"`
|
|
|
|
|
Amount decimal.Decimal `json:"amount"`
|
2025-09-21 00:30:53 +08:00
|
|
|
Amount2 *decimal.Decimal `json:"amount2"`
|
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
2025-09-20 20:28:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 指针嵌套结构体
|
|
|
|
|
type PointerStruct struct {
|
|
|
|
|
Basic *BasicStruct `json:"basic"`
|
|
|
|
|
Enabled bool `json:"enabled"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 多层嵌套结构体
|
|
|
|
|
type MultiLevelStruct struct {
|
|
|
|
|
Nested NestedStruct `json:"nested"`
|
|
|
|
|
Pointer *PointerStruct `json:"pointer"`
|
|
|
|
|
Tags []string `json:"tags"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 自定义类型
|
|
|
|
|
type CustomString string
|
|
|
|
|
type CustomInt int
|
|
|
|
|
|
|
|
|
|
type CustomTypeStruct struct {
|
|
|
|
|
ID CustomString `json:"id"`
|
|
|
|
|
Version CustomInt `json:"version"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 实现 TextUnmarshaler 接口的类型
|
|
|
|
|
type CustomUnmarshaler string
|
|
|
|
|
|
|
|
|
|
func (c *CustomUnmarshaler) UnmarshalText(text []byte) error {
|
|
|
|
|
*c = CustomUnmarshaler("custom_" + string(text))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UnmarshalerStruct struct {
|
|
|
|
|
Data CustomUnmarshaler `json:"data"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 复杂嵌套结构体
|
|
|
|
|
type ComplexStruct struct {
|
|
|
|
|
Basic BasicStruct `json:"basic"`
|
|
|
|
|
Nested *NestedStruct `json:"nested"`
|
|
|
|
|
Custom CustomTypeStruct `json:"custom"`
|
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
|
|
|
|
Metadata map[string]string `json:"metadata"`
|
|
|
|
|
Unmarshaler CustomUnmarshaler `json:"unmarshaler"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 基础类型测试
|
|
|
|
|
func TestAttactToStruct_BasicTypes(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected BasicStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "所有基础类型",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"name": "John Doe",
|
|
|
|
|
"age": "30",
|
|
|
|
|
"salary": "50000.50",
|
|
|
|
|
"is_active": "true",
|
|
|
|
|
"count": "100",
|
|
|
|
|
},
|
|
|
|
|
expected: BasicStruct{
|
|
|
|
|
Name: "John Doe",
|
|
|
|
|
Age: 30,
|
|
|
|
|
Salary: 50000.50,
|
|
|
|
|
IsActive: true,
|
|
|
|
|
Count: 100,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "部分字段",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"name": "Alice",
|
|
|
|
|
"age": "25",
|
|
|
|
|
},
|
|
|
|
|
expected: BasicStruct{
|
|
|
|
|
Name: "Alice",
|
|
|
|
|
Age: 25,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "无效布尔值",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"is_active": "invalid",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "无效数字",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"age": "not_a_number",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "空字符串处理",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"name": "",
|
|
|
|
|
"age": "0",
|
|
|
|
|
},
|
|
|
|
|
expected: BasicStruct{
|
|
|
|
|
Name: "",
|
|
|
|
|
Age: 0,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s != tt.expected {
|
|
|
|
|
t.Errorf("期望 %+v, 得到 %+v", tt.expected, s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证变更记录
|
|
|
|
|
if len(changes) != len(tt.input) {
|
|
|
|
|
t.Errorf("期望 %d 个变更记录, 得到 %d", len(tt.input), len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试嵌套结构体
|
|
|
|
|
func TestAttactToStruct_NestedStruct(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected NestedStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
2026-05-27 22:02:17 +08:00
|
|
|
{
|
|
|
|
|
name: "嵌套结构体赋值",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic.name": "John",
|
|
|
|
|
"basic.age": "30",
|
|
|
|
|
"basic.salary": "50000.0",
|
|
|
|
|
"basic.is_active": "true",
|
|
|
|
|
"comment": "test comment",
|
|
|
|
|
},
|
|
|
|
|
expected: NestedStruct{
|
|
|
|
|
Basic: BasicStruct{
|
|
|
|
|
Name: "John",
|
|
|
|
|
Age: 30,
|
|
|
|
|
Salary: 50000.0,
|
|
|
|
|
IsActive: true,
|
|
|
|
|
},
|
|
|
|
|
Comment: "test comment",
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
2025-09-20 20:28:59 +08:00
|
|
|
{
|
|
|
|
|
name: "部分嵌套字段",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic.name": "Alice",
|
|
|
|
|
"comment": "partial",
|
2025-09-21 12:06:32 +08:00
|
|
|
"amount": "123.45",
|
|
|
|
|
"amount2": "123.45", // 测试指针字段
|
|
|
|
|
"timestamp": "2024-01-01T15:04:05Z",
|
2025-09-20 20:28:59 +08:00
|
|
|
},
|
|
|
|
|
expected: NestedStruct{
|
|
|
|
|
Basic: BasicStruct{
|
|
|
|
|
Name: "Alice",
|
|
|
|
|
},
|
|
|
|
|
Comment: "partial",
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.Basic.Name != tt.expected.Basic.Name ||
|
|
|
|
|
s.Basic.Age != tt.expected.Basic.Age ||
|
|
|
|
|
s.Comment != tt.expected.Comment {
|
|
|
|
|
t.Errorf("期望 %+v, 得到 %+v", tt.expected, s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证嵌套字段的变更记录
|
|
|
|
|
for key := range tt.input {
|
|
|
|
|
if _, exists := changes[key]; !exists {
|
|
|
|
|
t.Errorf("缺少变更记录 for key: %s", key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试指针嵌套结构体
|
|
|
|
|
func TestAttactToStruct_PointerNested(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected PointerStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
2026-05-27 22:02:17 +08:00
|
|
|
{
|
|
|
|
|
name: "指针嵌套结构体",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic.name": "John",
|
|
|
|
|
"basic.age": "30",
|
|
|
|
|
"basic.is_active": "true",
|
|
|
|
|
"enabled": "true",
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
expected: PointerStruct{
|
|
|
|
|
Basic: &BasicStruct{
|
|
|
|
|
Name: "John",
|
|
|
|
|
Age: 30,
|
|
|
|
|
IsActive: true,
|
|
|
|
|
},
|
|
|
|
|
Enabled: true,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
2025-09-20 20:28:59 +08:00
|
|
|
{
|
|
|
|
|
name: "空指针初始化",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic.name": "New User",
|
|
|
|
|
"enabled": "true",
|
|
|
|
|
},
|
|
|
|
|
expected: PointerStruct{
|
|
|
|
|
Basic: &BasicStruct{
|
|
|
|
|
Name: "New User",
|
|
|
|
|
},
|
|
|
|
|
Enabled: true,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s PointerStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
_ = changes
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.Basic == nil || s.Basic.Name != tt.expected.Basic.Name ||
|
|
|
|
|
s.Enabled != tt.expected.Enabled {
|
|
|
|
|
t.Errorf("期望 %+v, 得到 %+v", tt.expected, s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 自定义类型测试
|
|
|
|
|
func TestAttactToStruct_CustomTypes(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected CustomTypeStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "自定义类型转换",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"id": "user_123",
|
|
|
|
|
"version": "2",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
},
|
|
|
|
|
expected: CustomTypeStruct{
|
|
|
|
|
ID: CustomString("user_123"),
|
|
|
|
|
Version: CustomInt(2),
|
|
|
|
|
Email: "test@example.com",
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s CustomTypeStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if string(s.ID) != string(tt.expected.ID) ||
|
|
|
|
|
int(s.Version) != int(tt.expected.Version) ||
|
|
|
|
|
s.Email != tt.expected.Email {
|
|
|
|
|
t.Errorf("期望 %+v, 得到 %+v", tt.expected, s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(changes) != len(tt.input) {
|
|
|
|
|
t.Errorf("期望 %d 变更记录, 得到 %d", len(tt.input), len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unmarshaler 接口测试
|
|
|
|
|
func TestAttactToStruct_TextUnmarshaler(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected UnmarshalerStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "TextUnmarshaler 接口",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"data": "test_data",
|
|
|
|
|
"name": "John",
|
|
|
|
|
},
|
|
|
|
|
expected: UnmarshalerStruct{
|
|
|
|
|
Data: CustomUnmarshaler("custom_test_data"),
|
|
|
|
|
Name: "John",
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s UnmarshalerStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if string(s.Data) != string(tt.expected.Data) || s.Name != tt.expected.Name {
|
|
|
|
|
t.Errorf("期望 Data=%s, Name=%s; 得到 Data=%s, Name=%s",
|
|
|
|
|
tt.expected.Data, tt.expected.Name, s.Data, s.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(changes) != len(tt.input) {
|
|
|
|
|
t.Errorf("期望 %d 变更记录, 得到 %d", len(tt.input), len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 错误场景测试
|
|
|
|
|
func TestAttactToStruct_ErrorScenarios(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
structPtr interface{}
|
|
|
|
|
input map[string]string
|
|
|
|
|
expectedErr string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "非指针参数",
|
|
|
|
|
structPtr: BasicStruct{},
|
|
|
|
|
input: map[string]string{"name": "test"},
|
|
|
|
|
expectedErr: "structxx 需要是非空指针",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "空指针",
|
|
|
|
|
structPtr: (*BasicStruct)(nil),
|
|
|
|
|
input: map[string]string{"name": "test"},
|
|
|
|
|
expectedErr: "需要是非空指针",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-09-20 21:17:17 +08:00
|
|
|
name: "自定义类型",
|
2025-09-20 20:28:59 +08:00
|
|
|
structPtr: &struct{ Data []string }{},
|
2025-09-20 21:17:17 +08:00
|
|
|
input: map[string]string{"Data": `["test","test2"]`},
|
|
|
|
|
expectedErr: "",
|
2025-09-20 20:28:59 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "无效的嵌套路径",
|
|
|
|
|
structPtr: &NestedStruct{},
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"nonexistent.field": "value",
|
|
|
|
|
"basic.name": "test",
|
|
|
|
|
},
|
2025-09-20 21:17:17 +08:00
|
|
|
expectedErr: "字段 nonexistent.field 不存在", // 应该忽略不存在的字段而不报错
|
2025-09-20 20:28:59 +08:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
ch, err := structx.AttactToStruct(tt.structPtr, tt.input)
|
|
|
|
|
fmt.Printf("变更记录:%+v %+v\n", ch,tt.structPtr)
|
|
|
|
|
|
|
|
|
|
if tt.expectedErr == "" && err != nil {
|
|
|
|
|
t.Errorf("不期望错误但得到: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tt.expectedErr != "" {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误包含 '%s', 但得到 nil", tt.expectedErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), tt.expectedErr) {
|
|
|
|
|
t.Errorf("期望错误包含 '%s', 但得到: %v", tt.expectedErr, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 变更记录验证测试
|
|
|
|
|
func TestAttactToStruct_ChangeInfoValidation(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expectedFields []string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "变更记录完整性",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"name": "New Name",
|
|
|
|
|
"age": "25",
|
|
|
|
|
"is_active": "true",
|
|
|
|
|
},
|
|
|
|
|
expectedFields: []string{"name", "age", "is_active"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
// 设置初始值
|
|
|
|
|
s.Name = "Old Name"
|
|
|
|
|
s.Age = 30
|
|
|
|
|
s.IsActive = false
|
|
|
|
|
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证所有期望的字段都有变更记录
|
|
|
|
|
for _, field := range tt.expectedFields {
|
|
|
|
|
change, exists := changes[field]
|
|
|
|
|
if !exists {
|
|
|
|
|
t.Errorf("缺少字段 %s 的变更记录", field)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证旧值和新值
|
2026-05-27 22:02:17 +08:00
|
|
|
if change.Old == nil {
|
|
|
|
|
t.Errorf("字段 %s 的旧值不应为 nil", field)
|
2025-09-20 20:28:59 +08:00
|
|
|
}
|
2026-05-27 22:02:17 +08:00
|
|
|
if change.New == nil {
|
|
|
|
|
t.Errorf("字段 %s 的新值不应为 nil", field)
|
2025-09-20 20:28:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证值类型正确
|
|
|
|
|
if change.Val == nil {
|
|
|
|
|
t.Errorf("字段 %s 的值不应为 nil", field)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证变更记录数量
|
|
|
|
|
if len(changes) != len(tt.expectedFields) {
|
|
|
|
|
t.Errorf("期望 %d 个变更记录, 得到 %d", len(tt.expectedFields), len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkAttactToStruct(b *testing.B) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
input := map[string]string{
|
|
|
|
|
"name": "benchmark",
|
|
|
|
|
"age": "30",
|
|
|
|
|
"salary": "50000.0",
|
|
|
|
|
"is_active": "true",
|
|
|
|
|
"count": "100",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
_, err := structx.AttactToStruct(&s, input)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.Fatalf("基准测试失败: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 性能测试
|
|
|
|
|
func BenchmarkAttactToStruct_Nested(b *testing.B) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
input := map[string]string{
|
|
|
|
|
"basic.name": "benchmark",
|
|
|
|
|
"basic.age": "30",
|
|
|
|
|
"basic.salary": "50000.0",
|
|
|
|
|
"basic.is_active": "true",
|
|
|
|
|
"comment": "test",
|
2025-09-20 21:17:17 +08:00
|
|
|
"amount": "500.0",
|
2025-09-21 00:30:53 +08:00
|
|
|
"amount2": "250.0",
|
|
|
|
|
"timestamp": "2024-01-01T12:00:00Z",
|
2025-09-20 20:28:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
_, err := structx.AttactToStruct(&s, input)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.Fatalf("基准测试失败: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AttactToStructAny 测试
|
|
|
|
|
func TestAttactToStructAny(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]interface{}
|
|
|
|
|
expected BasicStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "混合类型输入",
|
|
|
|
|
input: map[string]interface{}{
|
|
|
|
|
"name": "John", // string
|
|
|
|
|
"age": 30, // int
|
|
|
|
|
"salary": 50000.50, // float64
|
|
|
|
|
"is_active": true, // bool
|
|
|
|
|
"count": uint(100), // uint
|
|
|
|
|
},
|
|
|
|
|
expected: BasicStruct{
|
|
|
|
|
Name: "John",
|
|
|
|
|
Age: 30,
|
|
|
|
|
Salary: 50000.50,
|
|
|
|
|
IsActive: true,
|
|
|
|
|
Count: 100,
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "无法转换的类型",
|
|
|
|
|
input: map[string]interface{}{
|
|
|
|
|
"name": make(chan int), // 无法转换为字符串的类型
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
_, err := structx.AttactToStructAny(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s != tt.expected {
|
|
|
|
|
t.Errorf("期望 %+v, 得到 %+v", tt.expected, s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试指针类型嵌套结构体
|
|
|
|
|
type PointerNestedStruct struct {
|
|
|
|
|
BasicPtr *BasicStruct `json:"basic_ptr"`
|
|
|
|
|
Direct BasicStruct `json:"direct"`
|
|
|
|
|
Value string `json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_PointerNested2(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected PointerNestedStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "指针类型嵌套结构体",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic_ptr.name": "Pointer John",
|
|
|
|
|
"basic_ptr.age": "35",
|
|
|
|
|
"direct.name": "Direct John",
|
|
|
|
|
"direct.age": "25",
|
|
|
|
|
"value": "test",
|
|
|
|
|
},
|
|
|
|
|
expected: PointerNestedStruct{
|
|
|
|
|
BasicPtr: &BasicStruct{
|
|
|
|
|
Name: "Pointer John",
|
|
|
|
|
Age: 35,
|
|
|
|
|
},
|
|
|
|
|
Direct: BasicStruct{
|
|
|
|
|
Name: "Direct John",
|
|
|
|
|
Age: 25,
|
|
|
|
|
},
|
|
|
|
|
Value: "test",
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "空指针初始化",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"basic_ptr.name": "New User",
|
|
|
|
|
},
|
|
|
|
|
expected: PointerNestedStruct{
|
|
|
|
|
BasicPtr: &BasicStruct{
|
|
|
|
|
Name: "New User",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s PointerNestedStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("期望错误,但得到 nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证指针类型字段
|
|
|
|
|
if s.BasicPtr == nil {
|
|
|
|
|
t.Error("BasicPtr 不应该为 nil")
|
|
|
|
|
} else if s.BasicPtr.Name != tt.expected.BasicPtr.Name {
|
|
|
|
|
t.Errorf("BasicPtr.Name 期望 %s, 得到 %s", tt.expected.BasicPtr.Name, s.BasicPtr.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证直接嵌套字段
|
|
|
|
|
if s.Direct.Name != tt.expected.Direct.Name {
|
|
|
|
|
t.Errorf("Direct.Name 期望 %s, 得到 %s", tt.expected.Direct.Name, s.Direct.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证变更记录
|
|
|
|
|
expectedChangeCount := len(tt.input)
|
|
|
|
|
if len(changes) != expectedChangeCount {
|
|
|
|
|
t.Errorf("期望 %d 个变更记录, 得到 %d", expectedChangeCount, len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试多层指针嵌套
|
|
|
|
|
type MultiLevelPointerStruct struct {
|
|
|
|
|
Level1 *Level1Struct `json:"level1"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Level1Struct struct {
|
|
|
|
|
Level2 *Level2Struct `json:"level2"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Level2Struct struct {
|
|
|
|
|
Level3 *Level3Struct `json:"level3"`
|
|
|
|
|
Value int `json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Level3Struct struct {
|
|
|
|
|
FinalValue string `json:"final_value"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_MultiLevelPointer(t *testing.T) {
|
|
|
|
|
input := map[string]string{
|
|
|
|
|
"level1.name": "Top Level",
|
|
|
|
|
"level1.level2.value": "100",
|
|
|
|
|
"level1.level2.level3.final_value": "end_value",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var s MultiLevelPointerStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, input)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证多层指针嵌套
|
|
|
|
|
if s.Level1 == nil {
|
|
|
|
|
t.Error("Level1 不应该为 nil")
|
|
|
|
|
} else if s.Level1.Name != "Top Level" {
|
|
|
|
|
t.Errorf("Level1.Name 期望 Top Level, 得到 %s", s.Level1.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.Level1.Level2 == nil {
|
|
|
|
|
t.Error("Level2 不应该为 nil")
|
|
|
|
|
} else if s.Level1.Level2.Value != 100 {
|
|
|
|
|
t.Errorf("Level2.Value 期望 100, 得到 %d", s.Level1.Level2.Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.Level1.Level2.Level3 == nil {
|
|
|
|
|
t.Error("Level3 不应该为 nil")
|
|
|
|
|
} else if s.Level1.Level2.Level3.FinalValue != "end_value" {
|
|
|
|
|
t.Errorf("Level3.FinalValue 期望 end_value, 得到 %s", s.Level1.Level2.Level3.FinalValue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证变更记录
|
|
|
|
|
if len(changes) != 3 {
|
|
|
|
|
t.Errorf("期望 3 个变更记录, 得到 %d", len(changes))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试混合指针和值类型嵌套
|
|
|
|
|
type MixedNestedStruct struct {
|
|
|
|
|
PtrField *BasicStruct `json:"ptr_field"`
|
|
|
|
|
ValueField BasicStruct `json:"value_field"`
|
|
|
|
|
Simple string `json:"simple"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_MixedNested(t *testing.T) {
|
|
|
|
|
input := map[string]string{
|
|
|
|
|
"ptr_field.name": "Pointer Name",
|
|
|
|
|
"ptr_field.age": "40",
|
|
|
|
|
"value_field.name": "Value Name",
|
|
|
|
|
"value_field.age": "30",
|
|
|
|
|
"simple": "simple_value",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var s MixedNestedStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, input)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("意外的错误: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证指针字段
|
|
|
|
|
if s.PtrField == nil {
|
|
|
|
|
t.Error("PtrField 不应该为 nil")
|
|
|
|
|
} else {
|
|
|
|
|
if s.PtrField.Name != "Pointer Name" {
|
|
|
|
|
t.Errorf("PtrField.Name 期望 Pointer Name, 得到 %s", s.PtrField.Name)
|
|
|
|
|
}
|
|
|
|
|
if s.PtrField.Age != 40 {
|
|
|
|
|
t.Errorf("PtrField.Age 期望 40, 得到 %d", s.PtrField.Age)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证值字段
|
|
|
|
|
if s.ValueField.Name != "Value Name" {
|
|
|
|
|
t.Errorf("ValueField.Name 期望 Value Name, 得到 %s", s.ValueField.Name)
|
|
|
|
|
}
|
|
|
|
|
if s.ValueField.Age != 30 {
|
|
|
|
|
t.Errorf("ValueField.Age 期望 30, 得到 %d", s.ValueField.Age)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证简单字段
|
|
|
|
|
if s.Simple != "simple_value" {
|
|
|
|
|
t.Errorf("Simple 期望 simple_value, 得到 %s", s.Simple)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证变更记录
|
|
|
|
|
if len(changes) != 5 { // 5个字段的变更
|
|
|
|
|
t.Errorf("期望 5 个变更记录, 得到 %d", len(changes))
|
|
|
|
|
}
|
2026-05-27 22:02:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 新增综合测试 =====
|
|
|
|
|
|
|
|
|
|
// 全类型边界测试结构体
|
|
|
|
|
type AllKindsStruct struct {
|
|
|
|
|
Str string `json:"str"`
|
|
|
|
|
I int `json:"i"`
|
|
|
|
|
I8 int8 `json:"i8"`
|
|
|
|
|
I16 int16 `json:"i16"`
|
|
|
|
|
I32 int32 `json:"i32"`
|
|
|
|
|
I64 int64 `json:"i64"`
|
|
|
|
|
Ui uint `json:"ui"`
|
|
|
|
|
Ui8 uint8 `json:"ui8"`
|
|
|
|
|
Ui16 uint16 `json:"ui16"`
|
|
|
|
|
Ui32 uint32 `json:"ui32"`
|
|
|
|
|
Ui64 uint64 `json:"ui64"`
|
|
|
|
|
F32 float32 `json:"f32"`
|
|
|
|
|
F64 float64 `json:"f64"`
|
|
|
|
|
B bool `json:"b"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 测试所有基本类型及边界值
|
|
|
|
|
func TestAttactToStruct_AllBasicKinds(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
wantErr bool
|
|
|
|
|
check func(*testing.T, AllKindsStruct)
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "所有类型正常值",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"str": "hello", "i": "42", "i8": "127", "i16": "32767",
|
|
|
|
|
"i32": "2147483647", "i64": "9223372036854775807",
|
|
|
|
|
"ui": "42", "ui8": "255", "ui16": "65535",
|
|
|
|
|
"ui32": "4294967295", "ui64": "18446744073709551615",
|
|
|
|
|
"f32": "3.14", "f64": "3.141592653589793", "b": "true",
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, s AllKindsStruct) {
|
|
|
|
|
if s.Str != "hello" || s.I != 42 || s.I8 != 127 || s.I16 != 32767 || s.I32 != 2147483647 || s.I64 != 9223372036854775807 {
|
|
|
|
|
t.Errorf("int类型不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
if s.Ui != 42 || s.Ui8 != 255 || s.Ui16 != 65535 || s.Ui32 != 4294967295 || s.Ui64 != 18446744073709551615 {
|
|
|
|
|
t.Errorf("uint类型不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(float64(s.F32)-3.14) > 0.001 || math.Abs(s.F64-3.141592653589793) > 1e-14 {
|
|
|
|
|
t.Errorf("float类型不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
if s.B != true {
|
|
|
|
|
t.Errorf("bool不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "int8溢出错误",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"i8": "128",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "uint8溢出错误",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"ui16": "70000",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "负整数",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"i": "-1", "i8": "-128", "i64": "-9223372036854775808",
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, s AllKindsStruct) {
|
|
|
|
|
if s.I != -1 || s.I8 != -128 || s.I64 != -9223372036854775808 {
|
|
|
|
|
t.Errorf("负数不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "uint负数错误",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"ui": "-1",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "浮点数科学计数法",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"f64": "1e10",
|
|
|
|
|
"f32": "1e10",
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, s AllKindsStruct) {
|
|
|
|
|
if math.Abs(s.F64-1e10) > 1 {
|
|
|
|
|
t.Errorf("科学计数法不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "布尔值多种表达",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"b": "true",
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, s AllKindsStruct) {
|
|
|
|
|
if s.B != true {
|
|
|
|
|
t.Errorf("true不匹配")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "字符串含特殊字符",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"str": "hello\nworld\tunicodë",
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, s AllKindsStruct) {
|
|
|
|
|
if s.Str != "hello\nworld\tunicodë" {
|
|
|
|
|
t.Errorf("特殊字符串不匹配: %q", s.Str)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s AllKindsStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("期望错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if tt.check != nil {
|
|
|
|
|
tt.check(t, s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Map 字段测试 =====
|
|
|
|
|
|
|
|
|
|
type MapStruct struct {
|
|
|
|
|
Metadata map[string]string `json:"metadata"`
|
|
|
|
|
Counts map[string]int `json:"counts"`
|
|
|
|
|
Empty map[string]string `json:"empty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_Maps(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
input map[string]string
|
|
|
|
|
expected MapStruct
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "map[string]string",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"metadata": `{"key1":"val1","key2":"val2"}`,
|
|
|
|
|
},
|
|
|
|
|
expected: MapStruct{
|
|
|
|
|
Metadata: map[string]string{"key1": "val1", "key2": "val2"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "map[string]int",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"counts": `{"a":1,"b":2,"c":3}`,
|
|
|
|
|
},
|
|
|
|
|
expected: MapStruct{
|
|
|
|
|
Counts: map[string]int{"a": 1, "b": 2, "c": 3},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "空map",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"metadata": `{}`,
|
|
|
|
|
},
|
|
|
|
|
expected: MapStruct{
|
|
|
|
|
Metadata: map[string]string{},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "无效JSON错误",
|
|
|
|
|
input: map[string]string{
|
|
|
|
|
"metadata": `not-json`,
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
var s MapStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, tt.input)
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("期望错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for k, v := range tt.expected.Metadata {
|
|
|
|
|
if s.Metadata[k] != v {
|
|
|
|
|
t.Errorf("Metadata[%s] 期望 %s, 得到 %s", k, v, s.Metadata[k])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k, v := range tt.expected.Counts {
|
|
|
|
|
if s.Counts[k] != v {
|
|
|
|
|
t.Errorf("Counts[%s] 期望 %d, 得到 %d", k, v, s.Counts[k])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 切片/数组字段测试 =====
|
|
|
|
|
|
|
|
|
|
type SliceArrayStruct struct {
|
|
|
|
|
Tags []string `json:"tags"`
|
|
|
|
|
Scores []int64 `json:"scores"`
|
|
|
|
|
Empty []string `json:"empty"`
|
|
|
|
|
Floats []float64 `json:"floats"`
|
|
|
|
|
Coords [3]int `json:"coords"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SliceStructElemStruct struct {
|
|
|
|
|
Items []BasicStruct `json:"items"`
|
|
|
|
|
PtrItems []*BasicStruct `json:"ptr_items"`
|
|
|
|
|
Times []time.Time `json:"times"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_SlicesArrays(t *testing.T) {
|
|
|
|
|
t.Run("字符串切片", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"tags": `["go","json","test"]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Tags) != 3 || s.Tags[0] != "go" || s.Tags[1] != "json" || s.Tags[2] != "test" {
|
|
|
|
|
t.Fatalf("期望 [go json test], 得到 %v", s.Tags)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("整数切片大数值精度", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"scores": `[9007199254740993,9223372036854775807]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Scores) != 2 || s.Scores[0] != 9007199254740993 || s.Scores[1] != 9223372036854775807 {
|
|
|
|
|
t.Fatalf("大整数精度丢失: %v", s.Scores)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("浮点数切片", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"floats": `[3.14,2.718,1.0]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Floats) != 3 || math.Abs(s.Floats[0]-3.14) > 0.001 {
|
|
|
|
|
t.Fatalf("浮点数切片不匹配: %v", s.Floats)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("空切片", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"empty": `[]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Empty) != 0 {
|
|
|
|
|
t.Fatalf("期望空切片, 得到 %v", s.Empty)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("固定数组正常", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"coords": `[1,2,3]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Coords != [3]int{1, 2, 3} {
|
|
|
|
|
t.Fatalf("期望 [1 2 3], 得到 %v", s.Coords)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("固定数组长度不匹配错误", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"coords": `[1,2]`,
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("期望数组长度不匹配错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("切片元素为结构体", func(t *testing.T) {
|
|
|
|
|
var s SliceStructElemStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"items": `[{"name":"Alice","age":30,"salary":1000.5,"is_active":true,"count":1}]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Items) != 1 || s.Items[0].Name != "Alice" || s.Items[0].Age != 30 {
|
|
|
|
|
t.Fatalf("结构体切片不匹配: %+v", s.Items)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("切片元素为结构体指针", func(t *testing.T) {
|
|
|
|
|
var s SliceStructElemStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"ptr_items": `[{"name":"Bob","age":25}]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.PtrItems) != 1 || s.PtrItems[0] == nil || s.PtrItems[0].Name != "Bob" {
|
|
|
|
|
t.Fatalf("指针结构体切片不匹配: %+v", s.PtrItems)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("切片元素为time.Time", func(t *testing.T) {
|
|
|
|
|
var s SliceStructElemStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"times": `["2024-01-01T00:00:00Z","2025-06-15T12:30:00Z"]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Times) != 2 {
|
|
|
|
|
t.Fatalf("期望2个时间, 得到 %d", len(s.Times))
|
|
|
|
|
}
|
|
|
|
|
expected, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
|
|
|
|
|
if !s.Times[0].Equal(expected) {
|
|
|
|
|
t.Fatalf("时间不匹配: %v", s.Times[0])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("无效JSON数组错误", func(t *testing.T) {
|
|
|
|
|
var s SliceArrayStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"tags": `not-an-array`,
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("期望JSON数组解析错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== json.Unmarshaler 接口测试 =====
|
|
|
|
|
|
|
|
|
|
type CustomJSONType struct {
|
|
|
|
|
Value string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *CustomJSONType) UnmarshalJSON(data []byte) error {
|
|
|
|
|
var s string
|
|
|
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
c.Value = "json_" + s
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type JSONUnmarshalerStruct struct {
|
|
|
|
|
Data CustomJSONType `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_JSONUnmarshaler(t *testing.T) {
|
|
|
|
|
t.Run("json.Unmarshaler字符串值", func(t *testing.T) {
|
|
|
|
|
var s JSONUnmarshalerStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"data": "hello",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Data.Value != "json_hello" {
|
|
|
|
|
t.Fatalf("期望 json_hello, 得到 %s", s.Data.Value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("json.Unmarshaler JSON字符串值", func(t *testing.T) {
|
|
|
|
|
var s JSONUnmarshalerStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"data": `"quoted"`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Data.Value != "json_quoted" {
|
|
|
|
|
t.Fatalf("期望 json_quoted, 得到 %s", s.Data.Value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== time.Duration 测试 =====
|
|
|
|
|
|
|
|
|
|
type DurationStruct struct {
|
|
|
|
|
Timeout time.Duration `json:"timeout"`
|
|
|
|
|
Delay time.Duration `json:"delay"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_Duration(t *testing.T) {
|
|
|
|
|
t.Run("Duration字符串解析", func(t *testing.T) {
|
|
|
|
|
var s DurationStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"timeout": "5s",
|
|
|
|
|
"delay": "100ms",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Timeout != 5*time.Second {
|
|
|
|
|
t.Fatalf("期望 5s, 得到 %v", s.Timeout)
|
|
|
|
|
}
|
|
|
|
|
if s.Delay != 100*time.Millisecond {
|
|
|
|
|
t.Fatalf("期望 100ms, 得到 %v", s.Delay)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Duration无效格式错误", func(t *testing.T) {
|
|
|
|
|
var s DurationStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"timeout": "invalid",
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("期望Duration解析错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 嵌套结构体JSON对象字符串测试 =====
|
|
|
|
|
|
|
|
|
|
type JSONNestedParent struct {
|
|
|
|
|
Basic BasicStruct `json:"basic"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_NestedJSONObject(t *testing.T) {
|
|
|
|
|
t.Run("JSON对象字符串赋值嵌套结构体", func(t *testing.T) {
|
|
|
|
|
var s JSONNestedParent
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic": `{"name":"John","age":"30","salary":"50000.5","is_active":"true","count":"5"}`,
|
|
|
|
|
"name": "parent",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Basic.Name != "John" || s.Basic.Age != 30 || s.Basic.Salary != 50000.5 || !s.Basic.IsActive || s.Basic.Count != 5 {
|
|
|
|
|
t.Fatalf("嵌套结构体不匹配: %+v", s.Basic)
|
|
|
|
|
}
|
|
|
|
|
if s.Name != "parent" {
|
|
|
|
|
t.Fatalf("父字段不匹配: %s", s.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("无效JSON对象错误", func(t *testing.T) {
|
|
|
|
|
var s JSONNestedParent
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic": "not-json-object",
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("期望无效JSON对象错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 覆盖已有值测试 =====
|
|
|
|
|
|
|
|
|
|
type OverwriteStruct struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Age int `json:"age"`
|
|
|
|
|
Salary float64 `json:"salary"`
|
|
|
|
|
Active bool `json:"active"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_OverwriteValues(t *testing.T) {
|
|
|
|
|
t.Run("覆盖已有字符串值", func(t *testing.T) {
|
|
|
|
|
s := OverwriteStruct{Name: "OldName", Age: 10, Salary: 100.0, Active: false}
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"name": "NewName",
|
|
|
|
|
"age": "20",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Name != "NewName" || s.Age != 20 || s.Salary != 100.0 || s.Active != false {
|
|
|
|
|
t.Fatalf("覆盖不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
// 验证ChangeInfo记录旧值
|
|
|
|
|
if changes["name"].Old != "OldName" || changes["name"].New != "NewName" {
|
|
|
|
|
t.Fatalf("ChangeInfo.name 不匹配: %+v", changes["name"])
|
|
|
|
|
}
|
|
|
|
|
if changes["age"].Old != 10 || changes["age"].New != 20 {
|
|
|
|
|
t.Fatalf("ChangeInfo.age 不匹配: %+v", changes["age"])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("覆盖嵌套结构体", func(t *testing.T) {
|
|
|
|
|
s := NestedStruct{
|
|
|
|
|
Basic: BasicStruct{Name: "Old", Age: 1, Salary: 1.0, IsActive: false, Count: 0},
|
|
|
|
|
}
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic.name": "New",
|
|
|
|
|
"basic.age": "99",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Basic.Name != "New" || s.Basic.Age != 99 || s.Basic.Salary != 1.0 {
|
|
|
|
|
t.Fatalf("嵌套覆盖不匹配: %+v", s.Basic)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 边界与错误场景测试 =====
|
|
|
|
|
|
|
|
|
|
type SimpleStruct struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_EmptyEdgeCases(t *testing.T) {
|
|
|
|
|
t.Run("空输入map", func(t *testing.T) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, map[string]string{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("空map不应错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(changes) != 0 {
|
|
|
|
|
t.Fatalf("空map应无变更记录, 得到 %d", len(changes))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("AllowUnknownFields选项", func(t *testing.T) {
|
|
|
|
|
s := structx.NewStructProcessor(structx.AllowUnknownFields())
|
|
|
|
|
var v SimpleStruct
|
|
|
|
|
_, err := s.AttactToStruct(&v, map[string]string{
|
|
|
|
|
"name": "hello",
|
|
|
|
|
"unknown_field": "ignored",
|
|
|
|
|
"another_unknown": "ignored",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("AllowUnknownFields不应错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if v.Name != "hello" {
|
|
|
|
|
t.Fatalf("期望 hello, 得到 %s", v.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("默认不允许未知字段", func(t *testing.T) {
|
|
|
|
|
var v SimpleStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&v, map[string]string{
|
|
|
|
|
"name": "hello",
|
|
|
|
|
"bad_field": "error",
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("期望未知字段错误但得到nil")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 全指针字段结构体测试 =====
|
|
|
|
|
|
|
|
|
|
type AllPointerStruct struct {
|
|
|
|
|
Name *string `json:"name"`
|
|
|
|
|
Age *int `json:"age"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_AllPointerFields(t *testing.T) {
|
|
|
|
|
t.Run("全指针字段赋值", func(t *testing.T) {
|
|
|
|
|
var s AllPointerStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"name": "Alice",
|
|
|
|
|
"age": "30",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Name == nil || *s.Name != "Alice" {
|
|
|
|
|
t.Fatalf("name指针不匹配: %v", s.Name)
|
|
|
|
|
}
|
|
|
|
|
if s.Age == nil || *s.Age != 30 {
|
|
|
|
|
t.Fatalf("age指针不匹配: %v", s.Age)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 同一结构体类型多次出现测试 =====
|
|
|
|
|
|
|
|
|
|
type SameTypeReusedStruct struct {
|
|
|
|
|
First BasicStruct `json:"first"`
|
|
|
|
|
Second BasicStruct `json:"second"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_SameTypeReused(t *testing.T) {
|
|
|
|
|
var s SameTypeReusedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"first.name": "First",
|
|
|
|
|
"first.age": "10",
|
|
|
|
|
"second.name": "Second",
|
|
|
|
|
"second.age": "20",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.First.Name != "First" || s.First.Age != 10 {
|
|
|
|
|
t.Fatalf("First不匹配: %+v", s.First)
|
|
|
|
|
}
|
|
|
|
|
if s.Second.Name != "Second" || s.Second.Age != 20 {
|
|
|
|
|
t.Fatalf("Second不匹配: %+v", s.Second)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== ChangeInfo.Val 类型验证 =====
|
|
|
|
|
|
|
|
|
|
type TypedValStruct struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Age int `json:"age"`
|
|
|
|
|
Salary float64 `json:"salary"`
|
|
|
|
|
Active bool `json:"active"`
|
|
|
|
|
Count uint `json:"count"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_ChangeInfo_ValTypes(t *testing.T) {
|
|
|
|
|
var s TypedValStruct
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"name": "test",
|
|
|
|
|
"age": "25",
|
|
|
|
|
"salary": "1000.5",
|
|
|
|
|
"active": "true",
|
|
|
|
|
"count": "42",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typeCheck := func(field string, val interface{}, expectedType string) {
|
|
|
|
|
if val == nil {
|
|
|
|
|
t.Fatalf("字段 %s 的Val为nil", field)
|
|
|
|
|
}
|
|
|
|
|
actualType := fmt.Sprintf("%T", val)
|
|
|
|
|
if actualType != expectedType {
|
|
|
|
|
t.Errorf("字段 %s 的Val类型期望 %s, 得到 %s", field, expectedType, actualType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typeCheck("name", changes["name"].Val, "string")
|
|
|
|
|
typeCheck("age", changes["age"].Val, "int")
|
|
|
|
|
typeCheck("salary", changes["salary"].Val, "float64")
|
|
|
|
|
typeCheck("active", changes["active"].Val, "bool")
|
|
|
|
|
typeCheck("count", changes["count"].Val, "uint")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== AttactToStructAny 综合测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStructAny_Comprehensive(t *testing.T) {
|
|
|
|
|
t.Run("各种类型混合输入", func(t *testing.T) {
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
_, err := structx.AttactToStructAny(&s, map[string]interface{}{
|
|
|
|
|
"name": "John",
|
|
|
|
|
"age": 30,
|
|
|
|
|
"salary": 50000.50,
|
|
|
|
|
"is_active": true,
|
|
|
|
|
"count": uint(100),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Name != "John" || s.Age != 30 || s.Salary != 50000.50 || !s.IsActive || s.Count != 100 {
|
|
|
|
|
t.Fatalf("值不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("int8/int16等小类型", func(t *testing.T) {
|
|
|
|
|
var s AllKindsStruct
|
|
|
|
|
_, err := structx.AttactToStructAny(&s, map[string]interface{}{
|
|
|
|
|
"str": "hello", "i": 42, "i8": int8(127), "i16": int16(32767),
|
|
|
|
|
"i32": int32(100), "i64": int64(999),
|
|
|
|
|
"ui": uint(1), "ui8": uint8(255), "ui16": uint16(1),
|
|
|
|
|
"ui32": uint32(1), "ui64": uint64(1),
|
|
|
|
|
"f32": float32(1.5), "f64": float64(3.14), "b": true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.I8 != 127 || s.I16 != 32767 || s.Ui8 != 255 {
|
|
|
|
|
t.Fatalf("小类型不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("嵌套map输入", func(t *testing.T) {
|
|
|
|
|
var s PointerNestedStruct
|
|
|
|
|
_, err := structx.AttactToStructAny(&s, map[string]interface{}{
|
|
|
|
|
"basic_ptr.name": "FromAny",
|
|
|
|
|
"direct.name": "DirectAny",
|
|
|
|
|
"value": "val",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.BasicPtr == nil || s.BasicPtr.Name != "FromAny" {
|
|
|
|
|
t.Fatalf("BasicPtr不匹配: %+v", s.BasicPtr)
|
|
|
|
|
}
|
|
|
|
|
if s.Direct.Name != "DirectAny" {
|
|
|
|
|
t.Fatalf("Direct不匹配: %s", s.Direct.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 并发访问测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_ConcurrentAccess(t *testing.T) {
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
errChan := make(chan error, 20)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func(idx int) {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
var s BasicStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"name": fmt.Sprintf("goroutine_%d", idx),
|
|
|
|
|
"age": fmt.Sprintf("%d", 20+idx),
|
|
|
|
|
"salary": "1000.0",
|
|
|
|
|
"is_active": "true",
|
|
|
|
|
"count": "1",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
errChan <- err
|
|
|
|
|
}
|
|
|
|
|
}(i)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func(idx int) {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic.name": fmt.Sprintf("nested_%d", idx),
|
|
|
|
|
"basic.age": fmt.Sprintf("%d", idx),
|
|
|
|
|
"comment": "concurrent",
|
|
|
|
|
"amount": "1.23",
|
|
|
|
|
"amount2": "4.56",
|
|
|
|
|
"timestamp": "2024-01-01T00:00:00Z",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
errChan <- err
|
|
|
|
|
}
|
|
|
|
|
}(i)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
close(errChan)
|
|
|
|
|
|
|
|
|
|
for err := range errChan {
|
|
|
|
|
t.Errorf("并发访问错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== decimal.Decimal 指针字段测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_DecimalPointer(t *testing.T) {
|
|
|
|
|
t.Run("*decimal.Decimal初始化与赋值", func(t *testing.T) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"amount": "123.45",
|
|
|
|
|
"amount2": "678.90",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !s.Amount.Equal(decimal.RequireFromString("123.45")) {
|
|
|
|
|
t.Fatalf("amount期望 123.45, 得到 %s", s.Amount)
|
|
|
|
|
}
|
|
|
|
|
if s.Amount2 == nil {
|
|
|
|
|
t.Fatal("amount2不应为nil")
|
|
|
|
|
}
|
|
|
|
|
if !s.Amount2.Equal(decimal.RequireFromString("678.90")) {
|
|
|
|
|
t.Fatalf("amount2期望 678.90, 得到 %s", s.Amount2)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== time.Time 字段测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_Time(t *testing.T) {
|
|
|
|
|
t.Run("time.Time赋值", func(t *testing.T) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"timestamp": "2024-06-15T10:30:00Z",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
expected, _ := time.Parse(time.RFC3339, "2024-06-15T10:30:00Z")
|
|
|
|
|
if !s.Timestamp.Equal(expected) {
|
|
|
|
|
t.Fatalf("期望 %v, 得到 %v", expected, s.Timestamp)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("time.Time RFC3339Nano格式", func(t *testing.T) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"timestamp": "2024-01-01T15:04:05.999999999Z",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
expected, _ := time.Parse(time.RFC3339Nano, "2024-01-01T15:04:05.999999999Z")
|
|
|
|
|
if !s.Timestamp.Equal(expected) {
|
|
|
|
|
t.Fatalf("期望 %v, 得到 %v", expected, s.Timestamp)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 自定义类型别名各基础类型全覆盖测试 =====
|
|
|
|
|
|
|
|
|
|
type CustomBool bool
|
|
|
|
|
type CustomFloat64 float64
|
|
|
|
|
type CustomUint64 uint64
|
|
|
|
|
|
|
|
|
|
type FullAliasStruct struct {
|
|
|
|
|
Str CustomString `json:"str"`
|
|
|
|
|
I CustomInt `json:"i"`
|
|
|
|
|
Ui64 CustomUint64 `json:"ui64"`
|
|
|
|
|
F64 CustomFloat64 `json:"f64"`
|
|
|
|
|
B CustomBool `json:"b"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_FullAliasTypes(t *testing.T) {
|
|
|
|
|
var s FullAliasStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"str": "alias",
|
|
|
|
|
"i": "42",
|
|
|
|
|
"ui64": "18446744073709551615",
|
|
|
|
|
"f64": "3.14159",
|
|
|
|
|
"b": "true",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(s.Str) != "alias" || int(s.I) != 42 || uint64(s.Ui64) != 18446744073709551615 {
|
|
|
|
|
t.Fatalf("别名不匹配: str=%s i=%d ui64=%d", s.Str, s.I, s.Ui64)
|
|
|
|
|
}
|
|
|
|
|
if float64(s.F64) != 3.14159 || bool(s.B) != true {
|
|
|
|
|
t.Fatalf("别名不匹配: f64=%f b=%v", s.F64, s.B)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== ComplexStruct 全字段覆盖测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_ComplexStruct(t *testing.T) {
|
|
|
|
|
var s ComplexStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic.name": "Complex",
|
|
|
|
|
"basic.age": "50",
|
|
|
|
|
"nested.comment": "deep",
|
|
|
|
|
"nested.amount": "99.99",
|
|
|
|
|
"nested.timestamp": "2024-12-25T00:00:00Z",
|
|
|
|
|
"custom.id": "cid_001",
|
|
|
|
|
"custom.version": "3",
|
|
|
|
|
"custom.email": "c@test.com",
|
|
|
|
|
"timestamp": "2024-01-01T00:00:00Z",
|
|
|
|
|
"metadata": `{"env":"prod","region":"us-east-1"}`,
|
|
|
|
|
"unmarshaler": "raw_data",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Basic.Name != "Complex" || s.Basic.Age != 50 {
|
|
|
|
|
t.Errorf("Basic不匹配: %+v", s.Basic)
|
|
|
|
|
}
|
|
|
|
|
if s.Nested == nil {
|
|
|
|
|
t.Fatal("Nested不应为nil")
|
|
|
|
|
}
|
|
|
|
|
if s.Nested.Comment != "deep" {
|
|
|
|
|
t.Errorf("Nested.Comment不匹配: %s", s.Nested.Comment)
|
|
|
|
|
}
|
|
|
|
|
if s.Custom.ID != CustomString("cid_001") || s.Custom.Version != CustomInt(3) {
|
|
|
|
|
t.Errorf("Custom不匹配: %+v", s.Custom)
|
|
|
|
|
}
|
|
|
|
|
if s.Metadata["env"] != "prod" || s.Metadata["region"] != "us-east-1" {
|
|
|
|
|
t.Errorf("Metadata不匹配: %+v", s.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if string(s.Unmarshaler) != "custom_raw_data" {
|
|
|
|
|
t.Errorf("Unmarshaler不匹配: %s", s.Unmarshaler)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== processNestedStruct UseNumber 精度测试 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_NestedJSONObject_LargeNumber(t *testing.T) {
|
|
|
|
|
// 验证嵌套JSON对象中的大整数不会因float64丢失精度
|
|
|
|
|
var s JSONNestedParent
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic": `{"name":"test","age":9007199254740993}`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
// age是int类型,9007199254740993 > 2^53,如果经过float64会变成9007199254740992
|
|
|
|
|
if s.Basic.Age != 9007199254740993 {
|
|
|
|
|
t.Fatalf("大数精度丢失: 期望 9007199254740993, 得到 %d", s.Basic.Age)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== SetSliceElementValue bool 字符串值测试 =====
|
|
|
|
|
|
|
|
|
|
type SliceBoolStruct struct {
|
|
|
|
|
Flags []bool `json:"flags"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_SliceBoolString(t *testing.T) {
|
|
|
|
|
t.Run("布尔值字符串true/false", func(t *testing.T) {
|
|
|
|
|
var s SliceBoolStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"flags": `["true","false","true"]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Flags) != 3 || s.Flags[0] != true || s.Flags[1] != false || s.Flags[2] != true {
|
|
|
|
|
t.Fatalf("布尔切片不匹配: %v", s.Flags)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("布尔值数字1/0", func(t *testing.T) {
|
|
|
|
|
var s SliceBoolStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"flags": `[true,false]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Flags) != 2 || s.Flags[0] != true || s.Flags[1] != false {
|
|
|
|
|
t.Fatalf("布尔切片不匹配: %v", s.Flags)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 嵌入式结构体测试 =====
|
|
|
|
|
|
|
|
|
|
type EmbeddedBase struct {
|
|
|
|
|
BaseName string `json:"base_name"`
|
|
|
|
|
BaseAge int `json:"base_age"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EmbeddedStruct struct {
|
|
|
|
|
EmbeddedBase
|
|
|
|
|
OwnField string `json:"own_field"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_EmbeddedStruct(t *testing.T) {
|
|
|
|
|
t.Run("嵌入式字段直接访问", func(t *testing.T) {
|
|
|
|
|
var s EmbeddedStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"base_name": "embedded",
|
|
|
|
|
"base_age": "25",
|
|
|
|
|
"own_field": "own",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.BaseName != "embedded" || s.BaseAge != 25 || s.OwnField != "own" {
|
|
|
|
|
t.Fatalf("嵌入式结构体不匹配: %+v", s)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("嵌入式指针结构体字段提升", func(t *testing.T) {
|
|
|
|
|
type Inner struct {
|
|
|
|
|
Val string `json:"val"`
|
|
|
|
|
}
|
|
|
|
|
type Outer struct {
|
|
|
|
|
*Inner
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var s Outer
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"val": "inner_value",
|
|
|
|
|
"name": "outer_name",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Inner == nil {
|
|
|
|
|
t.Fatal("Inner不应为nil")
|
|
|
|
|
}
|
|
|
|
|
if s.Val != "inner_value" || s.Name != "outer_name" {
|
|
|
|
|
t.Fatalf("指针嵌入不匹配: Val=%s, Name=%s", s.Val, s.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== interface{} 字段测试 =====
|
|
|
|
|
|
|
|
|
|
type InterfaceStruct struct {
|
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_InterfaceField(t *testing.T) {
|
|
|
|
|
t.Run("interface{}字符串值", func(t *testing.T) {
|
|
|
|
|
var s InterfaceStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"data": `"hello"`,
|
|
|
|
|
"name": "test",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Name != "test" {
|
|
|
|
|
t.Fatalf("Name不匹配: %s", s.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("interface{}数字值", func(t *testing.T) {
|
|
|
|
|
var s InterfaceStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"data": "12345",
|
|
|
|
|
"name": "num",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Name != "num" {
|
|
|
|
|
t.Fatalf("Name不匹配: %s", s.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("interface{}对象值", func(t *testing.T) {
|
|
|
|
|
var s InterfaceStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"data": `{"key":"value"}`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
result, ok := s.Data.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("期望map[string]interface{}, 得到 %T", s.Data)
|
|
|
|
|
}
|
|
|
|
|
if result["key"] != "value" {
|
|
|
|
|
t.Fatalf("data.key不匹配: %v", result["key"])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== *[]Type 指针指向切片测试 =====
|
|
|
|
|
|
|
|
|
|
type PtrSliceStruct struct {
|
|
|
|
|
Tags *[]string `json:"tags"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_PtrToSlice(t *testing.T) {
|
|
|
|
|
var s PtrSliceStruct
|
|
|
|
|
_, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"tags": `["a","b","c"]`,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if s.Tags == nil {
|
|
|
|
|
t.Fatal("Tags不应为nil")
|
|
|
|
|
}
|
|
|
|
|
if len(*s.Tags) != 3 || (*s.Tags)[0] != "a" {
|
|
|
|
|
t.Fatalf("Tags不匹配: %v", *s.Tags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== ChangeInfo Old/New 类型验证(any) =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_ChangeInfo_OldNewTypes(t *testing.T) {
|
|
|
|
|
s := OverwriteStruct{Name: "old", Age: 10, Salary: 1.5, Active: true}
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"name": "new",
|
|
|
|
|
"age": "20",
|
|
|
|
|
"salary": "2.5",
|
|
|
|
|
"active": "false",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证Old是原始Go类型,而非字符串
|
|
|
|
|
if _, ok := changes["name"].Old.(string); !ok {
|
|
|
|
|
t.Errorf("name.Old期望string, 得到 %T", changes["name"].Old)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := changes["age"].Old.(int); !ok {
|
|
|
|
|
t.Errorf("age.Old期望int, 得到 %T", changes["age"].Old)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := changes["salary"].Old.(float64); !ok {
|
|
|
|
|
t.Errorf("salary.Old期望float64, 得到 %T", changes["salary"].Old)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := changes["active"].Old.(bool); !ok {
|
|
|
|
|
t.Errorf("active.Old期望bool, 得到 %T", changes["active"].Old)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== ChangeInfo Nested Old/New 验证 =====
|
|
|
|
|
|
|
|
|
|
func TestAttactToStruct_ChangeInfo_NestedOldNew(t *testing.T) {
|
|
|
|
|
var s NestedStruct
|
|
|
|
|
s.Basic = BasicStruct{Name: "old_name", Age: 10}
|
|
|
|
|
changes, err := structx.AttactToStruct(&s, map[string]string{
|
|
|
|
|
"basic.name": "new_name",
|
|
|
|
|
"basic.age": "99",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("意外错误: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if changes["basic.name"].Old != "old_name" || changes["basic.name"].New != "new_name" {
|
|
|
|
|
t.Errorf("basic.name Old/New 不匹配: Old=%v, New=%v",
|
|
|
|
|
changes["basic.name"].Old, changes["basic.name"].New)
|
|
|
|
|
}
|
|
|
|
|
if changes["basic.age"].Old != 10 || changes["basic.age"].New != 99 {
|
|
|
|
|
t.Errorf("basic.age Old/New 不匹配: Old=%v, New=%v",
|
|
|
|
|
changes["basic.age"].Old, changes["basic.age"].New)
|
|
|
|
|
}
|
2025-09-20 20:28:59 +08:00
|
|
|
}
|