mitchellh / mapstructure

Go library for decoding generic map values into native Go structures and vice versa.
https://gist.github.com/mitchellh/90029601268e59a29e64e55bab1c5bdc
MIT License
7.89k stars 669 forks source link

Can StringToTimeHookFunc be enhanced? #330

Open kaysonwu opened 1 year ago

kaysonwu commented 1 year ago

I defined an alias type DateTime for time.Time, It mainly implements the time.DateTime format string and time.Time interchange

type DateTime time.Time

func (d DateTime) MarshalJSON() ([]byte, error) {
    t := time.Time(d)

    if t.Equal(time.Time{}) {
        return []byte("null"), nil
    }

    return []byte(`"` + t.Format(time.DateTime) + `"`), nil
}

func (d *DateTime) UnmarshalJSON(data []byte) error {
    str := string(data)

    if str == "null" {
        *d = DateTime(time.Time{})
    } else if date, err := time.Parse(time.DateTime, str); err == nil {
        *d = DateTime(date)
    } else {
        return err
    }

    return nil
}

func (d DateTime) Value() (driver.Value, error) {
    t := time.Time(d)

    if t.Equal(time.Time{}) {
        return nil, nil
    }

    return t.Format(time.DateTime), nil
}

func (d *DateTime) Scan(value any) error {
    if val, ok := value.(time.Time); ok {
        *d = DateTime(val)

        return nil
    }

    return fmt.Errorf("Failed to scan type %T into DateTime", value)
}

Next, I will apply DateTime to the user model and provide a NewModel factory function

type User struct {
    Name  string `json:"name" gorm:"size:255"`
   CreatedAt DateTime `json:"created_at"`
   UpdatedAt DateTime `json:"created_at"`
}

func NewModel[T any](attributes map[string]any, tagName string) (T, error) {
    var model T

    decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
        DecodeHook:       mapstructure.StringToTimeHookFunc(time.DateTime),
        WeaklyTypedInput: true,
        TagName:          tagName,
        Result:           &model,
    })

    if err != nil {
        return model, err
    }

    return model, decoder.Decode(attributes)
}

When I run, I receive 'created_at' expected a map, got 'string' error message.

 attributes := map[string]any {
  "name": "foo",
  "created_at": "2006-01-02 15:04:05",
    "updated_at": "2006-01-02 15:04:05",
}

user, err := NewModel[User](attributes)

if err != nil {
   fmt.Println(err)
} else {
   fmt.Println("ok")
}

Tracking the code, I found that it can enhance StringToTimeHookFunc to make it more adaptable, e.g.

func StringToTimeHookFunc(layout string) mapstructure.DecodeHookFunc {
    return func(
        f reflect.Type,
        t reflect.Type,
        data interface{}) (interface{}, error) {
        if f.Kind() != reflect.String {
            return data, nil
        }

                //  Rewrite t  != reflect.TypeOf(time.Time{})  to  ConvertibleTo
        if !t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
            return data, nil
        }

        // Convert it by parsing
        value, err := time.Parse(layout, data.(string))

        if err != nil {
            return data, err
        }

        return reflect.ValueOf(value).Convert(t).Interface(), nil
    }
}