添加struct与map互相转换的功能

This commit is contained in:
liuxiaobo 2025-06-04 22:59:12 +08:00
parent a66b008478
commit 8d20db12d2
5 changed files with 169 additions and 98 deletions

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings"
) )
// applyStructToMapHook applies hooks for struct-to-map conversion. // 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. //// getFieldName extracts the field name from struct tags.
func getFieldName(field reflect.StructField, tagName string) (string, bool) { //func getFieldName(field reflect.StructField, tagName string) (string, bool) {
if tag, ok := field.Tag.Lookup(tagName); ok { // if tag, ok := field.Tag.Lookup(tagName); ok {
if tag == "-" { // if tag == "-" {
return "", true // return "", true
} // }
if commaIdx := strings.Index(tag, ","); commaIdx != -1 { // if commaIdx := strings.Index(tag, ","); commaIdx != -1 {
tag = tag[:commaIdx] // tag = tag[:commaIdx]
} // }
if tag != "" { // if tag != "" {
return tag, false // return tag, false
} // }
} // }
return field.Name, false // return field.Name, false
} //}

View File

@ -5,103 +5,149 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strings" "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 { func ToStruct(m map[string]string, out any, opts ...Option) error {
// 初始化默认选项
options := defaultOptions() options := defaultOptions()
// 应用所有提供的选项
for _, opt := range opts { for _, opt := range opts {
opt(options) opt(options)
} }
// 检查输出参数是否有效
if out == nil { if out == nil {
return errors.New("output is nil") return errors.New("输出参数不能为nil")
} }
// 获取反射值
v := reflect.ValueOf(out) v := reflect.ValueOf(out)
// 检查是否为指针且非nil
if v.Kind() != reflect.Ptr || v.IsNil() { if v.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("output must be a non-nil pointer") return errors.New("输出参数必须是非nil指针")
} }
// 获取指向的值
v = v.Elem() v = v.Elem()
// 检查是否为结构体
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return errors.New("output must be a pointer to struct") return errors.New("输出参数必须指向结构体")
} }
// 执行转换
return fillStructFromMap(m, v, options) 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 { func fillStructFromMap(m map[string]string, v reflect.Value, opts *Options) error {
// 获取结构体类型信息
t := v.Type() t := v.Type()
// 遍历所有字段
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
field := t.Field(i) field := t.Field(i) // 获取字段信息
fieldValue := v.Field(i) fieldValue := v.Field(i) // 获取字段值
// 跳过不可设置的字段(非导出字段)
if !fieldValue.CanSet() { if !fieldValue.CanSet() {
continue continue
} }
// 获取字段映射名称
name, skip := getFieldName(field, opts.TagName) name, skip := getFieldName(field, opts.TagName)
if skip { if skip {
continue continue // 跳过标记为"-"的字段
} }
// 从map中获取值
value, ok := m[name] value, ok := m[name]
if !ok { if !ok {
continue continue // map中没有对应字段则跳过
} }
// 设置字段值
if err := setField(m, fieldValue, field, value, opts); err != nil { 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 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 { 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 fieldValue.Kind() == reflect.Ptr {
if strings.TrimSpace(value) == "" {
// 空字符串表示nil指针
fieldValue.Set(reflect.Zero(field.Type))
return nil
}
// 创建新实例(如果指针为nil)
if fieldValue.IsNil() { if fieldValue.IsNil() {
fieldValue.Set(reflect.New(field.Type.Elem())) fieldValue.Set(reflect.New(field.Type.Elem()))
} }
// 递归处理指向的值
return setField(m, fieldValue.Elem(), field, value, opts) return setField(m, fieldValue.Elem(), field, value, opts)
} }
// Apply hooks or basic conversion // 应用转换钩子或基本类型转换
result, err := applyMapToStructHook(value, field, opts) result, err := applyMapToStructHook(value, field, opts)
if err != nil { if err != nil {
return err 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) nestedMap := make(map[string]string)
name, _ := getFieldName(field, opts.TagName) name, _ := getFieldName(field, opts.TagName)
prefix := name + "." prefix := name + "."
for k, v := range m { for k, v := range m {
if strings.HasPrefix(k, prefix) { if strings.HasPrefix(k, prefix) {
// 去掉前缀后作为嵌套字段名
nestedMap[strings.TrimPrefix(k, prefix)] = v nestedMap[strings.TrimPrefix(k, prefix)] = v
} }
} }
// 如果嵌套map不为空则递归处理
if len(nestedMap) > 0 { if len(nestedMap) > 0 {
return fillStructFromMap(nestedMap, fieldValue, opts) return fillStructFromMap(nestedMap, fieldValue, opts)
} }
return nil return nil
} }
// Set the value // 设置最终值
if result != nil { if result != nil {
rv := reflect.ValueOf(result) rv := reflect.ValueOf(result)
// 检查类型是否可转换
if rv.Type().ConvertibleTo(fieldValue.Type()) { if rv.Type().ConvertibleTo(fieldValue.Type()) {
fieldValue.Set(rv.Convert(fieldValue.Type())) fieldValue.Set(rv.Convert(fieldValue.Type()))
return nil return nil
} }
} }
return fmt.Errorf("cannot convert %q to %v", value, fieldValue.Type()) return fmt.Errorf("无法将 %q 转换为 %v 类型", value, fieldValue.Type())
} }

