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

This commit is contained in:
liuxiaobo 2025-06-04 22:01:47 +08:00
parent b07cff2f5d
commit a66b008478
6 changed files with 537 additions and 0 deletions

82
mapstruct/hooks.go Normal file
View File

@ -0,0 +1,82 @@
package mapstruct
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// applyStructToMapHook applies hooks for struct-to-map conversion.
func applyStructToMapHook(v reflect.Value, field reflect.StructField, opts *Options) (any, bool, error) {
// Check field-specific hooks first
if hook, ok := opts.FieldHooks[field.Name]; ok {
result, err := hook(v, field)
return result, true, err
}
// Check type hooks
if hook, ok := opts.TypeHooks[v.Type()]; ok {
result, err := hook(v, field)
return result, true, err
}
// Handle pointers
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, true, nil
}
v = v.Elem()
if hook, ok := opts.TypeHooks[v.Type()]; ok {
result, err := hook(v, field)
return result, true, err
}
}
return nil, false, nil
}
// applyMapToStructHook applies hooks for map-to-struct conversion.
func applyMapToStructHook(value string, field reflect.StructField, opts *Options) (any, error) {
// Check field-specific hooks
if hook, ok := opts.FieldHooks[field.Name]; ok {
return hook(reflect.ValueOf(value), field)
}
// Check type hooks
if hook, ok := opts.StringTypeHooks[field.Type]; ok {
return hook(value, field)
}
// Handle basic types
switch field.Type.Kind() {
case reflect.String:
return value, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.ParseInt(value, 10, 64)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.ParseUint(value, 10, 64)
case reflect.Float32, reflect.Float64:
return strconv.ParseFloat(value, 64)
case reflect.Bool:
return strconv.ParseBool(value)
default:
return nil, fmt.Errorf("unsupported type: %s", field.Type.String())
}
}
// 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
}

107
mapstruct/map2struct.go Normal file
View File

@ -0,0 +1,107 @@
package mapstruct
import (
"errors"
"fmt"
"reflect"
"strings"
)
// ToStruct converts a map[string]string to a struct.
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")
}
v := reflect.ValueOf(out)
if v.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("output must be a non-nil pointer")
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return errors.New("output must be a pointer to struct")
}
return fillStructFromMap(m, v, options)
}
// fillStructFromMap performs the actual map-to-struct conversion.
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
}
value, ok := m[name]
if !ok {
continue
}
if err := setField(m, fieldValue, field, value, opts); err != nil {
return fmt.Errorf("field %s: %w", name, err)
}
}
return nil
}
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.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 {
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
}
}
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())
}

39
mapstruct/mapstruct.go Normal file
View File

@ -0,0 +1,39 @@
// Package mapstruct provides bidirectional conversion between Go structs and maps.
// It supports:
// - Struct <-> map[string]any
// - Struct <-> map[string]string
// - Custom type conversion via hooks
// - Nested structs and pointers
// - 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
//)
// 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

@ -0,0 +1,83 @@
package mapstruct
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Active bool `json:"active"`
Score float64 `json:"score"`
Address struct {
City string `json:"city"`
Country string `json:"country"`
} `json:"address"`
}
func TestStruct2Map(t *testing.T) {
// 1. Struct to map
user := User{
Name: "Alice",
Age: 25,
CreatedAt: time.Now(),
Active: true,
Score: 95.5,
}
user.Address.City = "New York"
user.Address.Country = "USA"
result, err := ToMap(user)
if err != nil {
panic(err)
}
fmt.Printf("Struct to map:\n%+v\n", result)
// 2. Map to struct
data := map[string]string{
"name": "Bob",
"age": "30",
"created_at": "2025-06-04 21:07:10",
"active": "true",
"score": "88.5",
"address.city": "Los Angeles",
"address.country": "USA",
}
var user2 User
err = ToStruct(data, &user2)
if err != nil {
panic(err)
}
fmt.Printf("\nMap to struct:\n%+v\n", user2)
// 3. With custom hooks
timeHook := func(s string, _ reflect.StructField) (any, error) {
return time.Parse("2006/01/02", s)
}
nameHook := func(v reflect.Value, _ reflect.StructField) (any, error) {
return strings.ToUpper(v.String()), nil
}
data2 := map[string]string{
"name": "charlie",
"created_at": "2025/06/04",
}
var user3 User
err = ToStruct(data2, &user3,
WithStringTypeHook(reflect.TypeOf(time.Time{}), timeHook),
WithFieldHook("Name", nameHook),
)
if err != nil {
panic(err)
}
fmt.Printf("\nWith custom hooks:\n%+v\n", user3)
}

