Feature/toStruct beta版本发布 (#95)

This commit is contained in:
libin
2025-07-11 09:57:06 +08:00
committed by GitHub
parent e1378e8d56
commit 8a9793b8a1
7 changed files with 508 additions and 1 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[![Go](https://img.shields.io/badge/Go->=1.24-green)](https://go.dev)
[![Release](https://img.shields.io/github/v/release/jefferyjob/go-easy-utils.svg)](https://github.com/jefferyjob/go-easy-utils/releases)
[![Action](https://github.com/jefferyjob/go-easy-utils/workflows/Go/badge.svg?branch=main)](https://github.com/jefferyjob/go-easy-utils/actions)
[![Action](https://github.com/jefferyjob/go-easy-utils/actions/workflows/go.yml/badge.svg)](https://github.com/jefferyjob/go-easy-utils/actions/workflows/go.yml)
[![Report](https://goreportcard.com/badge/github.com/jefferyjob/go-easy-utils)](https://goreportcard.com/report/github.com/jefferyjob/go-easy-utils)
[![Coverage](https://codecov.io/gh/jefferyjob/go-easy-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/jefferyjob/go-easy-utils)
[![Doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/jefferyjob/go-easy-utils/v2)
+185
View File
@@ -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())
}
}
+133
View File
@@ -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
}
+116
View File
@@ -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)
}
+27
View File
@@ -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)
}
+27
View File
@@ -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)
}
+19
View File
@@ -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")
)