diff --git a/HISTORY.md b/HISTORY.md index fbdcdba6..899516b2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add [x/jsonx: DayTime](/x/jsonx/day_time.go) for JSON marshal and unmarshal of "15:04:05" (hour, minute, second). + - Fix a bug of `WithoutBodyConsumptionOnUnmarshal` configurator and a minor dependency injection issue caused by the previous alpha version between 20 and 26 February of 2022. - New basic [cors middleware](middleware/cors). diff --git a/x/client/client.go b/x/client/client.go index 299b9b3c..c77fd044 100644 --- a/x/client/client.go +++ b/x/client/client.go @@ -121,7 +121,7 @@ func (c *Client) emitEndRequest(ctx context.Context, resp *http.Response, err er // RequestOption declares the type of option one can pass // to the Do methods(JSON, Form, ReadJSON...). // Request options run before request constructed. -type RequestOption func(*http.Request) error +type RequestOption = func(*http.Request) error // We always add the following request headers, unless they're removed by custom ones. var defaultRequestOptions = []RequestOption{ diff --git a/x/client/option.go b/x/client/option.go index f3a1e950..dd53be1e 100644 --- a/x/client/option.go +++ b/x/client/option.go @@ -13,7 +13,7 @@ import ( // All the builtin client options should live here, for easy discovery. -type Option func(*Client) +type Option = func(*Client) // BaseURL registers the base URL of this client. // All of its methods will prepend this url. diff --git a/x/jsonx/day_time.go b/x/jsonx/day_time.go new file mode 100644 index 00000000..a39b337d --- /dev/null +++ b/x/jsonx/day_time.go @@ -0,0 +1,100 @@ +package jsonx + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +const ( + // DayTimeLayout holds the time layout for the the format of "hour:minute:second", hour can be 15, meaning 3 PM. + DayTimeLayout = "15:04:05" +) + +// DayTime describes a time compatible with DayTimeLayout. +type DayTime time.Time + +// ParseDayTime reads from "s" and returns the DayTime time. +func ParseDayTime(s string) (DayTime, error) { + if s == "" || s == "null" { + return DayTime{}, nil + } + + tt, err := time.Parse(DayTimeLayout, s) + if err != nil { + return DayTime{}, err + } + + return DayTime(tt), nil +} + +// UnmarshalJSON parses the "b" into DayTime time. +func (t *DayTime) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return nil + } + + s := strings.Trim(string(b), `"`) + tt, err := ParseDayTime(s) + if err != nil { + return err + } + + *t = tt + return nil +} + +// MarshalJSON writes a quoted string in the DayTime time format. +func (t DayTime) MarshalJSON() ([]byte, error) { + if s := t.String(); s != "" { + s = strconv.Quote(s) + return []byte(s), nil + } + + return nullLiteral, nil // Note: if the front-end wants an empty string instead I must change that. +} + +// ToTime returns the unwrapped *t to time.Time. +func (t *DayTime) ToTime() time.Time { + tt := time.Time(*t) + return tt +} + +// IsZero reports whether "t" is zero time. +func (t DayTime) IsZero() bool { + return time.Time(t).IsZero() +} + +// String returns the text representation of the "t" using the DayTime time layout. +func (t DayTime) String() string { + tt := t.ToTime() + if tt.IsZero() { + return "" + } + + return tt.Format(DayTimeLayout) +} + +// Scan completes the sql driver.Scanner interface. +func (t *DayTime) Scan(src interface{}) error { + switch v := src.(type) { + case time.Time: // type was set to timestamp + if v.IsZero() { + return nil // don't set zero, ignore it. + } + *t = DayTime(v) + case string: + tt, err := ParseDayTime(v) + if err != nil { + return err + } + *t = tt + case nil: + *t = DayTime(time.Time{}) + default: + return fmt.Errorf("DayTime: unknown type of: %T", v) + } + + return nil +} diff --git a/x/jsonx/day_time_test.go b/x/jsonx/day_time_test.go new file mode 100644 index 00000000..035a6148 --- /dev/null +++ b/x/jsonx/day_time_test.go @@ -0,0 +1,52 @@ +package jsonx + +import ( + "encoding/json" + "testing" + "time" +) + +func TestDayTime(t *testing.T) { + tests := []struct { + rawData string + }{ + { + rawData: `{"start": "8:33:00", "end": "15:00:42", "nothing": null, "empty": ""}`, + }, + { + rawData: `{"start": "8:33:00", "end": "15:00:42", "nothing": null, "empty": ""}`, + }, + } + + for _, tt := range tests { + v := struct { + Start DayTime `json:"start"` + End DayTime `json:"end"` + Nothing DayTime `json:"nothing"` + Empty DayTime `json:"empty"` + }{} + + err := json.Unmarshal([]byte(tt.rawData), &v) + if err != nil { + t.Fatal(err) + } + + if !v.Nothing.IsZero() { + t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) + } + + if !v.Empty.IsZero() { + t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) + } + + loc := time.UTC + + if expected, got := time.Date(0, time.January, 1, 8, 33, 0, 0, loc), v.Start.ToTime(); expected != got { + t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) + } + + if expected, got := time.Date(0, time.January, 1, 15, 0, 42, 0, loc), v.End.ToTime(); expected != got { + t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) + } + } +}