From 8d20db12d29a1da06d3c47d55f9d15a1ca6f2c22 Mon Sep 17 00:00:00 2001 From: liuxiaobo <1224730913@qq.com> Date: Wed, 4 Jun 2025 22:59:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0struct=E4=B8=8Emap=E4=BA=92?= =?UTF-8?q?=E7=9B=B8=E8=BD=AC=E6=8D=A2=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mapstruct/hooks.go | 31 +++++++------ mapstruct/map2struct.go | 78 +++++++++++++++++++++++++------- mapstruct/mapstruct.go | 58 ++++++++++++------------ mapstruct/mapstruct_test.go | 12 ++++- mapstruct/options.go | 88 ++++++++++++++++++++++--------------- 5 files changed, 169 insertions(+), 98 deletions(-) diff --git a/mapstruct/hooks.go b/mapstruct/hooks.go index 321dd18..6617d6a 100644 --- a/mapstruct/hooks.go +++ b/mapstruct/hooks.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "strconv" - "strings" ) // applyStructToMapHook applies hooks for struct-to-map conversion. @@ -65,18 +64,18 @@ func applyMapToStructHook(value string, field reflect.StructField, opts *Options } } -// getFieldName extracts the field name from struct tags. -func getFieldName(field reflect.StructField, tagName string) (string, bool) { - if tag, ok := field.Tag.Lookup(tagName); ok { - if tag == "-" { - return "", true - } - if commaIdx := strings.Index(tag, ","); commaIdx != -1 { - tag = tag[:commaIdx] - } - if tag != "" { - return tag, false - } - } - return field.Name, false -} +//// getFieldName extracts the field name from struct tags. +//func getFieldName(field reflect.StructField, tagName string) (string, bool) { +// if tag, ok := field.Tag.Lookup(tagName); ok { +// if tag == "-" { +// return "", true +// } +// if commaIdx := strings.Index(tag, ","); commaIdx != -1 { +// tag = tag[:commaIdx] +// } +// if tag != "" { +// return tag, false +// } +// } +// return field.Name, false +//} diff --git a/mapstruct/map2struct.go b/mapstruct/map2struct.go index faf0c7c..dde3bb2 100644 --- a/mapstruct/map2struct.go +++ b/mapstruct/map2struct.go @@ -5,103 +5,149 @@ import ( "fmt" "reflect" "strings" + "time" ) -// ToStruct converts a map[string]string to a struct. +// ToStruct 将map[string]string转换为结构体 func ToStruct(m map[string]string, out any, opts ...Option) error { + // 初始化默认选项 options := defaultOptions() + // 应用所有提供的选项 for _, opt := range opts { opt(options) } + // 检查输出参数是否有效 if out == nil { - return errors.New("output is nil") + return errors.New("输出参数不能为nil") } + // 获取反射值 v := reflect.ValueOf(out) + // 检查是否为指针且非nil if v.Kind() != reflect.Ptr || v.IsNil() { - return errors.New("output must be a non-nil pointer") + return errors.New("输出参数必须是非nil指针") } + // 获取指向的值 v = v.Elem() + // 检查是否为结构体 if v.Kind() != reflect.Struct { - return errors.New("output must be a pointer to struct") + return errors.New("输出参数必须指向结构体") } + // 执行转换 return fillStructFromMap(m, v, options) } -// fillStructFromMap performs the actual map-to-struct conversion. +// fillStructFromMap 从map填充结构体字段 func fillStructFromMap(m map[string]string, v reflect.Value, opts *Options) error { + // 获取结构体类型信息 t := v.Type() + // 遍历所有字段 for i := 0; i < v.NumField(); i++ { - field := t.Field(i) - fieldValue := v.Field(i) + field := t.Field(i) // 获取字段信息 + fieldValue := v.Field(i) // 获取字段值 + // 跳过不可设置的字段(非导出字段) if !fieldValue.CanSet() { continue } + // 获取字段映射名称 name, skip := getFieldName(field, opts.TagName) if skip { - continue + continue // 跳过标记为"-"的字段 } + // 从map中获取值 value, ok := m[name] if !ok { - continue + continue // map中没有对应字段则跳过 } + // 设置字段值 if err := setField(m, fieldValue, field, value, opts); err != nil { - return fmt.Errorf("field %s: %w", name, err) + return fmt.Errorf("设置字段%s失败: %w", name, err) } } return nil } +// getFieldName 从结构体字段获取映射名称 +func getFieldName(field reflect.StructField, tagName string) (string, bool) { + // 查找指定标签 + if tag, ok := field.Tag.Lookup(tagName); ok { + if tag == "-" { + return "", true // 标记为跳过 + } + // 处理标签中的选项(如omitempty) + if commaIdx := strings.Index(tag, ","); commaIdx != -1 { + tag = tag[:commaIdx] + } + if tag != "" { + return tag, false // 使用标签指定的名称 + } + } + return field.Name, false // 默认使用字段名 +} + +// setField 设置结构体字段值 func setField(m map[string]string, fieldValue reflect.Value, field reflect.StructField, value string, opts *Options) error { - // Handle pointers + // 处理指针类型 if fieldValue.Kind() == reflect.Ptr { + if strings.TrimSpace(value) == "" { + // 空字符串表示nil指针 + fieldValue.Set(reflect.Zero(field.Type)) + return nil + } + + // 创建新实例(如果指针为nil) if fieldValue.IsNil() { fieldValue.Set(reflect.New(field.Type.Elem())) } + // 递归处理指向的值 return setField(m, fieldValue.Elem(), field, value, opts) } - // Apply hooks or basic conversion + // 应用转换钩子或基本类型转换 result, err := applyMapToStructHook(value, field, opts) if err != nil { return err } - // Handle nested structs - if fieldValue.Kind() == reflect.Struct { + // 处理嵌套结构体 + if fieldValue.Kind() == reflect.Struct && fieldValue.Type() != reflect.TypeOf(time.Time{}) { + // 创建嵌套map(过滤出以当前字段名为前缀的键) nestedMap := make(map[string]string) name, _ := getFieldName(field, opts.TagName) prefix := name + "." for k, v := range m { if strings.HasPrefix(k, prefix) { + // 去掉前缀后作为嵌套字段名 nestedMap[strings.TrimPrefix(k, prefix)] = v } } + // 如果嵌套map不为空,则递归处理 if len(nestedMap) > 0 { return fillStructFromMap(nestedMap, fieldValue, opts) } return nil } - // Set the value + // 设置最终值 if result != nil { rv := reflect.ValueOf(result) + // 检查类型是否可转换 if rv.Type().ConvertibleTo(fieldValue.Type()) { fieldValue.Set(rv.Convert(fieldValue.Type())) return nil } } - return fmt.Errorf("cannot convert %q to %v", value, fieldValue.Type()) + return fmt.Errorf("无法将 %q 转换为 %v 类型", value, fieldValue.Type()) } diff --git a/mapstruct/mapstruct.go b/mapstruct/mapstruct.go index ecff0f2..b30dfa0 100644 --- a/mapstruct/mapstruct.go +++ b/mapstruct/mapstruct.go @@ -7,33 +7,33 @@ // - Field name mapping via struct tags package mapstruct -import ( - "reflect" -) - -// HookFunc defines a function type for custom field conversion. -type HookFunc func(reflect.Value, reflect.StructField) (any, error) - -// StringHookFunc defines a function type for custom string-to-value conversion. -type StringHookFunc func(string, reflect.StructField) (any, error) - -// Direction indicates the conversion direction. -//type Direction int -// -//const ( -// StructToMap Direction = iota -// MapToStruct +//import ( +// "reflect" //) - -// commonOptions contains options shared by both conversions. -type commonOptions struct { - TagName string - FieldHooks map[string]HookFunc -} - -// newCommonOptions creates a new commonOptions with defaults. -func newCommonOptions() *commonOptions { - return &commonOptions{ - TagName: "json", - } -} +// +//// HookFunc defines a function type for custom field conversion. +//type HookFunc func(reflect.Value, reflect.StructField) (any, error) +// +//// StringHookFunc defines a function type for custom string-to-value conversion. +//type StringHookFunc func(string, reflect.StructField) (any, error) +// +//// Direction indicates the conversion direction. +////type Direction int +//// +////const ( +//// StructToMap Direction = iota +//// MapToStruct +////) +// +//// commonOptions contains options shared by both conversions. +//type commonOptions struct { +// TagName string +// FieldHooks map[string]HookFunc +//} +// +//// newCommonOptions creates a new commonOptions with defaults. +//func newCommonOptions() *commonOptions { +// return &commonOptions{ +// TagName: "json", +// } +//} diff --git a/mapstruct/mapstruct_test.go b/mapstruct/mapstruct_test.go index e13478b..6d9c7be 100644 --- a/mapstruct/mapstruct_test.go +++ b/mapstruct/mapstruct_test.go @@ -33,7 +33,15 @@ func TestStruct2Map(t *testing.T) { user.Address.City = "New York" user.Address.Country = "USA" - result, err := ToMap(user) + time2stringHook := func(v reflect.Value, _ reflect.StructField) (any, error) { + if v.Type() == reflect.TypeOf(time.Time{}) { + if t, ok := v.Interface().(time.Time); ok { + return t.Format("2006-01-02 15:04:05"), nil + } + } + return v, nil + } + result, err := ToMap(user, WithTypeHook(reflect.TypeOf(time.Time{}), time2stringHook)) if err != nil { panic(err) } @@ -51,6 +59,7 @@ func TestStruct2Map(t *testing.T) { } var user2 User + err = ToStruct(data, &user2) if err != nil { panic(err) @@ -73,6 +82,7 @@ func TestStruct2Map(t *testing.T) { var user3 User err = ToStruct(data2, &user3, + WithTagName("json"), WithStringTypeHook(reflect.TypeOf(time.Time{}), timeHook), WithFieldHook("Name", nameHook), ) diff --git a/mapstruct/options.go b/mapstruct/options.go index 40ddd75..9798783 100644 --- a/mapstruct/options.go +++ b/mapstruct/options.go @@ -1,32 +1,36 @@ package mapstruct import ( + "fmt" "reflect" "time" ) -// Options contains all conversion options. +// HookFunc 定义字段转换钩子函数类型 +type HookFunc func(reflect.Value, reflect.StructField) (any, error) + +// StringHookFunc 定义字符串到值的转换钩子函数类型 +type StringHookFunc func(string, reflect.StructField) (any, error) + +// Options 包含所有转换选项 type Options struct { - *commonOptions - - // StructToMap options - TypeHooks map[reflect.Type]HookFunc - - // MapToStruct options - StringTypeHooks map[reflect.Type]StringHookFunc + TagName string // 使用的结构体标签名称 + TypeHooks map[reflect.Type]HookFunc // 类型级别的钩子 + StringTypeHooks map[reflect.Type]StringHookFunc // 字符串到类型的钩子 + FieldHooks map[string]HookFunc // 字段级别的钩子 } -// Option configures conversion options. +// Option 是配置选项的函数类型 type Option func(*Options) -// WithTagName sets the struct tag name to use for field mapping. +// WithTagName 设置结构体标签名称 func WithTagName(name string) Option { return func(o *Options) { o.TagName = name } } -// WithTypeHook adds a hook for struct-to-map conversion. +// WithTypeHook 添加类型级别的转换钩子 func WithTypeHook(typ reflect.Type, hook HookFunc) Option { return func(o *Options) { if o.TypeHooks == nil { @@ -36,7 +40,7 @@ func WithTypeHook(typ reflect.Type, hook HookFunc) Option { } } -// WithStringTypeHook adds a hook for map-to-struct conversion. +// WithStringTypeHook 添加字符串到类型的转换钩子 func WithStringTypeHook(typ reflect.Type, hook StringHookFunc) Option { return func(o *Options) { if o.StringTypeHooks == nil { @@ -46,7 +50,7 @@ func WithStringTypeHook(typ reflect.Type, hook StringHookFunc) Option { } } -// WithFieldHook adds a field-specific hook for struct-to-map conversion. +// WithFieldHook 添加字段级别的转换钩子 func WithFieldHook(fieldName string, hook HookFunc) Option { return func(o *Options) { if o.FieldHooks == nil { @@ -56,35 +60,47 @@ func WithFieldHook(fieldName string, hook HookFunc) Option { } } -// WithStringFieldHook adds a field-specific hook for map-to-struct conversion. -func WithStringFieldHook(fieldName string, hook StringHookFunc) Option { - return func(o *Options) { - if o.FieldHooks == nil { - o.FieldHooks = make(map[string]HookFunc) - } - // For map-to-struct, we need to adapt the hook type - o.FieldHooks[fieldName] = func(v reflect.Value, f reflect.StructField) (any, error) { - if v.Kind() != reflect.String { - return nil, nil - } - return hook(v.String(), f) - } - } -} +//// WithStringFieldHook 添加字段级别的字符串转换钩子 +//func WithStringFieldHook(fieldName string, hook StringHookFunc) Option { +// return func(o *Options) { +// if o.FieldHooks == nil { +// o.FieldHooks = make(map[string]HookFunc) +// } +// // 适配器:将StringHookFunc转换为HookFunc +// o.FieldHooks[fieldName] = func(v reflect.Value, f reflect.StructField) (any, error) { +// if v.Kind() != reflect.String { +// return nil, nil +// } +// return hook(v.String(), f) +// } +// } +//} -// defaultOptions returns default options with common type hooks. +// defaultOptions 返回默认配置选项 func defaultOptions() *Options { opts := &Options{ - commonOptions: newCommonOptions(), + TagName: "json", // 默认使用json标签 } - // Register default time.Time hooks - WithTypeHook(reflect.TypeOf(time.Time{}), func(v reflect.Value, _ reflect.StructField) (any, error) { - return v.Interface(), nil - })(opts) + // 注册默认的时间类型钩子 + WithStringTypeHook(reflect.TypeOf(time.Time{}), func(value string, _ reflect.StructField) (any, error) { + // 尝试多种常见时间格式 + formats := []string{ + "2006-01-02 15:04:05", // 标准格式 + "2006-01-02", // 仅日期 + time.RFC3339, // ISO8601格式 + "2006/01/02 15:04:05", // 斜杠分隔 + "2006/01/02", // 斜杠分隔仅日期 + "02-Jan-2006", // 英文月份缩写 + } - WithStringTypeHook(reflect.TypeOf(time.Time{}), func(s string, _ reflect.StructField) (any, error) { - return time.Parse("2006-01-02 15:04:05", s) + for _, layout := range formats { + if t, err := time.Parse(layout, value); err == nil { + return t, nil + } + } + + return nil, fmt.Errorf("无法解析时间字符串: %s", value) })(opts) return opts