修复嵌套结构体 + 嵌套数组的BUG #3
							
								
								
									
										48
									
								
								abstract.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								abstract.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | // 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 | ||||||
|  | 	// GetType 获取类型 | ||||||
|  | 	GetType() any | ||||||
|  | 	// GetTag 获取tag | ||||||
|  | 	GetTag() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								builder.go
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								builder.go
									
									
									
									
									
								
							| @ -7,73 +7,22 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ( | type nestedStruct struct { | ||||||
| 	// 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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// 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 | 	Field   string | ||||||
| 		Builder Builder | 	Builder IBuilder | ||||||
| 	Tag     string | 	Tag     string | ||||||
| 	} | } | ||||||
|  |  | ||||||
| 	builderImpl struct { | type builderImpl struct { | ||||||
| 	fields            []*fieldConfigImpl | 	fields            []*fieldConfigImpl | ||||||
| 	nestedStructTable map[string]nestedStruct // 嵌套结构体的定义, 父级路径 => 父级路径下挂接的子路径 | 	nestedStructTable map[string]nestedStruct // 嵌套结构体的定义, 父级路径 => 父级路径下挂接的子路径 | ||||||
| 	maxFieldDeep      int                     // 字段嵌套最大深度 | 	maxFieldDeep      int                     // 字段嵌套最大深度 | ||||||
| 	structTagTable    map[string]string       // 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag | 	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 |  | ||||||
| 	} |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // NewStruct 获取builder实例 | // NewStruct 获取builder实例 | ||||||
| // 传入的tag映射表: 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag | // 传入的tag映射表: 字段路径 => json tag,最高优先级, 没传的时候会使用AddField的tag, 也为空使用手搓json tag | ||||||
| func NewStruct(structTagTable map[string]string) Builder { | func NewStruct(structTagTable map[string]string) IBuilder { | ||||||
| 	return &builderImpl{ | 	return &builderImpl{ | ||||||
| 		fields:            []*fieldConfigImpl{}, | 		fields:            []*fieldConfigImpl{}, | ||||||
| 		nestedStructTable: make(map[string]nestedStruct), | 		nestedStructTable: make(map[string]nestedStruct), | ||||||
| @ -82,12 +31,12 @@ func NewStruct(structTagTable map[string]string) Builder { | |||||||
| } | } | ||||||
|  |  | ||||||
| // ExtendStruct 基于已有结构体, 生成动态结构体(相当于继承指定的结构体属性) | // ExtendStruct 基于已有结构体, 生成动态结构体(相当于继承指定的结构体属性) | ||||||
| func ExtendStruct(value ...any) Builder { | func ExtendStruct(value ...any) IBuilder { | ||||||
| 	return MergeStructs(value...) | 	return MergeStructs(value...) | ||||||
| } | } | ||||||
|  |  | ||||||
| // MergeStructs 多个结构体合并成一个动态结构体 | // MergeStructs 多个结构体合并成一个动态结构体 | ||||||
| func MergeStructs(values ...any) Builder { | func MergeStructs(values ...any) IBuilder { | ||||||
| 	builder := NewStruct(map[string]string{}) | 	builder := NewStruct(map[string]string{}) | ||||||
|  |  | ||||||
| 	for _, value := range values { | 	for _, value := range values { | ||||||
| @ -111,8 +60,8 @@ func (b *builderImpl) parseNestedField(fieldName string) ([]string, bool) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // AddField 添加结构体字段 | // AddField 添加结构体字段 | ||||||
| func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, anonymous bool) Builder { | func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, anonymous bool) IBuilder { | ||||||
| 	// 瞎话线转驼峰, 传入的name实际对应序列化时的json tag | 	// 下划线转驼峰, 传入的name实际对应序列化时的json tag | ||||||
| 	// name = wrapper.String(name).SnakeCaseToCamel() | 	// name = wrapper.String(name).SnakeCaseToCamel() | ||||||
| 	if cfgTag, exist := b.structTagTable[name]; exist && len(cfgTag) > 0 { | 	if cfgTag, exist := b.structTagTable[name]; exist && len(cfgTag) > 0 { | ||||||
| 		tag = cfgTag | 		tag = cfgTag | ||||||
| @ -191,7 +140,7 @@ func (b *builderImpl) addNestedField(nameArr []string, pkg string, typ any, tag | |||||||
| } | } | ||||||
|  |  | ||||||
| // RemoveField 根据名称移除结构体字段 | // RemoveField 根据名称移除结构体字段 | ||||||
| func (b *builderImpl) RemoveField(name string) Builder { | func (b *builderImpl) RemoveField(name string) IBuilder { | ||||||
| 	newFieldList := make([]*fieldConfigImpl, 0) | 	newFieldList := make([]*fieldConfigImpl, 0) | ||||||
| 	for i := range b.fields { | 	for i := range b.fields { | ||||||
| 		if b.fields[i].name == name { | 		if b.fields[i].name == name { | ||||||
| @ -214,7 +163,7 @@ func (b *builderImpl) HasField(name string) bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetField 根据名称获取字段配置, 不存在, 返回nil | // GetField 根据名称获取字段配置, 不存在, 返回nil | ||||||
| func (b *builderImpl) GetField(name string) FieldConfig { | func (b *builderImpl) GetField(name string) IFieldConfig { | ||||||
| 	for i := range b.fields { | 	for i := range b.fields { | ||||||
| 		if b.fields[i].name == name { | 		if b.fields[i].name == name { | ||||||
| 			return b.fields[i] | 			return b.fields[i] | ||||||
| @ -224,7 +173,7 @@ func (b *builderImpl) GetField(name string) FieldConfig { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Build 构建动态结构体 | // Build 构建动态结构体 | ||||||
| func (b *builderImpl) Build() DynamicStruct { | func (b *builderImpl) Build() IDynamicStruct { | ||||||
| 	// 按照嵌套深度, 进行一次回溯处理 | 	// 按照嵌套深度, 进行一次回溯处理 | ||||||
| 	for deep := b.maxFieldDeep - 1; deep > 0; deep-- { | 	for deep := b.maxFieldDeep - 1; deep > 0; deep-- { | ||||||
| 		// 嵌套数据结构 | 		// 嵌套数据结构 | ||||||
| @ -240,7 +189,7 @@ func (b *builderImpl) Build() DynamicStruct { | |||||||
| 			} else { | 			} else { | ||||||
| 				// 处理数组 | 				// 处理数组 | ||||||
| 				arrDeep := 0 | 				arrDeep := 0 | ||||||
| 				newParentNameArr := []string{} | 				var newParentNameArr []string | ||||||
| 				for i := len(parentNameArr) - 1; i >= 0; i-- { | 				for i := len(parentNameArr) - 1; i >= 0; i-- { | ||||||
| 					if parentNameArr[i] != ArraySplit { | 					if parentNameArr[i] != ArraySplit { | ||||||
| 						newParentNameArr = parentNameArr[:i+1] | 						newParentNameArr = parentNameArr[:i+1] | ||||||
| @ -257,18 +206,19 @@ func (b *builderImpl) Build() DynamicStruct { | |||||||
| 						val = reflect.New(reflect.SliceOf(reflect.TypeOf(val))).Interface() | 						val = reflect.New(reflect.SliceOf(reflect.TypeOf(val))).Interface() | ||||||
| 					} | 					} | ||||||
| 					// 解除指针引用 | 					// 解除指针引用 | ||||||
| 					val = reflect.ValueOf(val).Elem().Interface() | 					// val = reflect.ValueOf(val).Elem().Interface() | ||||||
| 					newParamIndex := strings.Join(newParentNameArr, ".") | 					newParamIndex := strings.Join(newParentNameArr, ".") | ||||||
| 					isTopIndex := len(newParentNameArr) == 1 | 					isTopIndex := len(newParentNameArr) == 1 | ||||||
| 					if isTopIndex { | 					if isTopIndex { | ||||||
| 						// 顶层结构, 数组类型不存在还有其他属性情况, 直接追加字段, 并移除嵌套表里的定义 | 						// 顶层结构, 数组类型不存在还有其他属性情况, 直接追加字段, 并移除嵌套表里的定义 | ||||||
| 						b.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | 						b.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | ||||||
| 						delete(b.nestedStructTable, newParamIndex) |  | ||||||
| 						// b.nestedStructTable[newParamIndex].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | 						// b.nestedStructTable[newParamIndex].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | ||||||
| 					} else { | 					} else { | ||||||
| 						// 非顶层结构, 再上探一级 | 						// 非顶层结构, 再上探一级 | ||||||
| 						b.nestedStructTable[strings.Join(newParentNameArr[:len(newParentNameArr)-1], ".")].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | 						b.nestedStructTable[strings.Join(newParentNameArr[:len(newParentNameArr)-1], ".")].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false) | ||||||
| 					} | 					} | ||||||
|  | 					// 嵌套结构中删除数组字段 | ||||||
|  | 					delete(b.nestedStructTable, newParamIndex) | ||||||
| 				} else { | 				} else { | ||||||
| 					// 非数组 | 					// 非数组 | ||||||
| 					// (非顶层) 父级结构存在, 将其追加到父级结构中即可, 向前看一步即为父级结构 | 					// (非顶层) 父级结构存在, 将其追加到父级结构中即可, 向前看一步即为父级结构 | ||||||
| @ -281,7 +231,7 @@ func (b *builderImpl) Build() DynamicStruct { | |||||||
| 	var structFields []reflect.StructField | 	var structFields []reflect.StructField | ||||||
| 	for _, field := range b.fields { | 	for _, field := range b.fields { | ||||||
| 		if strings.Contains(field.name, ArraySplit) { | 		if strings.Contains(field.name, ArraySplit) { | ||||||
| 			// TODO : 临时过滤, 后续需要在正确逻辑处正确移除 | 			// 去除数组路径 | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		structFields = append(structFields, reflect.StructField{ | 		structFields = append(structFields, reflect.StructField{ | ||||||
| @ -297,31 +247,3 @@ func (b *builderImpl) Build() DynamicStruct { | |||||||
| 		definition:   reflect.StructOf(structFields), | 		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() |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										187
									
								
								builder_test.go
									
									
									
									
									
								
							
							
						
						
									
										187
									
								
								builder_test.go
									
									
									
									
									
								
							| @ -9,19 +9,14 @@ package dynamicstruct | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" |  | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"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{}). | 	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.age", "", 20, `json:"age"`, false). | ||||||
| 		AddField("user.base.name", "", "", `json:"name"`, false). | 		AddField("user.base.name", "", "", `json:"name"`, false). | ||||||
| 		AddField("user.job.address", "", "", `json:"address"`, false). | 		AddField("user.job.address", "", "", `json:"address"`, false). | ||||||
| @ -52,3 +47,179 @@ func Test_dynamicStructImpl_New(t *testing.T) { | |||||||
| 	fmt.Println(string(valByte)) | 	fmt.Println(string(valByte)) | ||||||
| 	// fmt.Println(reflect.ValueOf(val).Elem().FieldByName("Integer").Interface()) | 	// 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)}) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestNestedStruct : 测试嵌套结构体 | ||||||
|  | func TestNestedStruct(t *testing.T) { | ||||||
|  | 	Convey("嵌套结构体", t, func() { | ||||||
|  | 		data := []byte(` | ||||||
|  | 		{ | ||||||
|  | "user": {"info": { | ||||||
|  | 			"age": 123, | ||||||
|  | 			"name": "example", | ||||||
|  | 			"double": 123.45, | ||||||
|  | 			"bool": true, | ||||||
|  | 			"slice": [1, 2, 3] | ||||||
|  | 		}}} | ||||||
|  | 		`) | ||||||
|  | 		instance := NewStruct(map[string]string{ | ||||||
|  | 			"test_tag_map": `json:"tag_map"`, | ||||||
|  | 		}). | ||||||
|  | 			AddField("user.info.age", "", 0, `json:"age"`, false). | ||||||
|  | 			AddField("user.info.name", "", "", `json:"name"`, 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, 1) | ||||||
|  | 		So(res["user"], ShouldNotBeEmpty) | ||||||
|  | 		userMap, ok := res["user"].(map[string]any) | ||||||
|  | 		So(ok, ShouldBeTrue) | ||||||
|  | 		So(len(userMap), ShouldEqual, 1) | ||||||
|  | 		userInfoMap, ok := userMap["info"].(map[string]any) | ||||||
|  | 		So(ok, ShouldBeTrue) | ||||||
|  | 		So(userInfoMap["age"], ShouldEqual, float64(123)) | ||||||
|  | 		So(userInfoMap["name"], ShouldEqual, "example") | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestStruct : 测试数组 | ||||||
|  | func TestArray(t *testing.T) { | ||||||
|  | 	Convey("嵌套结构体带多级数组", t, func() { | ||||||
|  | 		data := []byte(` | ||||||
|  | 		{ | ||||||
|  | "user": {"list":[[[{ | ||||||
|  | 			"age": 123, | ||||||
|  | 			"name": "example", | ||||||
|  | 			"double": 123.45, | ||||||
|  | 			"bool": true, | ||||||
|  | 			"slice": [1, 2, 3] | ||||||
|  | 		}]]]} | ||||||
|  | } | ||||||
|  | 		`) | ||||||
|  | 		instance := NewStruct(map[string]string{ | ||||||
|  | 			"test_tag_map": `json:"tag_map"`, | ||||||
|  | 		}). | ||||||
|  | 			AddField("user.list.[].[].[].age", "", 0, `json:"age"`, false). | ||||||
|  | 			AddField("user.list.[].[].[].name", "", "", `json:"name"`, 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]map[string][][][]any | ||||||
|  | 		targetUnmarshalErr := json.Unmarshal(targetByte, &res) | ||||||
|  | 		So(targetUnmarshalErr, ShouldBeNil) | ||||||
|  | 		So(len(res), ShouldEqual, 1) | ||||||
|  | 		So(res["user"], ShouldNotBeEmpty) | ||||||
|  | 		So(len(res["user"]), ShouldEqual, 1) | ||||||
|  | 		So(len(res["user"]["list"][0]), ShouldEqual, 1) | ||||||
|  | 		So(len(res["user"]["list"][0][0]), ShouldEqual, 1) | ||||||
|  | 		userMap, ok := res["user"]["list"][0][0][0].(map[string]any) | ||||||
|  | 		So(ok, ShouldBeTrue) | ||||||
|  | 		So(len(userMap), ShouldEqual, 2) | ||||||
|  | 		So(userMap["age"], ShouldEqual, float64(123)) | ||||||
|  | 		So(userMap["name"], ShouldEqual, "example") | ||||||
|  | 		So(userMap["double"], ShouldBeNil) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestFieldManage(t *testing.T) { | ||||||
|  | 	Convey("结构体字段管理", t, func() { | ||||||
|  | 		instance := NewStruct(map[string]string{ | ||||||
|  | 			"test_tag_map": `json:"tag_map"`, | ||||||
|  | 		}).AddField("age", "", 0, `json:"age"`, false). | ||||||
|  | 			AddField("name", "", "", `json:"name"`, false) | ||||||
|  | 		So(instance.HasField("Name"), ShouldBeTrue) | ||||||
|  | 		So(instance.GetField("Name"), ShouldNotBeNil) | ||||||
|  | 		instance.RemoveField("Name") | ||||||
|  | 		So(instance.HasField("Name"), ShouldBeFalse) | ||||||
|  | 		So(instance.GetField("Name"), ShouldBeNil) | ||||||
|  | 		So(instance.HasField("Age"), ShouldBeTrue) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestExtendStruct(t *testing.T) { | ||||||
|  | 	Convey("结构体字段继承", t, func() { | ||||||
|  | 		type User struct { | ||||||
|  | 			Name string `json:"name"` | ||||||
|  | 			Age  int    `json:"age"` | ||||||
|  | 		} | ||||||
|  | 		type Company struct { | ||||||
|  | 			Address string  `json:"address"` | ||||||
|  | 			Logo    string  `json:"logo"` | ||||||
|  | 			Age     float64 `json:"company_age"` // 应该覆盖掉 User.Age | ||||||
|  | 		} | ||||||
|  | 		instance := ExtendStruct(User{}, Company{}) | ||||||
|  | 		So(instance.HasField("Name"), ShouldBeTrue) | ||||||
|  | 		So(instance.HasField("Age"), ShouldBeTrue) | ||||||
|  | 		So(instance.HasField("Address"), ShouldBeTrue) | ||||||
|  | 		So(instance.HasField("Logo"), ShouldBeTrue) | ||||||
|  | 		So(reflect.TypeOf(instance.GetField("Age").GetType()).Kind(), ShouldEqual, reflect.Float64) | ||||||
|  | 		So(instance.GetField("Age").GetTag(), ShouldEqual, `json:"company_age"`) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										31
									
								
								dynamic_struct.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								dynamic_struct.go
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								field_config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								field_config.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | // 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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (f *fieldConfigImpl) GetType() any { | ||||||
|  | 	return f.typ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetTag 设置字段标签 | ||||||
|  | func (f *fieldConfigImpl) SetTag(tag string) IFieldConfig { | ||||||
|  | 	f.tag = tag | ||||||
|  | 	return f | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (f *fieldConfigImpl) GetTag() string { | ||||||
|  | 	return f.tag | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,18 +2,29 @@ module git.zhangdeman.cn/zhangdeman/dynamic-struct | |||||||
|  |  | ||||||
| go 1.24.1 | go 1.24.1 | ||||||
|  |  | ||||||
|  | require ( | ||||||
|  | 	git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436 | ||||||
|  | 	github.com/smartystreets/goconvey v1.8.1 | ||||||
|  | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8 // indirect | 	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/op_type v0.0.0-20240122104027-4928421213c0 // indirect | ||||||
| 	git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd // 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/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/BurntSushi/toml v1.5.0 // indirect | ||||||
| 	github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect | 	github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect | ||||||
| 	github.com/go-ini/ini v1.67.0 // 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/spaolacci/murmur3 v1.1.0 // indirect | ||||||
| 	github.com/tidwall/gjson v1.18.0 // indirect | 	github.com/tidwall/gjson v1.18.0 // indirect | ||||||
| 	github.com/tidwall/match v1.1.1 // indirect | 	github.com/tidwall/match v1.1.1 // indirect | ||||||
| 	github.com/tidwall/pretty v1.2.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 | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								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/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 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ= | ||||||
| github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg= | 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 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= | ||||||
| github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= | 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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= | ||||||
| github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= | 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 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= | ||||||
| github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= | 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 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.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= | ||||||
| github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= | ||||||
| github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= | 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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user