| @@ -13,6 +13,9 @@ type AnyToAnyOption struct { | |||||
| NoFromAny bool // 不判断目的字段是否实现了FromAny接口 | NoFromAny bool // 不判断目的字段是否实现了FromAny接口 | ||||
| NoToAny bool // 不判断源字段是否实现了ToAny接口 | NoToAny bool // 不判断源字段是否实现了ToAny接口 | ||||
| Converters []Converter // 字段类型转换函数 | Converters []Converter // 字段类型转换函数 | ||||
| // 当目的类型为map[string]any,是否要递归的将源类型的每一个字段都变成map[string]any。 | |||||
| // 注:字段的类型(而不是实际值的类型)必须为结构体或者结构体指针。 | |||||
| RecursiveStructToMap bool | |||||
| } | } | ||||
| type FromAny interface { | type FromAny interface { | ||||
| @@ -43,6 +46,10 @@ func AnyToAny(src any, dst any, opts ...AnyToAnyOption) error { | |||||
| hooks = append(hooks, c) | hooks = append(hooks, c) | ||||
| } | } | ||||
| if opt.RecursiveStructToMap { | |||||
| hooks = append(hooks, mp.RecursiveStructToMapHookFunc()) | |||||
| } | |||||
| config := &mp.DecoderConfig{ | config := &mp.DecoderConfig{ | ||||
| TagName: "json", | TagName: "json", | ||||
| Squash: true, | Squash: true, | ||||
| @@ -4,6 +4,7 @@ import ( | |||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | "fmt" | ||||
| "reflect" | "reflect" | ||||
| "strings" | |||||
| ) | ) | ||||
| func ObjectToJSON(obj any) ([]byte, error) { | func ObjectToJSON(obj any) ([]byte, error) { | ||||
| @@ -29,9 +30,80 @@ func MapToObject(m map[string]any, obj any) error { | |||||
| } | } | ||||
| func ObjectToMap(obj any) (map[string]any, error) { | func ObjectToMap(obj any) (map[string]any, error) { | ||||
| var m map[string]any | |||||
| err := AnyToAny(obj, &m) | |||||
| return m, err | |||||
| ctx := WalkValue(obj, func(ctx *WalkContext, event WalkEvent) WalkingOp { | |||||
| switch e := event.(type) { | |||||
| case StructBeginEvent: | |||||
| mp := make(map[string]any) | |||||
| ctx.StackPush(mp) | |||||
| case StructArriveFieldEvent: | |||||
| if !WillWalkInto(e.Value) { | |||||
| ctx.StackPush(e.Value.Interface()) | |||||
| } | |||||
| case StructLeaveFieldEvent: | |||||
| val := ctx.StackPop() | |||||
| mp := ctx.StackPeek().(map[string]any) | |||||
| jsonTag := e.Info.Tag.Get("json") | |||||
| if jsonTag == "-" { | |||||
| break | |||||
| } | |||||
| opts := strings.Split(jsonTag, ",") | |||||
| keyName := opts[0] | |||||
| if keyName == "" { | |||||
| keyName = e.Info.Name | |||||
| } | |||||
| if contains(opts, "string", 1) { | |||||
| val = fmt.Sprintf("%v", val) | |||||
| } | |||||
| mp[keyName] = val | |||||
| case StructEndEvent: | |||||
| case MapBeginEvent: | |||||
| ctx.StackPush(make(map[string]any)) | |||||
| case MapArriveEntryEvent: | |||||
| if !WillWalkInto(e.Value) { | |||||
| ctx.StackPush(e.Value.Interface()) | |||||
| } | |||||
| case MapLeaveEntryEvent: | |||||
| val := ctx.StackPop() | |||||
| mp := ctx.StackPeek().(map[string]any) | |||||
| mp[fmt.Sprintf("%v", e.Key)] = val | |||||
| case MapEndEvent: | |||||
| case ArrayBeginEvent: | |||||
| ctx.StackPush(make([]any, e.Value.Len())) | |||||
| case ArrayArriveElementEvent: | |||||
| if !WillWalkInto(e.Value) { | |||||
| ctx.StackPush(e.Value.Interface()) | |||||
| } | |||||
| case ArrayLeaveElementEvent: | |||||
| val := ctx.StackPop() | |||||
| arr := ctx.StackPeek().([]any) | |||||
| arr[e.Index] = val | |||||
| case ArrayEndEvent: | |||||
| } | |||||
| return Next | |||||
| }, WalkOption{ | |||||
| StackValues: []any{make(map[string]any)}, | |||||
| }) | |||||
| return ctx.StackPop().(map[string]any), nil | |||||
| } | |||||
| func contains(arr []string, ele string, startIndex int) bool { | |||||
| for i := startIndex; i < len(arr); i++ { | |||||
| if arr[i] == ele { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | } | ||||
| func TypedMapToObject(m map[string]any, opt TypedSerderOption) (any, error) { | func TypedMapToObject(m map[string]any, opt TypedSerderOption) (any, error) { | ||||
| @@ -77,7 +77,7 @@ func (a DirToAnySt) ToAny(typ reflect.Type) (val any, ok bool, err error) { | |||||
| return nil, false, nil | return nil, false, nil | ||||
| } | } | ||||
| func Test_MapToObject(t *testing.T) { | |||||
| func Test_AnyToAny(t *testing.T) { | |||||
| Convey("包含用字符串保存的int数据", t, func() { | Convey("包含用字符串保存的int数据", t, func() { | ||||
| type Struct struct { | type Struct struct { | ||||
| A string `json:"a"` | A string `json:"a"` | ||||
| @@ -233,3 +233,165 @@ func Test_TypedMapToObject(t *testing.T) { | |||||
| }) | }) | ||||
| } | } | ||||
| func Test_MapToObject(t *testing.T) { | |||||
| type Base struct { | |||||
| Int int | |||||
| Bool bool | |||||
| String string | |||||
| Float float32 | |||||
| } | |||||
| type ArraryStruct struct { | |||||
| IntArr []int | |||||
| StArr []Base | |||||
| ArrArr [][]int | |||||
| Nil []Base | |||||
| } | |||||
| type MapStruct struct { | |||||
| StrMap map[string]string | |||||
| StMap map[string]Base | |||||
| MapMap map[string]map[string]string | |||||
| Nil map[string]Base | |||||
| } | |||||
| type Top struct { | |||||
| ArrSt ArraryStruct | |||||
| MapSt *MapStruct | |||||
| BaseIf any | |||||
| NilPtr *Base | |||||
| } | |||||
| Convey("结构体递归转换成map[string]any", t, func() { | |||||
| val := Top{ | |||||
| ArrSt: ArraryStruct{ | |||||
| IntArr: []int{1, 2, 3}, | |||||
| StArr: []Base{ | |||||
| { | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| { | |||||
| Int: 2, | |||||
| Bool: false, | |||||
| String: "test2", | |||||
| Float: 2, | |||||
| }, | |||||
| }, | |||||
| ArrArr: [][]int{ | |||||
| {1, 2, 3}, | |||||
| {}, | |||||
| nil, | |||||
| }, | |||||
| Nil: nil, | |||||
| }, | |||||
| MapSt: &MapStruct{ | |||||
| StrMap: map[string]string{ | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| StMap: map[string]Base{ | |||||
| "a": { | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| "b": { | |||||
| Int: 2, | |||||
| Bool: false, | |||||
| String: "test2", | |||||
| Float: 2, | |||||
| }, | |||||
| }, | |||||
| MapMap: map[string]map[string]string{ | |||||
| "a": { | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| "b": nil, | |||||
| }, | |||||
| Nil: nil, | |||||
| }, | |||||
| BaseIf: Base{ | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| NilPtr: nil, | |||||
| } | |||||
| retMp, err := ObjectToMap(val) | |||||
| So(err, ShouldBeNil) | |||||
| exceptMap := map[string]any{ | |||||
| "ArrSt": map[string]any{ | |||||
| "IntArr": []any{1, 2, 3}, | |||||
| "StArr": []any{ | |||||
| map[string]any{ | |||||
| "Int": 1, | |||||
| "Bool": true, | |||||
| "String": "test", | |||||
| "Float": 1, | |||||
| }, | |||||
| map[string]any{ | |||||
| "Int": 2, | |||||
| "Bool": false, | |||||
| "String": "test2", | |||||
| "Float": 2, | |||||
| }, | |||||
| }, | |||||
| "ArrArr": []any{ | |||||
| []any{1, 2, 3}, | |||||
| []any{}, | |||||
| []int(nil), | |||||
| }, | |||||
| "Nil": []Base(nil), | |||||
| }, | |||||
| "MapSt": map[string]any{ | |||||
| "StrMap": map[string]any{ | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| "StMap": map[string]any{ | |||||
| "a": map[string]any{ | |||||
| "Int": 1, | |||||
| "Bool": true, | |||||
| "String": "test", | |||||
| "Float": 1, | |||||
| }, | |||||
| "b": map[string]any{ | |||||
| "Int": 2, | |||||
| "Bool": false, | |||||
| "String": "test2", | |||||
| "Float": 2, | |||||
| }, | |||||
| }, | |||||
| "MapMap": map[string]any{ | |||||
| "a": map[string]any{ | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| "b": map[string]string(nil), | |||||
| }, | |||||
| "Nil": map[string]Base(nil), | |||||
| }, | |||||
| "BaseIf": map[string]any{ | |||||
| "Int": 1, | |||||
| "Bool": true, | |||||
| "String": "test", | |||||
| "Float": 1, | |||||
| }, | |||||
| "NilPtr": (*Base)(nil), | |||||
| } | |||||
| mpRetJson, err := ObjectToJSON(retMp) | |||||
| So(err, ShouldBeNil) | |||||
| exceptMapJson, err := ObjectToJSON(exceptMap) | |||||
| So(err, ShouldBeNil) | |||||
| So(string(mpRetJson), ShouldEqualJSON, string(exceptMapJson)) | |||||
| }) | |||||
| } | |||||
| @@ -0,0 +1,150 @@ | |||||
| package serder | |||||
| import ( | |||||
| "fmt" | |||||
| "reflect" | |||||
| "testing" | |||||
| . "github.com/smartystreets/goconvey/convey" | |||||
| myreflect "gitlink.org.cn/cloudream/common/utils/reflect" | |||||
| ) | |||||
| func Test_WalkValue(t *testing.T) { | |||||
| type Base struct { | |||||
| Int int | |||||
| Bool bool | |||||
| String string | |||||
| Float float32 | |||||
| } | |||||
| type ArraryStruct struct { | |||||
| IntArr []int | |||||
| StArr []Base | |||||
| ArrArr [][]int | |||||
| Nil []Base | |||||
| } | |||||
| type MapStruct struct { | |||||
| StrMap map[string]string | |||||
| StMap map[string]Base | |||||
| MapMap map[string]map[string]string | |||||
| Nil map[string]Base | |||||
| } | |||||
| type Top struct { | |||||
| ArrSt ArraryStruct | |||||
| MapSt *MapStruct | |||||
| BaseIf any | |||||
| NilPtr *Base | |||||
| } | |||||
| isBaseDataType := func(val reflect.Value) bool { | |||||
| typ := val.Type() | |||||
| return typ == myreflect.TypeOf[int]() || typ == myreflect.TypeOf[bool]() || | |||||
| typ == myreflect.TypeOf[string]() || typ == myreflect.TypeOf[float32]() || val.IsZero() | |||||
| } | |||||
| toString := func(val any) string { | |||||
| return fmt.Sprintf("%v", val) | |||||
| } | |||||
| Convey("遍历", t, func() { | |||||
| val := Top{ | |||||
| ArrSt: ArraryStruct{ | |||||
| IntArr: []int{1, 2, 3}, | |||||
| StArr: []Base{ | |||||
| { | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| { | |||||
| Int: 2, | |||||
| Bool: false, | |||||
| String: "test2", | |||||
| Float: 2, | |||||
| }, | |||||
| }, | |||||
| ArrArr: [][]int{ | |||||
| {1, 2, 3}, | |||||
| {}, | |||||
| nil, | |||||
| }, | |||||
| Nil: nil, | |||||
| }, | |||||
| MapSt: &MapStruct{ | |||||
| StrMap: map[string]string{ | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| StMap: map[string]Base{ | |||||
| "a": { | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| "b": { | |||||
| Int: 2, | |||||
| Bool: false, | |||||
| String: "test2", | |||||
| Float: 2, | |||||
| }, | |||||
| }, | |||||
| MapMap: map[string]map[string]string{ | |||||
| "a": { | |||||
| "a": "1", | |||||
| "b": "2", | |||||
| }, | |||||
| "b": nil, | |||||
| }, | |||||
| Nil: nil, | |||||
| }, | |||||
| BaseIf: Base{ | |||||
| Int: 1, | |||||
| Bool: true, | |||||
| String: "test", | |||||
| Float: 1, | |||||
| }, | |||||
| NilPtr: nil, | |||||
| } | |||||
| var trace []string | |||||
| WalkValue(val, func(ctx *WalkContext, event WalkEvent) WalkingOp { | |||||
| switch e := event.(type) { | |||||
| case StructBeginEvent: | |||||
| trace = append(trace, "StructBeginEvent") | |||||
| case StructArriveFieldEvent: | |||||
| trace = append(trace, "StructFieldEvent", e.Info.Name) | |||||
| if isBaseDataType(e.Value) { | |||||
| trace = append(trace, toString(e.Value.Interface())) | |||||
| } | |||||
| case StructEndEvent: | |||||
| trace = append(trace, "StructEndEvent") | |||||
| case MapBeginEvent: | |||||
| trace = append(trace, "MapBeginEvent") | |||||
| case MapArriveEntryEvent: | |||||
| trace = append(trace, "MapEntryEvent", e.Key.String()) | |||||
| if isBaseDataType(e.Value) { | |||||
| trace = append(trace, toString(e.Value.Interface())) | |||||
| } | |||||
| case MapEndEvent: | |||||
| trace = append(trace, "MapEndEvent") | |||||
| case ArrayBeginEvent: | |||||
| trace = append(trace, "ArrayBeginEvent") | |||||
| case ArrayArriveElementEvent: | |||||
| trace = append(trace, "ArrayElementEvent", fmt.Sprintf("%d", e.Index)) | |||||
| if isBaseDataType(e.Value) { | |||||
| trace = append(trace, toString(e.Value.Interface())) | |||||
| } | |||||
| case ArrayEndEvent: | |||||
| trace = append(trace, "ArrayEndEvent") | |||||
| } | |||||
| return Next | |||||
| }) | |||||
| So(trace, ShouldResemble, []string{}) | |||||
| }) | |||||
| } | |||||
| @@ -0,0 +1,278 @@ | |||||
| package serder | |||||
| import ( | |||||
| "reflect" | |||||
| "github.com/zyedidia/generic/stack" | |||||
| ) | |||||
| type WalkEvent interface{} | |||||
| type StructBeginEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type StructArriveFieldEvent struct { | |||||
| Info reflect.StructField | |||||
| Value reflect.Value | |||||
| } | |||||
| type StructLeaveFieldEvent struct { | |||||
| Info reflect.StructField | |||||
| Value reflect.Value | |||||
| } | |||||
| type StructEndEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type ArrayBeginEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type ArrayArriveElementEvent struct { | |||||
| Index int | |||||
| Value reflect.Value | |||||
| } | |||||
| type ArrayLeaveElementEvent struct { | |||||
| Index int | |||||
| Value reflect.Value | |||||
| } | |||||
| type ArrayEndEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type MapBeginEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type MapArriveEntryEvent struct { | |||||
| Key reflect.Value | |||||
| Value reflect.Value | |||||
| } | |||||
| type MapLeaveEntryEvent struct { | |||||
| Key reflect.Value | |||||
| Value reflect.Value | |||||
| } | |||||
| type MapEndEvent struct { | |||||
| Value reflect.Value | |||||
| } | |||||
| type WalkingOp int | |||||
| const ( | |||||
| Next WalkingOp = iota | |||||
| Skip | |||||
| Stop | |||||
| ) | |||||
| type Walker func(ctx *WalkContext, event WalkEvent) WalkingOp | |||||
| type WalkContext struct { | |||||
| stack *stack.Stack[any] | |||||
| } | |||||
| func (c *WalkContext) StackPush(val any) { | |||||
| c.stack.Push(val) | |||||
| } | |||||
| func (c *WalkContext) StackPop() any { | |||||
| return c.stack.Pop() | |||||
| } | |||||
| func (c *WalkContext) StackPeek() any { | |||||
| return c.stack.Peek() | |||||
| } | |||||
| type WalkOption struct { | |||||
| StackValues []any | |||||
| } | |||||
| func WalkValue(value any, walker Walker, opts ...WalkOption) *WalkContext { | |||||
| var opt WalkOption | |||||
| if len(opts) > 0 { | |||||
| opt = opts[0] | |||||
| } | |||||
| ctx := &WalkContext{ | |||||
| stack: stack.New[any](), | |||||
| } | |||||
| for _, v := range opt.StackValues { | |||||
| ctx.StackPush(v) | |||||
| } | |||||
| doWalking(ctx, reflect.ValueOf(value), walker) | |||||
| return ctx | |||||
| } | |||||
| func doWalking(ctx *WalkContext, val reflect.Value, walker Walker) WalkingOp { | |||||
| if !WillWalkInto(val) { | |||||
| return Next | |||||
| } | |||||
| switch val.Kind() { | |||||
| case reflect.Array: | |||||
| fallthrough | |||||
| case reflect.Slice: | |||||
| if walker(ctx, ArrayBeginEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| for i := 0; i < val.Len(); i++ { | |||||
| eleVal := val.Index(i) | |||||
| op := walker(ctx, ArrayArriveElementEvent{ | |||||
| Index: i, | |||||
| Value: eleVal, | |||||
| }) | |||||
| if op == Skip { | |||||
| if walker(ctx, ArrayLeaveElementEvent{ | |||||
| Index: i, | |||||
| Value: eleVal, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| continue | |||||
| } | |||||
| if op == Stop { | |||||
| return Stop | |||||
| } | |||||
| if doWalking(ctx, eleVal, walker) == Stop { | |||||
| return Stop | |||||
| } | |||||
| if walker(ctx, ArrayLeaveElementEvent{ | |||||
| Index: i, | |||||
| Value: eleVal, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| } | |||||
| if walker(ctx, ArrayEndEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| case reflect.Map: | |||||
| if walker(ctx, MapBeginEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| keys := val.MapKeys() | |||||
| for _, key := range keys { | |||||
| val := val.MapIndex(key) | |||||
| op := walker(ctx, MapArriveEntryEvent{ | |||||
| Key: key, | |||||
| Value: val, | |||||
| }) | |||||
| if op == Skip { | |||||
| if walker(ctx, MapLeaveEntryEvent{ | |||||
| Key: key, | |||||
| Value: val, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| continue | |||||
| } | |||||
| if op == Stop { | |||||
| return Stop | |||||
| } | |||||
| if doWalking(ctx, val, walker) == Stop { | |||||
| return Stop | |||||
| } | |||||
| if walker(ctx, MapLeaveEntryEvent{ | |||||
| Key: key, | |||||
| Value: val, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| } | |||||
| if walker(ctx, MapEndEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| case reflect.Struct: | |||||
| if walker(ctx, StructBeginEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| for i := 0; i < val.NumField(); i++ { | |||||
| field := val.Field(i) | |||||
| op := walker(ctx, StructArriveFieldEvent{ | |||||
| Info: val.Type().Field(i), | |||||
| Value: field, | |||||
| }) | |||||
| if op == Skip { | |||||
| if walker(ctx, StructLeaveFieldEvent{ | |||||
| Info: val.Type().Field(i), | |||||
| Value: field, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| continue | |||||
| } | |||||
| if op == Stop { | |||||
| return Stop | |||||
| } | |||||
| if doWalking(ctx, field, walker) == Stop { | |||||
| return Stop | |||||
| } | |||||
| if walker(ctx, StructLeaveFieldEvent{ | |||||
| Info: val.Type().Field(i), | |||||
| Value: field, | |||||
| }) == Stop { | |||||
| return Stop | |||||
| } | |||||
| } | |||||
| if walker(ctx, StructEndEvent{Value: val}) == Stop { | |||||
| return Stop | |||||
| } | |||||
| case reflect.Interface: | |||||
| fallthrough | |||||
| case reflect.Pointer: | |||||
| eleVal := val.Elem() | |||||
| return doWalking(ctx, eleVal, walker) | |||||
| } | |||||
| return Next | |||||
| } | |||||
| const ( | |||||
| WillWalkIntoTypeKinds = (1 << reflect.Array) | (1 << reflect.Map) | (1 << reflect.Slice) | (1 << reflect.Struct) | |||||
| ) | |||||
| func WillWalkInto(val reflect.Value) bool { | |||||
| if val.IsZero() { | |||||
| return false | |||||
| } | |||||
| typ := val.Type() | |||||
| typeKind := typ.Kind() | |||||
| if typeKind == reflect.Interface || typeKind == reflect.Pointer { | |||||
| return WillWalkInto(val.Elem()) | |||||
| } | |||||
| return ((1 << typeKind) & WillWalkIntoTypeKinds) != 0 | |||||
| } | |||||