mirror of
https://github.com/jefferyjob/go-easy-utils.git
synced 2026-04-22 15:57:06 +08:00
Feature/toStruct beta版本发布 (#95)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://go.dev)
|
||||
[](https://github.com/jefferyjob/go-easy-utils/releases)
|
||||
[](https://github.com/jefferyjob/go-easy-utils/actions)
|
||||
[](https://github.com/jefferyjob/go-easy-utils/actions/workflows/go.yml)
|
||||
[](https://goreportcard.com/report/github.com/jefferyjob/go-easy-utils)
|
||||
[](https://codecov.io/gh/jefferyjob/go-easy-utils)
|
||||
[](https://pkg.go.dev/github.com/jefferyjob/go-easy-utils/v2)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func setBool(field reflect.Value, value any, path string) error {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
field.SetBool(v)
|
||||
case string:
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: cannot convert %v to bool", path, v)
|
||||
}
|
||||
field.SetBool(b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setInt(field reflect.Value, value any, path string) error {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
field.SetInt(int64(v))
|
||||
case string:
|
||||
i, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
field.SetInt(i)
|
||||
default:
|
||||
return fmt.Errorf("%s: 无法转换 %v 为 int", path, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUint(field reflect.Value, value any, path string) error {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
field.SetUint(uint64(v))
|
||||
case string:
|
||||
u, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
field.SetUint(u)
|
||||
default:
|
||||
return fmt.Errorf("%s: 无法转换 %v 为 uint", path, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setFloat(field reflect.Value, value any, path string) error {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
field.SetFloat(v)
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
field.SetFloat(f)
|
||||
default:
|
||||
return fmt.Errorf("%s: 无法转换 %v 为 float", path, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setSlice(field reflect.Value, value any, path string) error {
|
||||
valSlice, ok := value.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("不是数组类型")
|
||||
}
|
||||
|
||||
elemType := field.Type().Elem()
|
||||
slice := reflect.MakeSlice(field.Type(), 0, len(valSlice))
|
||||
|
||||
for i, item := range valSlice {
|
||||
elem := reflect.New(elemType).Elem()
|
||||
if err := assignValue(elem, item, fmt.Sprintf("%s[%d]", path, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
slice = reflect.Append(slice, elem)
|
||||
}
|
||||
field.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// func setMap(field reflect.Value, value any, path string) error {
|
||||
// if field.Type().Key().Kind() != reflect.String {
|
||||
// return fmt.Errorf("map 仅支持 string 类型的 key")
|
||||
// }
|
||||
//
|
||||
// valMap, ok := value.(map[string]any)
|
||||
// if !ok {
|
||||
// return fmt.Errorf("%s: 不是 map 类型", path)
|
||||
// }
|
||||
//
|
||||
// mapValue := reflect.MakeMap(field.Type())
|
||||
// elemType := field.Type().Elem()
|
||||
//
|
||||
// for k, v := range valMap {
|
||||
// elem := reflect.New(elemType).Elem()
|
||||
// if err := assignValue(elem, v, fmt.Sprintf("%s[%s]", path, k)); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// mapValue.SetMapIndex(reflect.ValueOf(k), elem)
|
||||
// }
|
||||
// field.Set(mapValue)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// setMap 支持任意基础类型作为 key
|
||||
func setMap(field reflect.Value, value any, path string) error {
|
||||
// JSON decode 后对象总是 map[string]any
|
||||
rawMap, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s: 不是 map 类型", path)
|
||||
}
|
||||
|
||||
fieldType := field.Type()
|
||||
keyType := fieldType.Key()
|
||||
elemType := fieldType.Elem()
|
||||
|
||||
// 新建目标类型的 map
|
||||
result := reflect.MakeMapWithSize(fieldType, len(rawMap))
|
||||
|
||||
for rawKey, rawVal := range rawMap {
|
||||
// 把 string key 转成目标类型
|
||||
keyVal, err := parseMapKey(rawKey, keyType, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 递归处理 value
|
||||
elem := reflect.New(elemType).Elem()
|
||||
if err := assignValue(elem, rawVal, fmt.Sprintf("%s[%s]", path, rawKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.SetMapIndex(keyVal, elem)
|
||||
}
|
||||
|
||||
field.Set(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseMapKey 将 JSON 的 string key 转换为目标 key 类型的 reflect.Value
|
||||
func parseMapKey(keyStr string, keyType reflect.Type, path string) (reflect.Value, error) {
|
||||
switch keyType.Kind() {
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(keyStr).Convert(keyType), nil
|
||||
case reflect.Bool:
|
||||
b, err := strconv.ParseBool(keyStr)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: 无法将 %q 转为 bool", path, keyStr)
|
||||
}
|
||||
return reflect.ValueOf(b).Convert(keyType), nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bitSize := keyType.Bits()
|
||||
i, err := strconv.ParseInt(keyStr, 10, bitSize)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: 无法将 %q 转为 %s", path, keyStr, keyType.Kind())
|
||||
}
|
||||
return reflect.ValueOf(i).Convert(keyType), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bitSize := keyType.Bits()
|
||||
u, err := strconv.ParseUint(keyStr, 10, bitSize)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: 无法将 %q 转为 %s", path, keyStr, keyType.Kind())
|
||||
}
|
||||
return reflect.ValueOf(u).Convert(keyType), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bitSize := keyType.Bits()
|
||||
f, err := strconv.ParseFloat(keyStr, bitSize)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: 无法将 %q 转为 %s", path, keyStr, keyType.Kind())
|
||||
}
|
||||
return reflect.ValueOf(f).Convert(keyType), nil
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("%s: 不支持的 map key 类型 %s", path, keyType.Kind())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ToStruct JSON转结构体
|
||||
func ToStruct(jsonStr string, target any) error {
|
||||
// 将 JSON 解成 map[string]any
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 判断必须是指针
|
||||
rv := reflect.ValueOf(target)
|
||||
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Struct {
|
||||
return ErrPoint
|
||||
}
|
||||
|
||||
// 递归赋值到结构体中
|
||||
return fill(raw, reflect.ValueOf(target).Elem(), "")
|
||||
}
|
||||
|
||||
// fill 递归填充结构体字段
|
||||
func fill(data map[string]any, target reflect.Value, path string) error {
|
||||
targetType := target.Type()
|
||||
|
||||
for i := 0; i < target.NumField(); i++ {
|
||||
field := target.Field(i)
|
||||
// 若字段不可写,则跳过
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
structField := targetType.Field(i)
|
||||
jsonTag := structField.Tag.Get("json")
|
||||
|
||||
// 如果 jsonTag 为空或为 "-",则使用字段名
|
||||
// if jsonTag == "" || jsonTag == "-" {
|
||||
// jsonTag = structField.Name
|
||||
// }
|
||||
|
||||
// 取对应的值
|
||||
value, ok := data[jsonTag]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// 构造字段的完整路径,用于错误提示追踪。
|
||||
// 例如当前字段 jsonTag 为 "city",上一级 path 为 "address"
|
||||
// 那么 newPath 就是 "address.city",表示这是嵌套结构 address 里的 city 字段。
|
||||
// 如果是顶层字段(如 "name"),path 为 "",则直接使用 jsonTag 作为路径。
|
||||
newPath := jsonTag
|
||||
if path != "" {
|
||||
newPath = path + "." + jsonTag
|
||||
}
|
||||
|
||||
err := setFieldValue(field, structField.Type, value, newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setFieldValue 根据字段类型处理普通值或指针值
|
||||
func setFieldValue(field reflect.Value, fieldType reflect.Type, value any, path string) error {
|
||||
// 如果是指针类型,则初始化并递归赋值其 Elem()
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
// 创建指针指向的类型实例
|
||||
elemType := fieldType.Elem()
|
||||
elemValue := reflect.New(elemType).Elem()
|
||||
// 递归设置值
|
||||
if err := setFieldValue(elemValue, elemType, value, path); err != nil {
|
||||
return err
|
||||
}
|
||||
// 设置指针值
|
||||
field.Set(elemValue.Addr())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 非指针类型直接处理
|
||||
return assignValue(field, value, path)
|
||||
}
|
||||
|
||||
// assignValue 实际执行字段值赋值的逻辑
|
||||
func assignValue(field reflect.Value, value any, path string) error {
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(fmt.Sprintf("%v", value))
|
||||
case reflect.Bool:
|
||||
return setBool(field, value, path)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return setInt(field, value, path)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return setUint(field, value, path)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return setFloat(field, value, path)
|
||||
case reflect.Slice:
|
||||
return setSlice(field, value, path)
|
||||
case reflect.Map:
|
||||
return setMap(field, value, path)
|
||||
case reflect.Struct:
|
||||
subMap, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
// 可能是 json.RawMessage 类型,要再解一层
|
||||
bs, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(bs, &m); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
subMap = m
|
||||
}
|
||||
return fill(subMap, field, path)
|
||||
case reflect.Interface:
|
||||
if value == nil {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
} else {
|
||||
// 如果是 interface 类型,直接设置值
|
||||
field.Set(reflect.ValueOf(value))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s: 不支持类型 %s", path, field.Kind())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const Json1 = `{
|
||||
"name": "Bob",
|
||||
"age": "25",
|
||||
"height": 175.5,
|
||||
"weight": "60.2",
|
||||
"isBoy": true,
|
||||
"isStudent": "false",
|
||||
"nickname": null,
|
||||
"hobbies": ["reading", 123, true, null],
|
||||
"likeNum": [101, 202],
|
||||
"scores": ["99.5", 88.1, "76"],
|
||||
"tags": [],
|
||||
"address": {
|
||||
"country": "China",
|
||||
"city": "Shanghai",
|
||||
"postcode": 200000
|
||||
},
|
||||
"contacts": {
|
||||
"email": "bob@example.com",
|
||||
"phone": 1234567890,
|
||||
"wechat": null
|
||||
},
|
||||
"preferences": {
|
||||
"food": ["noodle", "pizza"],
|
||||
"sports": {
|
||||
"indoor": ["chess", "table tennis"],
|
||||
"outdoor": ["basketball", "football"]
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"lastLogin": "2025-07-08T10:30:00Z",
|
||||
"active": "true",
|
||||
"loginCount": 10
|
||||
},
|
||||
"notes": null,
|
||||
"custom": {
|
||||
"field1": "value1",
|
||||
"field2": 2
|
||||
}
|
||||
}`
|
||||
|
||||
type S1 struct {
|
||||
Name *string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Height float64 `json:"height"`
|
||||
Weight float64 `json:"weight"`
|
||||
IsBoy bool `json:"isBoy"`
|
||||
IsStudent bool `json:"isStudent"`
|
||||
Nickname string `json:"nickname"` // null 会被忽略
|
||||
Hobbies []string `json:"hobbies"` // 数组中混合类型,将自动转换为字符串
|
||||
LikeNum []int64 `json:"likeNum"`
|
||||
Scores []float64 `json:"scores"`
|
||||
Tags []string `json:"tags"`
|
||||
Address *Address `json:"address"`
|
||||
Contacts map[string]string `json:"contacts"`
|
||||
Preferences struct {
|
||||
Food []string `json:"food"`
|
||||
Sports map[string][]string `json:"sports"` // indoor / outdoor
|
||||
} `json:"preferences"` // 嵌套结构体
|
||||
Meta struct {
|
||||
LastLogin string `json:"lastLogin"`
|
||||
Active bool `json:"active"`
|
||||
LoginCount int `json:"loginCount"`
|
||||
} `json:"meta"` // 键值混合类型
|
||||
Notes string `json:"notes"` // null 会被忽略
|
||||
Custom map[string]any `json:"custom"` // 深层次 map 类型
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Postcode int `json:"postcode"`
|
||||
}
|
||||
|
||||
func TestToStruct1(t *testing.T) {
|
||||
s := &S1{}
|
||||
err := ToStruct(Json1, s)
|
||||
require.NoError(t, err)
|
||||
fmt.Printf("%+v\n", s)
|
||||
|
||||
fmt.Println("name: ", s.Name)
|
||||
fmt.Println("country: ", s.Address.Country)
|
||||
}
|
||||
|
||||
const Json2 = `{
|
||||
"name": "Bob",
|
||||
"address": {
|
||||
"country": "China",
|
||||
"city": "Shanghai",
|
||||
"postcode": 200000
|
||||
}
|
||||
}`
|
||||
|
||||
type S2 struct {
|
||||
Name *string `json:"name"`
|
||||
Address *Address `json:"address"`
|
||||
}
|
||||
|
||||
func TestA1(t *testing.T) {
|
||||
s := &S2{}
|
||||
err := json.Unmarshal([]byte(Json2), s)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Printf("%+v\n", s)
|
||||
fmt.Println("name: ", s.Name)
|
||||
fmt.Println("country: ", s.Address.Country)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const tmJ1 = `
|
||||
{
|
||||
"mapNum": {"1": "one", "42": "forty-two"},
|
||||
"mapBool": {"true": 1.23, "false": 4.56}
|
||||
}
|
||||
`
|
||||
|
||||
type tmS1 struct {
|
||||
MapNum map[int]string `json:"mapNum"`
|
||||
MapBool map[bool]float64 `json:"mapBool"`
|
||||
}
|
||||
|
||||
func TestMap1(t *testing.T) {
|
||||
var s tmS1
|
||||
err := ToStruct(tmJ1, &s)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Printf("%+v\n", s)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const tsJ1 = `
|
||||
{
|
||||
"name": "Alice",
|
||||
"age": "30"
|
||||
}
|
||||
`
|
||||
|
||||
type tsS1 struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
func TestStruct1(t *testing.T) {
|
||||
var s tsS1
|
||||
err := ToStruct(tsJ1, &s)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Printf("%+v\n", s)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package jsonxbeta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPoint 不是指针类型
|
||||
ErrPoint = errors.New("the argument to Result must be a non-nil pointer")
|
||||
// ErrNotMap 不是Map类型
|
||||
ErrNotMap = errors.New("cannot parse map, value is not a map")
|
||||
// ErrNotSlice 不是Slice类型
|
||||
ErrNotSlice = errors.New("cannot parse slice, value is not a slice")
|
||||
// ErrSyntax 指示值不具有目标类型的正确语法
|
||||
ErrSyntax = strconv.ErrSyntax
|
||||
// ErrUnsupported 不支持的类型
|
||||
ErrUnsupported = errors.New("unsupported type")
|
||||
)
|
||||
Reference in New Issue
Block a user