添加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"
"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
//}

View File

@ -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())
}

View File

@ -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",
// }
//}

View File

@ -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),
)

View File

@ -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