添加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