View File

@ -7,33 +7,33 @@
// - Field name mapping via struct tags // - Field name mapping via struct tags
package mapstruct package mapstruct
import ( //import (
"reflect" // "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
//) //)
//
// commonOptions contains options shared by both conversions. //// HookFunc defines a function type for custom field conversion.
type commonOptions struct { //type HookFunc func(reflect.Value, reflect.StructField) (any, error)
TagName string //
FieldHooks map[string]HookFunc //// StringHookFunc defines a function type for custom string-to-value conversion.
} //type StringHookFunc func(string, reflect.StructField) (any, error)
//
// newCommonOptions creates a new commonOptions with defaults. //// Direction indicates the conversion direction.
func newCommonOptions() *commonOptions { ////type Direction int
return &commonOptions{ ////
TagName: "json", ////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",
// }
//}

View File

@ -33,7 +33,15 @@ func TestStruct2Map(t *testing.T) {
user.Address.City = "New York" user.Address.City = "New York"
user.Address.Country = "USA" 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 { if err != nil {
panic(err) panic(err)
} }
@ -51,6 +59,7 @@ func TestStruct2Map(t *testing.T) {
} }
var user2 User var user2 User
err = ToStruct(data, &user2) err = ToStruct(data, &user2)
if err != nil { if err != nil {
panic(err) panic(err)
@ -73,6 +82,7 @@ func TestStruct2Map(t *testing.T) {
var user3 User var user3 User
err = ToStruct(data2, &user3, err = ToStruct(data2, &user3,
WithTagName("json"),
WithStringTypeHook(reflect.TypeOf(time.Time{}), timeHook), WithStringTypeHook(reflect.TypeOf(time.Time{}), timeHook),
WithFieldHook("Name", nameHook), WithFieldHook("Name", nameHook),
) )

View File

@ -1,32 +1,36 @@
package mapstruct package mapstruct
import ( import (
"fmt"
"reflect" "reflect"
"time" "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 { type Options struct {
*commonOptions TagName string // 使用的结构体标签名称
TypeHooks map[reflect.Type]HookFunc // 类型级别的钩子
// StructToMap options StringTypeHooks map[reflect.Type]StringHookFunc // 字符串到类型的钩子
TypeHooks map[reflect.Type]HookFunc FieldHooks map[string]HookFunc // 字段级别的钩子
// MapToStruct options
StringTypeHooks map[reflect.Type]StringHookFunc
} }
// Option configures conversion options. // Option 是配置选项的函数类型
type Option func(*Options) type Option func(*Options)
// WithTagName sets the struct tag name to use for field mapping. // WithTagName 设置结构体标签名称
func WithTagName(name string) Option { func WithTagName(name string) Option {
return func(o *Options) { return func(o *Options) {
o.TagName = name o.TagName = name
} }
} }
// WithTypeHook adds a hook for struct-to-map conversion. // WithTypeHook 添加类型级别的转换钩子
func WithTypeHook(typ reflect.Type, hook HookFunc) Option { func WithTypeHook(typ reflect.Type, hook HookFunc) Option {
return func(o *Options) { return func(o *Options) {
if o.TypeHooks == nil { 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 { func WithStringTypeHook(typ reflect.Type, hook StringHookFunc) Option {
return func(o *Options) { return func(o *Options) {
if o.StringTypeHooks == nil { 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 { func WithFieldHook(fieldName string, hook HookFunc) Option {
return func(o *Options) { return func(o *Options) {
if o.FieldHooks == nil { 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. //// WithStringFieldHook 添加字段级别的字符串转换钩子
func WithStringFieldHook(fieldName string, hook StringHookFunc) Option { //func WithStringFieldHook(fieldName string, hook StringHookFunc) Option {
return func(o *Options) { // return func(o *Options) {
if o.FieldHooks == nil { // if o.FieldHooks == nil {
o.FieldHooks = make(map[string]HookFunc) // o.FieldHooks = make(map[string]HookFunc)
} // }
// For map-to-struct, we need to adapt the hook type // // 适配器将StringHookFunc转换为HookFunc
o.FieldHooks[fieldName] = func(v reflect.Value, f reflect.StructField) (any, error) { // o.FieldHooks[fieldName] = func(v reflect.Value, f reflect.StructField) (any, error) {
if v.Kind() != reflect.String { // if v.Kind() != reflect.String {
return nil, nil // return nil, nil
} // }
return hook(v.String(), f) // return hook(v.String(), f)
} // }
} // }
} //}
// defaultOptions returns default options with common type hooks. // defaultOptions 返回默认配置选项
func defaultOptions() *Options { func defaultOptions() *Options {
opts := &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) { WithStringTypeHook(reflect.TypeOf(time.Time{}), func(value string, _ reflect.StructField) (any, error) {
return v.Interface(), nil // 尝试多种常见时间格式
})(opts) 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) { for _, layout := range formats {
return time.Parse("2006-01-02 15:04:05", s) if t, err := time.Parse(layout, value); err == nil {
return t, nil
}
}
return nil, fmt.Errorf("无法解析时间字符串: %s", value)
})(opts) })(opts)
return opts return opts