From 9ef4123f38ff22cb74efbe9b4f03b37a03116d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Sun, 23 Mar 2025 14:20:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84,=20=E5=A2=9E=E5=8A=A0=E6=99=AE=E9=80=9A=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=BD=93=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- abstract.go | 44 +++++++++++++++++ builder.go | 119 ++++++++-------------------------------------- builder_test.go | 58 ++++++++++++++++++---- dynamic_struct.go | 31 ++++++++++++ field_config.go | 28 +++++++++++ go.mod | 13 ++++- go.sum | 25 ++++++++++ 7 files changed, 210 insertions(+), 108 deletions(-) create mode 100644 abstract.go create mode 100644 dynamic_struct.go create mode 100644 field_config.go diff --git a/abstract.go b/abstract.go new file mode 100644 index 0000000..30f3af2 --- /dev/null +++ b/abstract.go @@ -0,0 +1,44 @@ +// Package dynamicstruct ... +// +// Description : dynamicstruct ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-03-23 13:34 +package dynamicstruct + +// IBuilder 运行时动态生成结构体的接口约束 +type IBuilder interface { + // AddField 添加结构体字段 + AddField(name string, pkg string, typ any, tag string, anonymous bool) IBuilder + // RemoveField 移除指定名称的结构体字段 + RemoveField(name string) IBuilder + // HasField 检测指定名称的结构体字段是否存在 + HasField(name string) bool + // GetField 根据名称获取结构体字段定义 + GetField(name string) IFieldConfig + // Build 返回动态定义的结构体. + Build() IDynamicStruct +} + +// IFieldConfig 结构体字段的定义. +type IFieldConfig interface { + // SetType 设置字段类型. + SetType(typ any) IFieldConfig + // SetTag 设置字段 tag. + SetTag(tag string) IFieldConfig +} + +// IDynamicStruct contains defined dynamic struct. +// This definition can't be changed anymore, once is built. +// It provides a method for creating new instances of same defintion. +type IDynamicStruct interface { + // New 获取结构体实例, 所有字段值均为对应类型的初始零值 + New() any + + // NewSliceOfStructs slice实例化 + NewSliceOfStructs() any + + // NewMapOfStructs map 或者 struct实例化 + NewMapOfStructs(key any) any +} diff --git a/builder.go b/builder.go index e2633bf..518ee96 100644 --- a/builder.go +++ b/builder.go @@ -7,73 +7,22 @@ import ( "strings" ) -type ( - // Builder 运行时动态生成结构体的接口约束 - Builder interface { - // AddField 添加结构体字段 - AddField(name string, pkg string, typ any, tag string, anonymous bool) Builder - // RemoveField 移除指定名称的结构体字段 - RemoveField(name string) Builder - // HasField 检测指定名称的结构体字段是否存在 - HasField(name string) bool - // GetField 根据名称获取结构体字段定义 - GetField(name string) FieldConfig - // Build 返回动态定义的结构体. - Build() DynamicStruct - } +type nestedStruct struct { + Field string + Builder IBuilder + Tag string +} - // FieldConfig 结构体字段的定义. - FieldConfig interface { - // SetType 设置字段类型. - SetType(typ any) FieldConfig - // SetTag 设置字段 tag. - SetTag(tag string) FieldConfig - } - - // DynamicStruct contains defined dynamic struct. - // This definition can't be changed anymore, once is built. - // It provides a method for creating new instances of same defintion. - DynamicStruct interface { - // New 获取结构体实例, 所有字段值均为对应类型的初始零值 - New() any - - // NewSliceOfStructs slice实例化 - NewSliceOfStructs() any - - // NewMapOfStructs map 或者 struct实例化 - NewMapOfStructs(key any) any - } - - nestedStruct struct { - Field string - Builder Builder - Tag string - } - - builderImpl struct { - fields []*fieldConfigImpl - nestedStructTable map[string]nestedStruct // 嵌套结构体的定义, 父级路径 => 父级路径下挂接的子路径 - maxFieldDeep int // 字段嵌套最大深度 - structTagTable map[string]string // 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag - } - - fieldConfigImpl struct { - name string - pkg string - typ any - tag string - anonymous bool - } - - dynamicStructImpl struct { - structFields []reflect.StructField - definition reflect.Type - } -) +type builderImpl struct { + fields []*fieldConfigImpl + nestedStructTable map[string]nestedStruct // 嵌套结构体的定义, 父级路径 => 父级路径下挂接的子路径 + maxFieldDeep int // 字段嵌套最大深度 + structTagTable map[string]string // 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag +} // NewStruct 获取builder实例 // 传入的tag映射表: 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag -func NewStruct(structTagTable map[string]string) Builder { +func NewStruct(structTagTable map[string]string) IBuilder { return &builderImpl{ fields: []*fieldConfigImpl{}, nestedStructTable: make(map[string]nestedStruct), @@ -82,12 +31,12 @@ func NewStruct(structTagTable map[string]string) Builder { } // ExtendStruct 基于已有结构体, 生成动态结构体(相当于继承指定的结构体属性) -func ExtendStruct(value ...any) Builder { +func ExtendStruct(value ...any) IBuilder { return MergeStructs(value...) } // MergeStructs 多个结构体合并成一个动态结构体 -func MergeStructs(values ...any) Builder { +func MergeStructs(values ...any) IBuilder { builder := NewStruct(map[string]string{}) for _, value := range values { @@ -111,8 +60,8 @@ func (b *builderImpl) parseNestedField(fieldName string) ([]string, bool) { } // AddField 添加结构体字段 -func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, anonymous bool) Builder { - // 瞎话线转驼峰, 传入的name实际对应序列化时的json tag +func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, anonymous bool) IBuilder { + // 下划线转驼峰, 传入的name实际对应序列化时的json tag // name = wrapper.String(name).SnakeCaseToCamel() if cfgTag, exist := b.structTagTable[name]; exist && len(cfgTag) > 0 { tag = cfgTag @@ -191,7 +140,7 @@ func (b *builderImpl) addNestedField(nameArr []string, pkg string, typ any, tag } // RemoveField 根据名称移除结构体字段 -func (b *builderImpl) RemoveField(name string) Builder { +func (b *builderImpl) RemoveField(name string) IBuilder { newFieldList := make([]*fieldConfigImpl, 0) for i := range b.fields { if b.fields[i].name == name { @@ -214,7 +163,7 @@ func (b *builderImpl) HasField(name string) bool { } // GetField 根据名称获取字段配置, 不存在, 返回nil -func (b *builderImpl) GetField(name string) FieldConfig { +func (b *builderImpl) GetField(name string) IFieldConfig { for i := range b.fields { if b.fields[i].name == name { return b.fields[i] @@ -224,7 +173,7 @@ func (b *builderImpl) GetField(name string) FieldConfig { } // Build 构建动态结构体 -func (b *builderImpl) Build() DynamicStruct { +func (b *builderImpl) Build() IDynamicStruct { // 按照嵌套深度, 进行一次回溯处理 for deep := b.maxFieldDeep - 1; deep > 0; deep-- { // 嵌套数据结构 @@ -281,7 +230,7 @@ func (b *builderImpl) Build() DynamicStruct { var structFields []reflect.StructField for _, field := range b.fields { if strings.Contains(field.name, ArraySplit) { - // TODO : 临时过滤, 后续需要在正确逻辑处正确移除 + // 去除数组路径 continue } structFields = append(structFields, reflect.StructField{ @@ -297,31 +246,3 @@ func (b *builderImpl) Build() DynamicStruct { definition: reflect.StructOf(structFields), } } - -// SetType 设置字段类型 -func (f *fieldConfigImpl) SetType(typ any) FieldConfig { - f.typ = typ - return f -} - -// SetTag 设置字段标签 -func (f *fieldConfigImpl) SetTag(tag string) FieldConfig { - f.tag = tag - return f -} - -// New 创建动态结构体实例 -func (ds *dynamicStructImpl) New() any { - // 不要指针,全部解引用 - return reflect.New(ds.definition).Interface() -} - -// NewSliceOfStructs 创建动态结构体切片实例 -func (ds *dynamicStructImpl) NewSliceOfStructs() any { - return reflect.New(reflect.SliceOf(ds.definition)).Interface() -} - -// NewMapOfStructs 创建动态结构体map实例 -func (ds *dynamicStructImpl) NewMapOfStructs(key any) any { - return reflect.New(reflect.MapOf(reflect.Indirect(reflect.ValueOf(key)).Type(), ds.definition)).Interface() -} diff --git a/builder_test.go b/builder_test.go index 3db1c17..790235d 100644 --- a/builder_test.go +++ b/builder_test.go @@ -9,19 +9,14 @@ package dynamicstruct import ( "encoding/json" - "fmt" "reflect" "testing" + + . "github.com/smartystreets/goconvey/convey" ) -func Test_dynamicStructImpl_New(t *testing.T) { +/*func Test_dynamicStructImpl_New(t *testing.T) { instance := NewStruct(map[string]string{}). - /*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).*/ AddField("user.base.age", "", 20, `json:"age"`, false). AddField("user.base.name", "", "", `json:"name"`, false). AddField("user.job.address", "", "", `json:"address"`, false). @@ -52,3 +47,50 @@ func Test_dynamicStructImpl_New(t *testing.T) { fmt.Println(string(valByte)) // fmt.Println(reflect.ValueOf(val).Elem().FieldByName("Integer").Interface()) } +*/ +// TestStruct : 测试结构体 +func TestStruct(t *testing.T) { + Convey("普通结构体,无嵌套", t, func() { + data := []byte(` + { + "age": 123, + "name": "example", + "double": 123.45, + "tag_map": "ttt", + "bool": true, + "slice": [1, 2, 3] + } + `) + instance := NewStruct(map[string]string{ + "test_tag_map": `json:"tag_map"`, + }). + AddField("age", "", 0, `json:"age"`, false). + AddField("name", "", "", `json:"name"`, false). + AddField("test_tag_map", "", "", "", false). + AddField("double", "", 0.0, `json:"double"`, false). + AddField("bool", "", false, "", false). + AddField("slice", "", []int{}, `json:"slice"`, false).Build() + val := instance.New() + err := json.Unmarshal(data, &val) + So(err, ShouldBeNil) + reflectType := reflect.TypeOf(val) + // 需要是结构体类型 + So(reflectType.Elem().Kind(), ShouldEqual, reflect.Struct) + // 验证数据 + // 数据序列化成功 + targetByte, targetErr := json.Marshal(val) + So(targetErr, ShouldBeNil) + // 数据转map成功 + var res map[string]any + targetUnmarshalErr := json.Unmarshal(targetByte, &res) + So(targetUnmarshalErr, ShouldBeNil) + // 验证数据 + So(len(res), ShouldEqual, 6) + So(res["age"], ShouldEqual, float64(123)) + So(res["name"], ShouldEqual, "example") + So(res["tag_map"], ShouldEqual, "ttt") + So(res["double"], ShouldEqual, 123.45) + So(res["bool"], ShouldEqual, true) + So(res["slice"], ShouldEqual, []any{float64(1), float64(2), float64(3)}) + }) +} diff --git a/dynamic_struct.go b/dynamic_struct.go new file mode 100644 index 0000000..fcf16e4 --- /dev/null +++ b/dynamic_struct.go @@ -0,0 +1,31 @@ +// Package dynamicstruct ... +// +// Description : dynamicstruct ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-03-23 14:14 +package dynamicstruct + +import "reflect" + +type dynamicStructImpl struct { + structFields []reflect.StructField + definition reflect.Type +} + +// New 创建动态结构体实例 +func (ds *dynamicStructImpl) New() any { + // 不要指针,全部解引用 + return reflect.New(ds.definition).Interface() +} + +// NewSliceOfStructs 创建动态结构体切片实例 +func (ds *dynamicStructImpl) NewSliceOfStructs() any { + return reflect.New(reflect.SliceOf(ds.definition)).Interface() +} + +// NewMapOfStructs 创建动态结构体map实例 +func (ds *dynamicStructImpl) NewMapOfStructs(key any) any { + return reflect.New(reflect.MapOf(reflect.Indirect(reflect.ValueOf(key)).Type(), ds.definition)).Interface() +} diff --git a/field_config.go b/field_config.go new file mode 100644 index 0000000..8d59aaf --- /dev/null +++ b/field_config.go @@ -0,0 +1,28 @@ +// Package dynamicstruct ... +// +// Description : dynamicstruct ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2025-03-23 14:07 +package dynamicstruct + +type fieldConfigImpl struct { + name string + pkg string + typ any + tag string + anonymous bool +} + +// SetType 设置字段类型 +func (f *fieldConfigImpl) SetType(typ any) IFieldConfig { + f.typ = typ + return f +} + +// SetTag 设置字段标签 +func (f *fieldConfigImpl) SetTag(tag string) IFieldConfig { + f.tag = tag + return f +} diff --git a/go.mod b/go.mod index b027de9..56a61c9 100644 --- a/go.mod +++ b/go.mod @@ -2,18 +2,29 @@ module git.zhangdeman.cn/zhangdeman/dynamic-struct go 1.24.1 +require ( + git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436 + github.com/smartystreets/goconvey v1.8.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/gopherjs/gopherjs v1.17.2 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/smarty/assertions v1.15.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 + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 443b25d..21b18eb 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,26 @@ 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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= @@ -23,6 +39,15 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT 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= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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=