From c0ee6d2554c177087a0b0830775360f9e4a62eac Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 2 Apr 2021 20:10:43 -0400 Subject: [PATCH] Add RFC3339 timestamp (#201) @omarismail LGTM. I'm curious what you think about perhaps documenting these `iso8601` and `rfc3339` in the `Readme.md`? How did you find that this tag option/value existed? How can we make this better for others vs having to search the library implementation? --- constants.go | 2 ++ models_test.go | 6 +++++ request.go | 26 ++++++++++++++++++ request_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/constants.go b/constants.go index 23288d3..71fddd0 100644 --- a/constants.go +++ b/constants.go @@ -9,8 +9,10 @@ const ( annotationRelation = "relation" annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" + annotationRFC3339 = "rfc3339" annotationSeperator = "," + rfc3339TimeFormat = "2006-01-02T15:04:05Z07:00" iso8601TimeFormat = "2006-01-02T15:04:05Z" // MediaType is the identifier for the JSON API media type diff --git a/models_test.go b/models_test.go index 2d4aae4..6e44436 100644 --- a/models_test.go +++ b/models_test.go @@ -31,6 +31,12 @@ type Timestamp struct { Next *time.Time `jsonapi:"attr,next,iso8601"` } +type TimestampRFC3339 struct { + ID int `jsonapi:"primary,timestamps"` + Time time.Time `jsonapi:"attr,timestamp,rfc3339"` + Next *time.Time `jsonapi:"attr,next,rfc3339"` +} + type Car struct { ID *string `jsonapi:"primary,cars"` Make *string `jsonapi:"attr,make,omitempty"` diff --git a/request.go b/request.go index b2fa477..8f76cda 100644 --- a/request.go +++ b/request.go @@ -23,6 +23,9 @@ var ( // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") + // ErrInvalidRFC3339 is returned when a struct has a time.Time type field and includes + // "rfc3339" in the tag spec, but the JSON value was not an RFC3339 timestamp string. + ErrInvalidRFC3339 = errors.New("Only strings can be parsed as dates, RFC3339 timestamps") // ErrUnknownFieldNumberType is returned when the JSON value was a float // (numeric) but the Struct field was a non numeric type (i.e. not int, uint, // float, etc) @@ -446,12 +449,15 @@ func handleStringSlice(attribute interface{}) (reflect.Value, error) { func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) { var isIso8601 bool + var isRFC3339 bool v := reflect.ValueOf(attribute) if len(args) > 2 { for _, arg := range args[2:] { if arg == annotationISO8601 { isIso8601 = true + } else if arg == annotationRFC3339 { + isRFC3339 = true } } } @@ -476,6 +482,26 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) return reflect.ValueOf(t), nil } + if isRFC3339 { + var tm string + if v.Kind() == reflect.String { + tm = v.Interface().(string) + } else { + return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 + } + + t, err := time.Parse(time.RFC3339, tm) + if err != nil { + return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 + } + + if fieldValue.Kind() == reflect.Ptr { + return reflect.ValueOf(&t), nil + } + + return reflect.ValueOf(t), nil + } + var at int64 if v.Kind() == reflect.Float64 { diff --git a/request_test.go b/request_test.go index daa2159..09199d2 100644 --- a/request_test.go +++ b/request_test.go @@ -413,6 +413,78 @@ func TestUnmarshalInvalidISO8601(t *testing.T) { } } +func TestUnmarshalParsesRFC3339(t *testing.T) { + payload := &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "timestamp": "2020-03-16T23:09:59+00:00", + }, + }, + } + + in := bytes.NewBuffer(nil) + json.NewEncoder(in).Encode(payload) + + out := new(TimestampRFC3339) + + if err := UnmarshalPayload(in, out); err != nil { + t.Fatal(err) + } + + expected := time.Date(2020, 3, 16, 23, 9, 59, 0, time.UTC) + + if !out.Time.Equal(expected) { + t.Fatal("Parsing the RFC3339 timestamp failed") + } +} + +func TestUnmarshalParsesRFC3339TimePointer(t *testing.T) { + payload := &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "next": "2020-03-16T23:09:59+00:00", + }, + }, + } + + in := bytes.NewBuffer(nil) + json.NewEncoder(in).Encode(payload) + + out := new(TimestampRFC3339) + + if err := UnmarshalPayload(in, out); err != nil { + t.Fatal(err) + } + + expected := time.Date(2020, 3, 16, 23, 9, 59, 0, time.UTC) + + if !out.Next.Equal(expected) { + t.Fatal("Parsing the RFC3339 timestamp failed") + } +} + +func TestUnmarshalInvalidRFC3339(t *testing.T) { + payload := &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "timestamp": "17 Aug 16 08:027 MST", + }, + }, + } + + in := bytes.NewBuffer(nil) + json.NewEncoder(in).Encode(payload) + + out := new(TimestampRFC3339) + + if err := UnmarshalPayload(in, out); err != ErrInvalidRFC3339 { + t.Fatalf("Expected ErrInvalidRFC3339, got %v", err) + } +} + func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { data, err := json.Marshal(samplePayloadWithoutIncluded()) if err != nil {