diff --git a/utils/serder/any_to_any.go b/utils/serder/any_to_any.go index 9258488..a4eba9e 100644 --- a/utils/serder/any_to_any.go +++ b/utils/serder/any_to_any.go @@ -13,6 +13,9 @@ type AnyToAnyOption struct { NoFromAny bool // 不判断目的字段是否实现了FromAny接口 NoToAny bool // 不判断源字段是否实现了ToAny接口 Converters []Converter // 字段类型转换函数 + // 当目的类型为map[string]any,是否要递归的将源类型的每一个字段都变成map[string]any。 + // 注:字段的类型(而不是实际值的类型)必须为结构体或者结构体指针。 + RecursiveStructToMap bool } type FromAny interface { @@ -43,6 +46,10 @@ func AnyToAny(src any, dst any, opts ...AnyToAnyOption) error { hooks = append(hooks, c) } + if opt.RecursiveStructToMap { + hooks = append(hooks, mp.RecursiveStructToMapHookFunc()) + } + config := &mp.DecoderConfig{ TagName: "json", Squash: true, diff --git a/utils/serder/serder.go b/utils/serder/serder.go index 09d2abb..7efab53 100644 --- a/utils/serder/serder.go +++ b/utils/serder/serder.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "reflect" + "strings" ) 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) { - 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) { diff --git a/utils/serder/serder_test.go b/utils/serder/serder_test.go index 27d92a1..39558aa 100644 --- a/utils/serder/serder_test.go +++ b/utils/serder/serder_test.go @@ -77,7 +77,7 @@ func (a DirToAnySt) ToAny(typ reflect.Type) (val any, ok bool, err error) { return nil, false, nil } -func Test_MapToObject(t *testing.T) { +func Test_AnyToAny(t *testing.T) { Convey("包含用字符串保存的int数据", t, func() { type Struct struct { 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)) + }) +} diff --git a/utils/serder/walk_test.go b/utils/serder/walk_test.go new file mode 100644 index 0000000..91f0671 --- /dev/null +++ b/utils/serder/walk_test.go @@ -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{}) + }) +} diff --git a/utils/serder/walker.go b/utils/serder/walker.go new file mode 100644 index 0000000..4a19b36 --- /dev/null +++ b/utils/serder/walker.go @@ -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 +}