91
mapstruct/options.go Normal file
View File

@ -0,0 +1,91 @@
package mapstruct
import (
"reflect"
"time"
)
// Options contains all conversion options.
type Options struct {
*commonOptions
// StructToMap options
TypeHooks map[reflect.Type]HookFunc
// MapToStruct options
StringTypeHooks map[reflect.Type]StringHookFunc
}
// Option configures conversion options.
type Option func(*Options)
// WithTagName sets the struct tag name to use for field mapping.
func WithTagName(name string) Option {
return func(o *Options) {
o.TagName = name
}
}
// WithTypeHook adds a hook for struct-to-map conversion.
func WithTypeHook(typ reflect.Type, hook HookFunc) Option {
return func(o *Options) {
if o.TypeHooks == nil {
o.TypeHooks = make(map[reflect.Type]HookFunc)
}
o.TypeHooks[typ] = hook
}
}
// WithStringTypeHook adds a hook for map-to-struct conversion.
func WithStringTypeHook(typ reflect.Type, hook StringHookFunc) Option {
return func(o *Options) {
if o.StringTypeHooks == nil {
o.StringTypeHooks = make(map[reflect.Type]StringHookFunc)
}
o.StringTypeHooks[typ] = hook
}
}
// WithFieldHook adds a field-specific hook for struct-to-map conversion.
func WithFieldHook(fieldName string, hook HookFunc) Option {
return func(o *Options) {
if o.FieldHooks == nil {
o.FieldHooks = make(map[string]HookFunc)
}
o.FieldHooks[fieldName] = hook
}
}
// 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)
}
}
}
// defaultOptions returns default options with common type hooks.
func defaultOptions() *Options {
opts := &Options{
commonOptions: newCommonOptions(),
}
// 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(s string, _ reflect.StructField) (any, error) {
return time.Parse("2006-01-02 15:04:05", s)
})(opts)
return opts
}

135
mapstruct/struct2map.go Normal file
View File

@ -0,0 +1,135 @@
package mapstruct
import (
"errors"
"fmt"
"reflect"
)
// converts a struct to map[string]any.
func ToMap(input any, opts ...Option) (map[string]any, error) {
options := defaultOptions()
for _, opt := range opts {
opt(options)
}
v := reflect.ValueOf(input)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, errors.New("input is nil pointer")
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, errors.New("input is not a struct")
}
return convertStructToMap(v, options)
}
// convertStructToMap performs the actual struct-to-map conversion.
func convertStructToMap(v reflect.Value, opts *Options) (map[string]any, error) {
result := make(map[string]any)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
if !field.IsExported() {
continue
}
name, skip := getFieldName(field, opts.TagName)
if skip {
continue
}
value, err := convertField(fieldValue, field, opts)
if err != nil {
return nil, fmt.Errorf("field %s: %w", name, err)
}
if value != nil {
result[name] = value
}
}
return result, nil
}
// convertField converts a single struct field.
func convertField(v reflect.Value, field reflect.StructField, opts *Options) (any, error) {
// Apply hooks
if result, handled, err := applyStructToMapHook(v, field, opts); handled {
return result, err
}
// Handle pointers
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, nil
}
v = v.Elem()
}
// Handle nested structs
if v.Kind() == reflect.Struct {
return convertStructToMap(v, opts)
}
// Handle slices/arrays
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
return convertSlice(v, opts)
}
// Handle maps
if v.Kind() == reflect.Map {
return convertMap(v, opts)
}
return v.Interface(), nil
}
// convertSlice converts a slice/array field.
func convertSlice(v reflect.Value, opts *Options) (any, error) {
sliceLen := v.Len()
result := make([]any, sliceLen)
for i := 0; i < sliceLen; i++ {
elem := v.Index(i)
val, err := convertField(elem, reflect.StructField{}, opts)
if err != nil {
return nil, err
}
result[i] = val
}
return result, nil
}
// convertMap converts a map field.
func convertMap(v reflect.Value, opts *Options) (any, error) {
result := make(map[string]any)
iter := v.MapRange()
for iter.Next() {
key := iter.Key()
val := iter.Value()
keyStr, ok := key.Interface().(string)
if !ok {
return nil, fmt.Errorf("map key must be string, got %v", key.Kind())
}
value, err := convertField(val, reflect.StructField{}, opts)
if err != nil {
return nil, err
}
result[keyStr] = value
}
return result, nil
}