From a66b0084785cd2c8fea37de2c66d2480ff3ecf2f Mon Sep 17 00:00:00 2001 From: liuxiaobo <1224730913@qq.com> Date: Wed, 4 Jun 2025 22:01:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0struct=E4=B8=8Emap=E4=BA=92?= =?UTF-8?q?=E7=9B=B8=E8=BD=AC=E6=8D=A2=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mapstruct/hooks.go | 82 ++++++++++++++++++++++ mapstruct/map2struct.go | 107 ++++++++++++++++++++++++++++ mapstruct/mapstruct.go | 39 +++++++++++ mapstruct/mapstruct_test.go | 83 ++++++++++++++++++++++ mapstruct/options.go | 91 ++++++++++++++++++++++++ mapstruct/struct2map.go | 135 ++++++++++++++++++++++++++++++++++++ 6 files changed, 537 insertions(+) create mode 100644 mapstruct/hooks.go create mode 100644 mapstruct/map2struct.go create mode 100644 mapstruct/mapstruct.go create mode 100644 mapstruct/mapstruct_test.go create mode 100644 mapstruct/options.go create mode 100644 mapstruct/struct2map.go diff --git a/mapstruct/hooks.go b/mapstruct/hooks.go new file mode 100644 index 0000000..321dd18 --- /dev/null +++ b/mapstruct/hooks.go @@ -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 +} diff --git a/mapstruct/map2struct.go b/mapstruct/map2struct.go new file mode 100644 index 0000000..faf0c7c --- /dev/null +++ b/mapstruct/map2struct.go @@ -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()) +} diff --git a/mapstruct/mapstruct.go b/mapstruct/mapstruct.go new file mode 100644 index 0000000..ecff0f2 --- /dev/null +++ b/mapstruct/mapstruct.go @@ -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", + } +} diff --git a/mapstruct/mapstruct_test.go b/mapstruct/mapstruct_test.go new file mode 100644 index 0000000..e13478b --- /dev/null +++ b/mapstruct/mapstruct_test.go @@ -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) +} diff --git a/mapstruct/options.go b/mapstruct/options.go new file mode 100644 index 0000000..40ddd75 --- /dev/null +++ b/mapstruct/options.go @@ -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 +} diff --git a/mapstruct/struct2map.go b/mapstruct/struct2map.go new file mode 100644 index 0000000..fc869e3 --- /dev/null +++ b/mapstruct/struct2map.go @@ -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 +}