diff --git a/builder.go b/builder.go index 95a44ff..f5147ba 100644 --- a/builder.go +++ b/builder.go @@ -1,6 +1,11 @@ package dynamicstruct -import "reflect" +import ( + "fmt" + "git.zhangdeman.cn/zhangdeman/wrapper" + "reflect" + "strings" +) type ( // Builder 运行时动态生成结构体的接口约束 @@ -39,8 +44,17 @@ type ( NewMapOfStructs(key any) any } + nestedStruct struct { + Field string + Builder Builder + Tag string + } + builderImpl struct { - fields []*fieldConfigImpl + fields []*fieldConfigImpl + nestedStructTable map[string]nestedStruct // 嵌套结构体的定义, 父级路径 => 父级路径下挂接的子路径 + maxFieldDeep int // 字段嵌套最大深度 + structTagTable map[string]string // 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag } fieldConfigImpl struct { @@ -52,14 +66,18 @@ type ( } dynamicStructImpl struct { - definition reflect.Type + structFields []reflect.StructField + definition reflect.Type } ) // NewStruct 获取builder实例 -func NewStruct() Builder { +// 传入的tag映射表: 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag +func NewStruct(structTagTable map[string]string) Builder { return &builderImpl{ - fields: []*fieldConfigImpl{}, + fields: []*fieldConfigImpl{}, + nestedStructTable: make(map[string]nestedStruct), + structTagTable: structTagTable, } } @@ -70,7 +88,7 @@ func ExtendStruct(value ...any) Builder { // MergeStructs 多个结构体合并成一个动态结构体 func MergeStructs(values ...any) Builder { - builder := NewStruct() + builder := NewStruct(map[string]string{}) for _, value := range values { valueOf := reflect.Indirect(reflect.ValueOf(value)) @@ -86,14 +104,44 @@ func MergeStructs(values ...any) Builder { return builder } +// IsNestedField 判断是否是嵌套结构体 +func (b *builderImpl) parseNestedField(fieldName string) ([]string, bool) { + fieldNameArr := strings.Split(strings.Trim(fieldName, "."), ".") + return fieldNameArr, len(fieldNameArr) > 1 +} + // AddField 添加结构体字段 func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, anonymous bool) Builder { + // 瞎话线转驼峰, 传入的name实际对应序列化时的json tag + // name = wrapper.String(name).SnakeCaseToCamel() + if cfgTag, exist := b.structTagTable[name]; exist && len(cfgTag) > 0 { + tag = cfgTag + } else { + if len(tag) == 0 { + tag = fmt.Sprintf(`json:"%s"`, name) + } + } + // 判断是否嵌套结构体 + fieldNameArr, isNestedField := b.parseNestedField(name) + if !isNestedField { + // 普通字段 + b.addNormalField(name, pkg, typ, tag, anonymous) + return b + } + // 添加嵌套的结构体 + b.addNestedField(fieldNameArr, pkg, typ, tag, anonymous) + return b +} + +// addNormalField 添加普通无嵌套的字段 +func (b *builderImpl) addNormalField(name string, pkg string, typ any, tag string, anonymous bool) { + name = wrapper.String(name).SnakeCaseToCamel() if existFieldCfg := b.GetField(name); nil != existFieldCfg { // 说明已存在指定名称字段 // 重复添加, 则会议后面的标签以及类型, 覆盖前面的值 existFieldCfg.SetTag(tag) existFieldCfg.SetType(typ) - return b + return } b.fields = append(b.fields, &fieldConfigImpl{ name: name, @@ -102,7 +150,44 @@ func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, ano anonymous: anonymous, pkg: pkg, }) - return b +} + +// addNestedField 添加嵌套字段 +func (b *builderImpl) addNestedField(nameArr []string, pkg string, typ any, tag string, anonymous bool) { + if len(nameArr) == 1 { + // 说明已经是最顶层结构了 + b.addNormalField(nameArr[0], pkg, typ, tag, anonymous) + return + } + if len(nameArr) > b.maxFieldDeep { + // 设置字段嵌套的最大深度, 由于生成结构体确认深度使用 + b.maxFieldDeep = len(nameArr) + } + for i := len(nameArr) - 1; i > 0; i-- { + jsonTag := nameArr[i] + fieldName := wrapper.String(jsonTag).SnakeCaseToCamel() + parentName := strings.Join(nameArr[:i], ".") + parentJsonTag := nameArr[i-1] + parentFieldName := wrapper.String(parentJsonTag).SnakeCaseToCamel() + fieldTag := fmt.Sprintf(`json:"%s"`, parentJsonTag) + if tagCfg, exist := b.structTagTable[parentName]; exist && len(tagCfg) > 0 { + fieldTag = tagCfg + } + if len(parentName) > 0 { + if _, exist := b.nestedStructTable[parentName]; !exist { + b.nestedStructTable[parentName] = nestedStruct{ + Field: parentFieldName, + Builder: NewStruct(b.structTagTable), + Tag: fieldTag, + } + } + } + if i == len(nameArr)-1 { + // 最深层此字段, 直接追加到他的父级结构体中即可 + b.nestedStructTable[parentName].Builder.AddField(fieldName, pkg, typ, tag, anonymous) + continue + } + } } // RemoveField 根据名称移除结构体字段 @@ -140,8 +225,26 @@ func (b *builderImpl) GetField(name string) FieldConfig { // Build 构建动态结构体 func (b *builderImpl) Build() DynamicStruct { + // 按照嵌套深度, 进行一次回溯处理 + for deep := b.maxFieldDeep - 1; deep > 0; deep-- { + // 嵌套数据结构 + for parentIndex, builderCfg := range b.nestedStructTable { + parentNameArr := strings.Split(parentIndex, ".") + if len(parentNameArr) != deep { + // 从深度最深处向上处理 + continue + } + if deep == 1 { + // 说明是顶层了 + b.AddField(builderCfg.Field, "", builderCfg.Builder.Build().New(), builderCfg.Tag, false) + } else { + // (非顶层) 父级结构存在, 将其追加到父级结构中即可, 向前看一步即为父级结构 + b.nestedStructTable[strings.Join(parentNameArr[:len(parentNameArr)-1], ".")].Builder.AddField(builderCfg.Field, "", builderCfg.Builder.Build().New(), builderCfg.Tag, false) + } + } + } + // 一级字段属性 var structFields []reflect.StructField - for _, field := range b.fields { structFields = append(structFields, reflect.StructField{ Name: field.name, @@ -151,9 +254,9 @@ func (b *builderImpl) Build() DynamicStruct { Anonymous: field.anonymous, }) } - return &dynamicStructImpl{ - definition: reflect.StructOf(structFields), + structFields: structFields, + definition: reflect.StructOf(structFields), } } diff --git a/builder_test.go b/builder_test.go index 73bc22b..66274e6 100644 --- a/builder_test.go +++ b/builder_test.go @@ -1,6 +1,6 @@ // Package dynamicstruct ... // -// Description : dynamicstruct ... +// Description : dynamic struct ... // // Author : go_developer@163.com<白茶清欢> // @@ -10,20 +10,24 @@ package dynamicstruct import ( "encoding/json" "fmt" - "reflect" "testing" ) func Test_dynamicStructImpl_New(t *testing.T) { instance := NewStruct(). - AddField("Integer", "", 0, `json:"int"`, false). + /*AddField("Integer", "", 0, `json:"int"`, false). AddField("Text", "", "", `json:"someText"`, false). AddField("Float", "", 0.0, `json:"double"`, false). AddField("Boolean", "", false, "", false). AddField("Slice", "", []int{}, "", false). - AddField("Anonymous", "", "", `json:"-"`, false). - Build(). - New() + AddField("Anonymous", "", "", `json:"-"`, false).*/ + AddField("user.base.age", "", 20, `json:"age"`, false). + AddField("user.base.name", "", "", `json:"name"`, false). + AddField("user.job.address", "", "", `json:"address"`, false). + AddField("user.job.company.name", "", "", `json:"name"`, false). + Build() + + val := instance.New() data := []byte(` { @@ -32,10 +36,13 @@ func Test_dynamicStructImpl_New(t *testing.T) { "double": 123.45, "Boolean": true, "Slice": [1, 2, 3], + "user": {"job":{"address":"beijing","company":{"name":"unknown"}}, "base":{"age": 1800, "name":"baicha"}}, "Anonymous": "avoid to read" } `) - err := json.Unmarshal(data, &instance) - fmt.Println(err) - fmt.Println(reflect.ValueOf(instance).Elem().FieldByName("Integer").Interface()) + err := json.Unmarshal(data, &val) + fmt.Println(err, val) + valByte, _ := json.Marshal(val) + fmt.Println(string(valByte)) + // fmt.Println(reflect.ValueOf(val).Elem().FieldByName("Integer").Interface()) } diff --git a/go.mod b/go.mod index 138da8a..b027de9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,19 @@ module git.zhangdeman.cn/zhangdeman/dynamic-struct go 1.24.1 + +require ( + git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8 // indirect + git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 // indirect + git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd // indirect + git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e // indirect + git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..443b25d --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8 h1:VEifPc+vkpEQoX9rj7zxmT1m+IA81XjOxe7+Z1aqWNM= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= +git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y= +git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= +git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI= +git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd/go.mod h1:+D6uPSljwHywjVY5WSBY4TRVMj26TN5f5cFGEYMldjs= +git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e h1:Q973S6CcWr1ICZhFI1STFOJ+KUImCl2BaIXm6YppBqI= +git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e/go.mod h1:VpPjBlwz8U+OxZuxzHQBv1aEEZ3pStH6bZvT21ADEbI= +git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436 h1:SM4zc54W2wmM72+4pMNQ8iS371H6lj4J8rj8KJKf7pw= +git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436/go.mod h1:YJ1FlvFgkfAHkxkt3l5rKKUqEpQkNMbCFDzDmgteEU8= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=