Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing RFC3339 to response, add tests #5

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add missing RFC3338 to response, add tests
omarismail committed Apr 13, 2021
commit c9894524789a33a9071bc369683b59e2587f63f1
18 changes: 8 additions & 10 deletions models_test.go
Original file line number Diff line number Diff line change
@@ -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 {
21 changes: 7 additions & 14 deletions request.go
Original file line number Diff line number Diff line change
@@ -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
}
317 changes: 173 additions & 144 deletions request_test.go
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 7 additions & 1 deletion response.go
Original file line number Diff line number Diff line change
@@ -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()
}
166 changes: 111 additions & 55 deletions response_test.go
Original file line number Diff line number Diff line change
@@ -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,