diff --git a/models_test.go b/models_test.go index 6ada3ec..888c9ac 100644 --- a/models_test.go +++ b/models_test.go @@ -25,16 +25,14 @@ type WithPointer struct { FloatVal *float32 `jsonapi:"attr,float-val"` } -type Timestamp struct { - ID int `jsonapi:"primary,timestamps"` - Time time.Time `jsonapi:"attr,timestamp,iso8601"` - 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 TimestampModel struct { + ID int `jsonapi:"primary,timestamps"` + DefaultV time.Time `jsonapi:"attr,defaultv"` + DefaultP *time.Time `jsonapi:"attr,defaultp"` + ISO8601V time.Time `jsonapi:"attr,iso8601v,iso8601"` + ISO8601P *time.Time `jsonapi:"attr,iso8601p,iso8601"` + RFC3339V time.Time `jsonapi:"attr,rfc3339v,rfc3339"` + RFC3339P *time.Time `jsonapi:"attr,rfc3339p,rfc3339"` } type Car struct { diff --git a/request.go b/request.go index b3a6bff..1a81d81 100644 --- a/request.go +++ b/request.go @@ -547,29 +547,25 @@ func handleLinks(attribute interface{}, args []string, fieldValue reflect.Value) } func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) { - var isIso8601 bool - var isRFC3339 bool + var isISO8601, isRFC3339 bool v := reflect.ValueOf(attribute) if len(args) > 2 { for _, arg := range args[2:] { if arg == annotationISO8601 { - isIso8601 = true + isISO8601 = true } else if arg == annotationRFC3339 { isRFC3339 = true } } } - if isIso8601 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { + if isISO8601 { + if v.Kind() != reflect.String { return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } - t, err := time.Parse(iso8601TimeFormat, tm) + t, err := time.Parse(iso8601TimeFormat, v.Interface().(string)) if err != nil { return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } @@ -582,14 +578,11 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) } if isRFC3339 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { + if v.Kind() != reflect.String { return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 } - t, err := time.Parse(time.RFC3339, tm) + t, err := time.Parse(time.RFC3339, v.Interface().(string)) if err != nil { return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 } diff --git a/request_test.go b/request_test.go index d045029..aaa2ae3 100644 --- a/request_test.go +++ b/request_test.go @@ -3,6 +3,7 @@ package jsonapi import ( "bytes" "encoding/json" + "errors" "fmt" "io" "reflect" @@ -341,150 +342,6 @@ func TestUnmarshalSetsAttrs(t *testing.T) { } } -func TestUnmarshalParsesISO8601(t *testing.T) { - payload := &OnePayload{ - Data: &Node{ - Type: "timestamps", - Attributes: map[string]interface{}{ - "timestamp": "2016-08-17T08:27:12Z", - }, - }, - } - - in := bytes.NewBuffer(nil) - json.NewEncoder(in).Encode(payload) - - out := new(Timestamp) - - if err := UnmarshalPayload(in, out); err != nil { - t.Fatal(err) - } - - expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) - - if !out.Time.Equal(expected) { - t.Fatal("Parsing the ISO8601 timestamp failed") - } -} - -func TestUnmarshalParsesISO8601TimePointer(t *testing.T) { - payload := &OnePayload{ - Data: &Node{ - Type: "timestamps", - Attributes: map[string]interface{}{ - "next": "2016-08-17T08:27:12Z", - }, - }, - } - - in := bytes.NewBuffer(nil) - json.NewEncoder(in).Encode(payload) - - out := new(Timestamp) - - if err := UnmarshalPayload(in, out); err != nil { - t.Fatal(err) - } - - expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) - - if !out.Next.Equal(expected) { - t.Fatal("Parsing the ISO8601 timestamp failed") - } -} - -func TestUnmarshalInvalidISO8601(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(Timestamp) - - if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 { - t.Fatalf("Expected ErrInvalidISO8601, got %v", err) - } -} - -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 { @@ -716,6 +573,178 @@ func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { } } +func TestUnmarshal_Times(t *testing.T) { + aTime := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) + + for _, tc := range []struct { + desc string + inputPayload *OnePayload + wantErr bool + verification func(tm *TimestampModel) error + }{ + // Default: + { + desc: "default_byValue", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "defaultv": aTime.Unix(), + }, + }, + }, + verification: func(tm *TimestampModel) error { + if !tm.DefaultV.Equal(aTime) { + return errors.New("times not equal!") + } + return nil + }, + }, + { + desc: "default_byPointer", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "defaultp": aTime.Unix(), + }, + }, + }, + verification: func(tm *TimestampModel) error { + if !tm.DefaultP.Equal(aTime) { + return errors.New("times not equal!") + } + return nil + }, + }, + { + desc: "default_invalid", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "defaultv": "not a timestamp!", + }, + }, + }, + wantErr: true, + }, + // ISO 8601: + { + desc: "iso8601_byValue", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "iso8601v": "2016-08-17T08:27:12Z", + }, + }, + }, + verification: func(tm *TimestampModel) error { + if !tm.ISO8601V.Equal(aTime) { + return errors.New("times not equal!") + } + return nil + }, + }, + { + desc: "iso8601_byPointer", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "iso8601p": "2016-08-17T08:27:12Z", + }, + }, + }, + verification: func(tm *TimestampModel) error { + if !tm.ISO8601P.Equal(aTime) { + return errors.New("times not equal!") + } + return nil + }, + }, + { + desc: "iso8601_invalid", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "iso8601v": "not a timestamp", + }, + }, + }, + wantErr: true, + }, + // RFC 3339 + { + desc: "rfc3339_byValue", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "rfc3339v": "2016-08-17T08:27:12Z", + }, + }, + }, + verification: func(tm *TimestampModel) error { + if got, want := tm.RFC3339V, aTime; got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "rfc3339_byPointer", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "rfc3339p": "2016-08-17T08:27:12Z", + }, + }, + }, + verification: func(tm *TimestampModel) error { + if got, want := *tm.RFC3339P, aTime; got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "rfc3339_invalid", + inputPayload: &OnePayload{ + Data: &Node{ + Type: "timestamps", + Attributes: map[string]interface{}{ + "rfc3339v": "not a timestamp", + }, + }, + }, + wantErr: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + // Serialize the OnePayload using the standard JSON library. + in := bytes.NewBuffer(nil) + if err := json.NewEncoder(in).Encode(tc.inputPayload); err != nil { + t.Fatal(err) + } + + out := &TimestampModel{} + err := UnmarshalPayload(in, out) + if got, want := (err != nil), tc.wantErr; got != want { + t.Fatalf("UnmarshalPayload error: got %v, want %v", got, want) + } + if tc.verification != nil { + if err := tc.verification(out); err != nil { + t.Fatal(err) + } + } + }) + } +} + func unmarshalSamplePayload() (*Blog, error) { in := samplePayload() out := new(Blog) diff --git a/response.go b/response.go index eab0ec8..576614d 100644 --- a/response.go +++ b/response.go @@ -283,7 +283,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.ClientID = clientID } } else if annotation == annotationAttribute { - var omitEmpty, iso8601 bool + var omitEmpty, iso8601, rfc3339 bool if len(args) > 2 { for _, arg := range args[2:] { @@ -292,6 +292,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, omitEmpty = true case annotationISO8601: iso8601 = true + case annotationRFC3339: + rfc3339 = true } } } @@ -309,6 +311,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, if iso8601 { node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat) + } else if rfc3339 { + node.Attributes[args[1]] = t.UTC().Format(time.RFC3339) } else { node.Attributes[args[1]] = t.Unix() } @@ -329,6 +333,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, if iso8601 { node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat) + } else if rfc3339 { + node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339) } else { node.Attributes[args[1]] = tm.Unix() } diff --git a/response_test.go b/response_test.go index 5b42595..7b98632 100644 --- a/response_test.go +++ b/response_test.go @@ -3,6 +3,7 @@ package jsonapi import ( "bytes" "encoding/json" + "fmt" "reflect" "sort" "testing" @@ -470,61 +471,6 @@ func TestOmitsZeroTimes(t *testing.T) { } } -func TestMarshalISO8601Time(t *testing.T) { - testModel := &Timestamp{ - ID: 5, - Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC), - } - - out := bytes.NewBuffer(nil) - if err := MarshalPayload(out, testModel); err != nil { - t.Fatal(err) - } - - resp := new(OnePayload) - if err := json.NewDecoder(out).Decode(resp); err != nil { - t.Fatal(err) - } - - data := resp.Data - - if data.Attributes == nil { - t.Fatalf("Expected attributes") - } - - if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" { - t.Fatal("Timestamp was not serialised into ISO8601 correctly") - } -} - -func TestMarshalISO8601TimePointer(t *testing.T) { - tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC) - testModel := &Timestamp{ - ID: 5, - Next: &tm, - } - - out := bytes.NewBuffer(nil) - if err := MarshalPayload(out, testModel); err != nil { - t.Fatal(err) - } - - resp := new(OnePayload) - if err := json.NewDecoder(out).Decode(resp); err != nil { - t.Fatal(err) - } - - data := resp.Data - - if data.Attributes == nil { - t.Fatalf("Expected attributes") - } - - if data.Attributes["next"] != "2016-08-17T08:27:12Z" { - t.Fatal("Next was not serialised into ISO8601 correctly") - } -} - func TestSupportsLinkable(t *testing.T) { testModel := &Blog{ ID: 5, @@ -901,6 +847,116 @@ func TestMarshal_InvalidIntefaceArgument(t *testing.T) { } } +func TestMarshal_Times(t *testing.T) { + aTime := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC) + + for _, tc := range []struct { + desc string + input *TimestampModel + verification func(data map[string]interface{}) error + }{ + { + desc: "default_byValue", + input: &TimestampModel{ + ID: 5, + DefaultV: aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultv"].(float64) + if got, want := int64(v), aTime.Unix(); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "default_byPointer", + input: &TimestampModel{ + ID: 5, + DefaultP: &aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultp"].(float64) + if got, want := int64(v), aTime.Unix(); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "iso8601_byValue", + input: &TimestampModel{ + ID: 5, + ISO8601V: aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601v"].(string) + if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "iso8601_byPointer", + input: &TimestampModel{ + ID: 5, + ISO8601P: &aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601p"].(string) + if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "rfc3339_byValue", + input: &TimestampModel{ + ID: 5, + RFC3339V: aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339v"].(string) + if got, want := v, aTime.UTC().Format(time.RFC3339); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + { + desc: "rfc3339_byPointer", + input: &TimestampModel{ + ID: 5, + RFC3339P: &aTime, + }, + verification: func(root map[string]interface{}) error { + v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339p"].(string) + if got, want := v, aTime.UTC().Format(time.RFC3339); got != want { + return fmt.Errorf("got %v, want %v", got, want) + } + return nil + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + out := bytes.NewBuffer(nil) + if err := MarshalPayload(out, tc.input); err != nil { + t.Fatal(err) + } + // Use the standard JSON library to traverse the genereated JSON payload. + data := map[string]interface{}{} + json.Unmarshal(out.Bytes(), &data) + if tc.verification != nil { + if err := tc.verification(data); err != nil { + t.Fatal(err) + } + } + }) + } +} + func testBlog() *Blog { return &Blog{ ID: 5,