fox/mapstruct/map2struct.go

154 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mapstruct
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
)
// 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("输出参数不能为nil")
}
// 获取反射值
v := reflect.ValueOf(out)
// 检查是否为指针且非nil
if v.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("输出参数必须是非nil指针")
}
// 获取指向的值
v = v.Elem()
// 检查是否为结构体
if v.Kind() != reflect.Struct {
return errors.New("输出参数必须指向结构体")
}
// 执行转换
return fillStructFromMap(m, v, options)
}
// 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) // 获取字段值
// 跳过不可设置的字段(非导出字段)
if !fieldValue.CanSet() {
continue
}
// 获取字段映射名称
name, skip := getFieldName(field, opts.TagName)
if skip {
continue // 跳过标记为"-"的字段
}
// 从map中获取值
value, ok := m[name]
if !ok {
continue // map中没有对应字段则跳过
}
// 设置字段值
if err := setField(m, fieldValue, field, value, opts); err != nil {
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 {
// 处理指针类型
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)
}
// 应用转换钩子或基本类型转换
result, err := applyMapToStructHook(value, field, opts)
if err != nil {
return err
}
// 处理嵌套结构体
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
}
// 设置最终值
if result != nil {
rv := reflect.ValueOf(result)
// 检查类型是否可转换
if rv.Type().ConvertibleTo(fieldValue.Type()) {
fieldValue.Set(rv.Convert(fieldValue.Type()))
return nil
}
}
return fmt.Errorf("无法将 %q 转换为 %v 类型", value, fieldValue.Type())
}