添加struct与map互相转换的功能
This commit is contained in:
parent
b07cff2f5d
commit
a66b008478
82
mapstruct/hooks.go
Normal file
82
mapstruct/hooks.go
Normal 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
107
mapstruct/map2struct.go
Normal 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
39
mapstruct/mapstruct.go
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
83
mapstruct/mapstruct_test.go
Normal file
83
mapstruct/mapstruct_test.go
Normal 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
91
mapstruct/options.go
Normal 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
135
mapstruct/struct2map.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user