From d1e1cdd200b75e0a2c05494e319804e7d926d7ea Mon Sep 17 00:00:00 2001 From: MacRat Date: Sun, 27 Nov 2022 00:55:49 +0900 Subject: [PATCH] feat: support datetime format variants --- internal/endpoint/log.go | 4 +-- lib-ayd/args.go | 2 +- lib-ayd/args_test.go | 2 +- lib-ayd/history.go | 2 +- lib-ayd/incident.go | 4 +-- lib-ayd/record.go | 2 +- lib-ayd/record_fuzz_test.go | 8 ++--- lib-ayd/record_test.go | 2 +- lib-ayd/report.go | 2 +- lib-ayd/time.go | 49 ++++++++++++++++++++++++++ lib-ayd/time_test.go | 70 +++++++++++++++++++++++++++++++++++++ 11 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 lib-ayd/time.go create mode 100644 lib-ayd/time_test.go diff --git a/internal/endpoint/log.go b/internal/endpoint/log.go index c3539d84..ff8bc7f1 100644 --- a/internal/endpoint/log.go +++ b/internal/endpoint/log.go @@ -20,9 +20,9 @@ func getTimeQuery(queries url.Values, name string, default_ time.Time) (time.Tim return default_, nil } - t, err := time.Parse(time.RFC3339, q) + t, err := api.ParseTime(q) if err != nil { - return default_, fmt.Errorf("invalid %s format: %w", name, err) + return default_, fmt.Errorf("invalid %s format: %q", name, q) } return t, nil } diff --git a/lib-ayd/args.go b/lib-ayd/args.go index a411fe08..c95dce3e 100644 --- a/lib-ayd/args.go +++ b/lib-ayd/args.go @@ -58,7 +58,7 @@ func ParseAlertPluginArgsFrom(args []string) (AlertPluginArgs, error) { return AlertPluginArgs{}, ayderr.New(ErrInvalidArgumentValue, err, "invalid alert URL") } - timestamp, err := time.Parse(time.RFC3339, args[2]) + timestamp, err := ParseTime(args[2]) if err != nil { return AlertPluginArgs{}, ayderr.New(ErrInvalidArgumentValue, err, "invalid timestamp") } diff --git a/lib-ayd/args_test.go b/lib-ayd/args_test.go index 248dc45e..aa2ab2a4 100644 --- a/lib-ayd/args_test.go +++ b/lib-ayd/args_test.go @@ -131,7 +131,7 @@ func TestParseAlertPluginArgs(t *testing.T) { "", "", nil, - `invalid timestamp: parsing time "this is not a time" as "2006-01-02T15:04:05Z07:00": cannot parse "this is not a time" as "2006"`, + `invalid timestamp: invalid format: "this is not a time"`, }, { []string{"./ayd-test-alert", "foo:bar", "2001-02-03T16:05:06Z", "HEALTHY", "not a latency", "bar:baz", "foo bar", "{}"}, diff --git a/lib-ayd/history.go b/lib-ayd/history.go index db3dd26a..33624bc4 100644 --- a/lib-ayd/history.go +++ b/lib-ayd/history.go @@ -41,7 +41,7 @@ func (ph *ProbeHistory) UnmarshalJSON(data []byte) error { return err } - updated, err := time.Parse(time.RFC3339, jh.Updated) + updated, err := ParseTime(jh.Updated) if err != nil { return err } diff --git a/lib-ayd/incident.go b/lib-ayd/incident.go index 961af581..e57e1b2b 100644 --- a/lib-ayd/incident.go +++ b/lib-ayd/incident.go @@ -43,14 +43,14 @@ func (i *Incident) UnmarshalJSON(data []byte) error { return err } - startsAt, err := time.Parse(time.RFC3339, ji.StartsAt) + startsAt, err := ParseTime(ji.StartsAt) if err != nil { return err } var endsAt time.Time if ji.EndsAt != "" { - endsAt, err = time.Parse(time.RFC3339, ji.EndsAt) + endsAt, err = ParseTime(ji.EndsAt) if err != nil { return err } diff --git a/lib-ayd/record.go b/lib-ayd/record.go index aa843825..58a8addf 100644 --- a/lib-ayd/record.go +++ b/lib-ayd/record.go @@ -122,7 +122,7 @@ func (r *Record) UnmarshalJSON(data []byte) error { } else { if s, ok := value.(string); !ok { return ayderr.New(ErrInvalidRecord, nil, "invalid record: time: should be a string") - } else if r.Time, err = time.Parse(time.RFC3339, s); err != nil { + } else if r.Time, err = ParseTime(s); err != nil { return ayderr.New(ErrInvalidRecord, err, "invalid record: time") } delete(raw, "time") diff --git a/lib-ayd/record_fuzz_test.go b/lib-ayd/record_fuzz_test.go index 4b77a40e..e2e1440c 100644 --- a/lib-ayd/record_fuzz_test.go +++ b/lib-ayd/record_fuzz_test.go @@ -13,10 +13,10 @@ import ( func FuzzParseRecord(f *testing.F) { f.Add(`{"time":"2021-01-02T15:04:05+09:00", "status":"HEALTHY", "latency":123.456, "target":"ping:example.com", "message":"hello world"}`) - f.Add(`{"time":"2021-01-02T15:04:05+09:00", "status":"FAILURE", "latency":123.456, "target":"exec:/path/to/file.sh", "message":"hello world"}`) - f.Add(`{"time":"2021-01-02T15:04:05+09:00", "status":"ABORTED", "latency":1234.567, "target":"dummy:#hello", "message":"hello world"}`) - f.Add(`{"time":"2021-01-02T15:04:05+09:00", "status":"DEGRADE", "latency":1.234, "target":"dummy:"}`) - f.Add(`{"time":"2021-01-02T15:04:05+09:00", "status":"DEGRADE", "latency":1.234, "target":"dummy:", "extra":123.456, "hello":"world"}`) + f.Add(`{"time":"2021-01-02_15:04:05+09:00", "status":"FAILURE", "latency":123.456, "target":"exec:/path/to/file.sh", "message":"hello world"}`) + f.Add(`{"time":"2021-01-02 15:04:05+09", "status":"ABORTED", "latency":1234.567, "target":"dummy:#hello", "message":"hello world"}`) + f.Add(`{"time":"2021-01-02T15:04:05+0900", "status":"DEGRADE", "latency":1.234, "target":"dummy:"}`) + f.Add(`{"time":"20210102T150405+09:00", "status":"DEGRADE", "latency":1.234, "target":"dummy:", "extra":123.456, "hello":"world"}`) f.Add(`{"time":"2001-02-03T04:05:06-10:00", "status":"HEALTHY", "latency":1234.456, "target":"https://example.com/path/to/healthz", "message":"hello\tworld"}`) f.Add(`{"time":"1234-10-30T22:33:44Z", "status":"FAILURE", "latency":0.123, "target":"source+http://example.com/hello/world", "message":"this is test\nhello"}`) f.Add(`{"time":"2000-10-23T14:56:37Z", "status":"ABORTED", "latency":987654.321, "target":"alert:foobar:alert-url", "message":"cancelled"}`) diff --git a/lib-ayd/record_test.go b/lib-ayd/record_test.go index 8f0c9423..ce4d08d4 100644 --- a/lib-ayd/record_test.go +++ b/lib-ayd/record_test.go @@ -103,7 +103,7 @@ func TestRecord(t *testing.T) { }, { String: `{"time":"2021/01/02 15:04:05", "status":"HEALTHY", "latency":123.456, "target":"ping:example.com", "message":"hello world"}`, - Error: `invalid record: time: parsing time "2021/01/02 15:04:05" as "2006-01-02T15:04:05Z07:00": cannot parse "/01/02 15:04:05" as "-"`, + Error: `invalid record: time: invalid format: "2021/01/02 15:04:05"`, }, { String: `{"time":"2021-01-02T15:04:05+09:00", "status":"HEALTHY", "latency":123.456, "target":"::invalid target::", "message":"hello world"}`, diff --git a/lib-ayd/report.go b/lib-ayd/report.go index 5a196e19..f18f2fde 100644 --- a/lib-ayd/report.go +++ b/lib-ayd/report.go @@ -40,7 +40,7 @@ func (r *Report) UnmarshalJSON(data []byte) error { return err } - reportedAt, err := time.Parse(time.RFC3339, jr.ReportedAt) + reportedAt, err := ParseTime(jr.ReportedAt) if err != nil { return err } diff --git a/lib-ayd/time.go b/lib-ayd/time.go new file mode 100644 index 00000000..bafd96e6 --- /dev/null +++ b/lib-ayd/time.go @@ -0,0 +1,49 @@ +package ayd + +import ( + "errors" + "fmt" + "strings" + "time" +) + +var ( + dateformats = []string{ + "2006-01-02T", + "2006-01-02_", + "2006-01-02 ", + "20060102 ", + "20060102T", + "20060102_", + } + timeformats = []string{ + "15:04:05", + "15:04:05.999999999", + "150405", + "150405.999999999", + } + zoneformats = []string{ + "Z07:00", + "Z0700", + "Z07", + } + + ErrInvalidTime = errors.New("invalid format") +) + +// ParseTime parses time string in Ayd way. +// This function supports RFC3339 and some variant formats. +func ParseTime(s string) (time.Time, error) { + x := strings.ToUpper(strings.TrimSpace(s)) + for _, df := range dateformats { + for _, tf := range timeformats { + for _, zf := range zoneformats { + t, err := time.Parse(df+tf+zf, x) + if err == nil { + return t, nil + } + } + } + } + return time.Time{}, fmt.Errorf("%w: %q", ErrInvalidTime, s) +} diff --git a/lib-ayd/time_test.go b/lib-ayd/time_test.go new file mode 100644 index 00000000..ad9eabc7 --- /dev/null +++ b/lib-ayd/time_test.go @@ -0,0 +1,70 @@ +package ayd_test + +import ( + "errors" + "testing" + "time" + + "github.com/macrat/ayd/lib-ayd" +) + +func TestParseTime_valid(t *testing.T) { + tests := []string{ + "2000-01-02T00:03:04+00:00", + "2000-01-02T00:03:04+0000", + "2000-01-02T00:03:04-00:00", + "20000102T00:03:04-0000", + "20000102T00:03:04.897+00:00", + "20000102T00:03:04.897-00:00", + "20000102T00:03:04.897010Z", + "20000102T000304Z", + "20000102T00:03:04z", + "2000-01-02T08:48:04+08:45", + "2000-01-02T08:48:04+0845", + "2000-01-02T09:03:04+09:00", + "2000-01-02T09:03:04+0900", + "2000-01-02T090304.897+09:00", + "2000-01-02T090304.897010+09:00", + "2000-01-02T090304.897010+0900", + "2000-01-02t000304Z", + "2000-01-02t000304z", + "2000-01-02_00:03:04.897010Z", + "2000-01-02_00:03:04.897010z", + "2000-01-02_00:03:04.897z", + "20000102_00:03:04Z", + "20000102_00:03:04z", + "20000102 00:03:04+00:00", + "20000102 00:03:04-0000", + "2000-01-02 00:03:04.897-00:00", + "2000-01-02 00:03:04.897010z", + "2000-01-02 00:03:04.897Z", + "2000-01-02 00:03:04Z", + "2000-01-02 000304z", + "2000-01-02 090304+09", + "2000-01-02 09:03:04.897010+09", + } + + want := time.Date(2000, 1, 2, 0, 3, 4, 0, time.UTC) + + for _, tt := range tests { + actual, err := ayd.ParseTime(tt) + if err != nil { + t.Errorf("failed to parse %q: %s", tt, err) + } else if want.Unix() != actual.Truncate(time.Second).Unix() { + t.Errorf("unexpected result from %q: %s", tt, actual) + } + } +} + +func TestParseTime_invalid(t *testing.T) { + tests := []string{ + "2000/01/02 00:03:04", + } + + for _, tt := range tests { + _, err := ayd.ParseTime(tt) + if !errors.Is(err, ayd.ErrInvalidTime) { + t.Errorf("unexpected error from %q: %s", tt, err) + } + } +}