mapstructure icon indicating copy to clipboard operation
mapstructure copied to clipboard

Can StringToTimeHookFunc be enhanced?

Open kaysonwu opened this issue 2 years ago • 0 comments

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
	}
}

kaysonwu avatar May 11 '23 02:05 kaysonwu