From 776433d17de0b308a9bb0e407e7b3b10736f76d6 Mon Sep 17 00:00:00 2001 From: Samantha Belkin Date: Thu, 16 Feb 2017 18:40:50 -0700 Subject: [PATCH 1/3] Add support for 'meta' (#72) * add Meta field in node structs and Metable/RelationshipMetable interfaces * update response marshalling to add Meta * update tests for presence of Meta * add Metable and RelationshipMetable interfaces to example * update README to include Meta details --- README.md | 22 +++++++++++++++++++++ examples/app.go | 21 ++++++++++++++++++++ node.go | 19 ++++++++++++++++++ response.go | 13 +++++++++++++ response_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+) diff --git a/README.md b/README.md index d5e673a..b799b91 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,28 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *Links { } ``` +### Meta + + If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta: + + ```go + func (post Post) JSONAPIMeta() *Meta { + return &Meta{ + "details": "sample details here", + } + } + + // Invoked for each relationship defined on the Post struct when marshaled + func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { + if relation == "comments" { + return &Meta{ + "details": "comment meta details here", + } + } + return nil + } + ``` + ### Errors This package also implements support for JSON API compatible `errors` payloads using the following types. diff --git a/examples/app.go b/examples/app.go index 3326688..a385a36 100644 --- a/examples/app.go +++ b/examples/app.go @@ -345,3 +345,24 @@ func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface } return nil } + +// Blog Meta +func (blog Blog) JSONAPIMeta() map[string]interface{} { + return map[string]interface{}{ + "detail": "extra details regarding the blog", + } +} + +func (blog Blog) JSONAPIRelationshipMeta(relation string) map[string]interface{} { + if relation == "posts" { + return map[string]interface{}{ + "detail": "posts meta information", + } + } + if relation == "current_post" { + return map[string]interface{}{ + "detail": "current post meta information", + } + } + return nil +} diff --git a/node.go b/node.go index 3a0c02e..72083ce 100644 --- a/node.go +++ b/node.go @@ -8,6 +8,7 @@ type OnePayload struct { Data *Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta,omitempty"` } // ManyPayload is used to represent a generic JSON API payload where many @@ -16,6 +17,7 @@ type ManyPayload struct { Data []*Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta,omitempty"` } // Node is used to represent a generic JSON API Resource @@ -26,12 +28,14 @@ type Node struct { Attributes map[string]interface{} `json:"attributes,omitempty"` Relationships map[string]interface{} `json:"relationships,omitempty"` Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta,omitempty"` } // RelationshipOneNode is used to represent a generic has one JSON API relation type RelationshipOneNode struct { Data *Node `json:"data"` Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta,omitempty"` } // RelationshipManyNode is used to represent a generic has many JSON API @@ -39,12 +43,17 @@ type RelationshipOneNode struct { type RelationshipManyNode struct { Data []*Node `json:"data"` Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta,omitempty"` } // Links is used to represent a `links` object. // http://jsonapi.org/format/#document-links type Links map[string]interface{} +// Meta is used to represent a `meta` object. +// http://jsonapi.org/format/#document-meta +type Meta map[string]interface{} + func (l *Links) validate() (err error) { // Each member of a links object is a “link”. A link MUST be represented as // either: @@ -85,3 +94,13 @@ type RelationshipLinkable interface { // JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipLinks(relation string) *Links } + +type Metable interface { + JSONAPIMeta() *Meta +} + +// RelationshipMetable is used to include relationship meta in response data +type RelationshipMetable interface { + // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) + JSONAPIRelationshipMeta(relation string) *Meta +} diff --git a/response.go b/response.go index c44cd3b..fcee64b 100644 --- a/response.go +++ b/response.go @@ -373,6 +373,11 @@ func visitModelNode(model interface{}, included *map[string]*Node, relLinks = linkableModel.JSONAPIRelationshipLinks(args[1]) } + var relMeta *Meta + if metableModel, ok := model.(RelationshipMetable); ok { + relMeta = metableModel.JSONAPIRelationshipMeta(args[1]) + } + if isSlice { // to-many relationship relationship, err := visitModelNodeRelationships( @@ -385,6 +390,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, break } relationship.Links = relLinks + relationship.Meta = relMeta if sideload { shallowNodes := []*Node{} @@ -396,6 +402,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.Relationships[args[1]] = &RelationshipManyNode{ Data: shallowNodes, Links: relationship.Links, + Meta: relationship.Meta, } } else { node.Relationships[args[1]] = relationship @@ -424,11 +431,13 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.Relationships[args[1]] = &RelationshipOneNode{ Data: toShallowNode(relationship), Links: relLinks, + Meta: relMeta, } } else { node.Relationships[args[1]] = &RelationshipOneNode{ Data: relationship, Links: relLinks, + Meta: relMeta, } } } @@ -451,6 +460,10 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.Links = linkableModel.JSONAPILinks() } + if metableModel, ok := model.(Metable); ok { + node.Meta = metableModel.JSONAPIMeta() + } + return node, nil } diff --git a/response_test.go b/response_test.go index 756fe87..edceae9 100644 --- a/response_test.go +++ b/response_test.go @@ -58,6 +58,26 @@ func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { return nil } +func (blog Blog) JSONAPIMeta() *Meta { + return &Meta{ + "detail": "extra details regarding the blog", + } +} + +func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { + if relation == "posts" { + return &Meta{ + "detail": "extra posts detail", + } + } + if relation == "current_post" { + return &Meta{ + "detail": "extra current_post detail", + } + } + return nil +} + type Post struct { Blog ID uint64 `jsonapi:"primary,posts"` @@ -557,6 +577,30 @@ func TestSupportsLinkable(t *testing.T) { } } +func TestSupportsMetable(t *testing.T) { + testModel := &Blog{ + ID: 5, + Title: "Title 1", + CreatedAt: time.Now(), + } + + out := bytes.NewBuffer(nil) + if err := MarshalOnePayload(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.Meta == nil { + t.Fatalf("Expected 'details' meta") + } +} + func TestInvalidLinkable(t *testing.T) { testModel := &BadComment{ ID: 5, @@ -594,6 +638,9 @@ func TestRelations(t *testing.T) { if relations["posts"].(map[string]interface{})["links"] == nil { t.Fatalf("Posts relationship links were not materialized") } + if relations["posts"].(map[string]interface{})["meta"] == nil { + t.Fatalf("Posts relationship meta were not materialized") + } } if relations["current_post"] == nil { @@ -602,6 +649,9 @@ func TestRelations(t *testing.T) { if relations["current_post"].(map[string]interface{})["links"] == nil { t.Fatalf("Current post relationship links were not materialized") } + if relations["current_post"].(map[string]interface{})["meta"] == nil { + t.Fatalf("Current post relationship meta were not materialized") + } } if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 { From 0a2decba4344ef02b42092609f9be8d7b610783b Mon Sep 17 00:00:00 2001 From: Aren Patel Date: Thu, 16 Feb 2017 20:25:19 -0800 Subject: [PATCH 2/3] Make the Meta test also check for the value of the detail key. Moving all testing models to their own file. Updated the readme to include a deeply nested, varying typed meta example. Convert the map[string]interface to a Meta in the tests. Make the Meta field of a Link of type Meta rather than a map[string]inteface{}. Use the headerAccept constant defined for the example app. Commenting the new Metable interface and moving the Meta type beside it. Example app updated to use jsonapi.Links and jsonapi.Meta types rather than the underlying map[string]interface{}. --- README.md | 48 ++++++++------ examples/app.go | 8 +-- examples/models.go | 33 ++++++++-- models_test.go | 157 +++++++++++++++++++++++++++++++++++++++++++++ node.go | 14 ++-- request_test.go | 20 ------ response_test.go | 156 ++++++-------------------------------------- 7 files changed, 246 insertions(+), 190 deletions(-) create mode 100644 models_test.go diff --git a/README.md b/README.md index b799b91..dc59105 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi) [![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi) -A serializer/deserializer for json payloads that comply to the +A serializer/deserializer for JSON payloads that comply to the [JSON API - jsonapi.org](http://jsonapi.org) spec in go. ## Installation @@ -365,26 +365,36 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *Links { ``` ### Meta - + If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta: - + ```go - func (post Post) JSONAPIMeta() *Meta { - return &Meta{ - "details": "sample details here", - } - } - - // Invoked for each relationship defined on the Post struct when marshaled - func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { - if relation == "comments" { - return &Meta{ - "details": "comment meta details here", - } - } - return nil - } - ``` +func (post Post) JSONAPIMeta() *Meta { + return &Meta{ + "details": "sample details here", + } +} + +// Invoked for each relationship defined on the Post struct when marshaled +func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { + if relation == "comments" { + return &Meta{ + "this": map[string]interface{}{ + "can": map[string]interface{}{ + "go": []interface{}{ + "as", + "deep", + map[string]interface{}{ + "as": "required", + }, + }, + }, + }, + } + } + return nil +} +``` ### Errors This package also implements support for JSON API compatible `errors` payloads using the following types. diff --git a/examples/app.go b/examples/app.go index 0e3b2a3..0ba50c3 100644 --- a/examples/app.go +++ b/examples/app.go @@ -43,7 +43,7 @@ func exerciseHandler() { // list req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) - req.Header.Set("Accept", jsonapi.MediaType) + req.Header.Set(headerAccept, jsonapi.MediaType) w := httptest.NewRecorder() @@ -60,7 +60,7 @@ func exerciseHandler() { // show req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil) - req.Header.Set("Accept", jsonapi.MediaType) + req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() @@ -81,7 +81,7 @@ func exerciseHandler() { req, _ = http.NewRequest(http.MethodPost, "/blogs", in) - req.Header.Set("Accept", jsonapi.MediaType) + req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() @@ -107,7 +107,7 @@ func exerciseHandler() { req, _ = http.NewRequest(http.MethodPut, "/blogs", in) - req.Header.Set("Accept", jsonapi.MediaType) + req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() diff --git a/examples/models.go b/examples/models.go index c28a303..399d739 100644 --- a/examples/models.go +++ b/examples/models.go @@ -3,6 +3,8 @@ package main import ( "fmt" "time" + + "github.com/google/jsonapi" ) type Blog struct { @@ -30,22 +32,43 @@ type Comment struct { } // Blog Links -func (blog Blog) JSONAPILinks() *map[string]interface{} { - return &map[string]interface{}{ +func (blog Blog) JSONAPILinks() *jsonapi.Links { + return &jsonapi.Links{ "self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), } } -func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} { +func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links { if relation == "posts" { - return &map[string]interface{}{ + return &jsonapi.Links{ "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), } } if relation == "current_post" { - return &map[string]interface{}{ + return &jsonapi.Links{ "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), } } return nil } + +// Blog Meta +func (blog Blog) JSONAPIMeta() *jsonapi.Meta { + return &jsonapi.Meta{ + "detail": "extra details regarding the blog", + } +} + +func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta { + if relation == "posts" { + return &jsonapi.Meta{ + "detail": "posts meta information", + } + } + if relation == "current_post" { + return &jsonapi.Meta{ + "detail": "current post meta information", + } + } + return nil +} diff --git a/models_test.go b/models_test.go new file mode 100644 index 0000000..a53dd61 --- /dev/null +++ b/models_test.go @@ -0,0 +1,157 @@ +package jsonapi + +import ( + "fmt" + "time" +) + +type BadModel struct { + ID int `jsonapi:"primary"` +} + +type ModelBadTypes struct { + ID string `jsonapi:"primary,badtypes"` + StringField string `jsonapi:"attr,string_field"` + FloatField float64 `jsonapi:"attr,float_field"` + TimeField time.Time `jsonapi:"attr,time_field"` + TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` +} + +type WithPointer struct { + ID *uint64 `jsonapi:"primary,with-pointers"` + Name *string `jsonapi:"attr,name"` + IsActive *bool `jsonapi:"attr,is-active"` + IntVal *int `jsonapi:"attr,int-val"` + 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 Car struct { + ID *string `jsonapi:"primary,cars"` + Make *string `jsonapi:"attr,make,omitempty"` + Model *string `jsonapi:"attr,model,omitempty"` + Year *uint `jsonapi:"attr,year,omitempty"` +} + +type Post struct { + Blog + ID uint64 `jsonapi:"primary,posts"` + BlogID int `jsonapi:"attr,blog_id"` + ClientID string `jsonapi:"client-id"` + Title string `jsonapi:"attr,title"` + Body string `jsonapi:"attr,body"` + Comments []*Comment `jsonapi:"relation,comments"` + LatestComment *Comment `jsonapi:"relation,latest_comment"` +} + +type Comment struct { + ID int `jsonapi:"primary,comments"` + ClientID string `jsonapi:"client-id"` + PostID int `jsonapi:"attr,post_id"` + Body string `jsonapi:"attr,body"` +} + +type Book struct { + ID uint64 `jsonapi:"primary,books"` + Author string `jsonapi:"attr,author"` + ISBN string `jsonapi:"attr,isbn"` + Title string `jsonapi:"attr,title,omitempty"` + Description *string `jsonapi:"attr,description"` + Pages *uint `jsonapi:"attr,pages,omitempty"` + PublishedAt time.Time + Tags []string `jsonapi:"attr,tags"` +} + +type Blog struct { + ID int `jsonapi:"primary,blogs"` + ClientID string `jsonapi:"client-id"` + Title string `jsonapi:"attr,title"` + Posts []*Post `jsonapi:"relation,posts"` + CurrentPost *Post `jsonapi:"relation,current_post"` + CurrentPostID int `jsonapi:"attr,current_post_id"` + CreatedAt time.Time `jsonapi:"attr,created_at"` + ViewCount int `jsonapi:"attr,view_count"` +} + +func (b *Blog) JSONAPILinks() *Links { + return &Links{ + "self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID), + "comments": Link{ + Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID), + Meta: Meta{ + "counts": map[string]uint{ + "likes": 4, + "comments": 20, + }, + }, + }, + } +} + +func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { + if relation == "posts" { + return &Links{ + "related": Link{ + Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID), + Meta: Meta{ + "count": len(b.Posts), + }, + }, + } + } + if relation == "current_post" { + return &Links{ + "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), + "related": Link{ + Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID), + }, + } + } + return nil +} + +func (b *Blog) JSONAPIMeta() *Meta { + return &Meta{ + "detail": "extra details regarding the blog", + } +} + +func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { + if relation == "posts" { + return &Meta{ + "this": map[string]interface{}{ + "can": map[string]interface{}{ + "go": []interface{}{ + "as", + "deep", + map[string]interface{}{ + "as": "required", + }, + }, + }, + }, + } + } + if relation == "current_post" { + return &Meta{ + "detail": "extra current_post detail", + } + } + return nil +} + +type BadComment struct { + ID uint64 `jsonapi:"primary,bad-comment"` + Body string `jsonapi:"attr,body"` +} + +func (bc *BadComment) JSONAPILinks() *Links { + return &Links{ + "self": []string{"invalid", "should error"}, + } +} diff --git a/node.go b/node.go index 72083ce..1e722e8 100644 --- a/node.go +++ b/node.go @@ -50,10 +50,6 @@ type RelationshipManyNode struct { // http://jsonapi.org/format/#document-links type Links map[string]interface{} -// Meta is used to represent a `meta` object. -// http://jsonapi.org/format/#document-meta -type Meta map[string]interface{} - func (l *Links) validate() (err error) { // Each member of a links object is a “link”. A link MUST be represented as // either: @@ -78,8 +74,8 @@ func (l *Links) validate() (err error) { // Link is used to represent a member of the `links` object. type Link struct { - Href string `json:"href"` - Meta map[string]interface{} `json:"meta,omitempty"` + Href string `json:"href"` + Meta Meta `json:"meta,omitempty"` } // Linkable is used to include document links in response data @@ -95,6 +91,12 @@ type RelationshipLinkable interface { JSONAPIRelationshipLinks(relation string) *Links } +// Meta is used to represent a `meta` object. +// http://jsonapi.org/format/#document-meta +type Meta map[string]interface{} + +// Metable is used to include document meta in response data +// e.g. {"foo": "bar"} type Metable interface { JSONAPIMeta() *Meta } diff --git a/request_test.go b/request_test.go index c429701..a2e40e8 100644 --- a/request_test.go +++ b/request_test.go @@ -11,26 +11,6 @@ import ( "time" ) -type BadModel struct { - ID int `jsonapi:"primary"` -} - -type WithPointer struct { - ID *uint64 `jsonapi:"primary,with-pointers"` - Name *string `jsonapi:"attr,name"` - IsActive *bool `jsonapi:"attr,is-active"` - IntVal *int `jsonapi:"attr,int-val"` - FloatVal *float32 `jsonapi:"attr,float-val"` -} - -type ModelBadTypes struct { - ID string `jsonapi:"primary,badtypes"` - StringField string `jsonapi:"attr,string_field"` - FloatField float64 `jsonapi:"attr,float_field"` - TimeField time.Time `jsonapi:"attr,time_field"` - TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` -} - func TestUnmarshall_attrStringSlice(t *testing.T) { out := &Book{} tags := []string{"fiction", "sale"} diff --git a/response_test.go b/response_test.go index edceae9..331023e 100644 --- a/response_test.go +++ b/response_test.go @@ -3,110 +3,12 @@ package jsonapi import ( "bytes" "encoding/json" - "fmt" "reflect" "sort" "testing" "time" ) -type Blog struct { - ID int `jsonapi:"primary,blogs"` - ClientID string `jsonapi:"client-id"` - Title string `jsonapi:"attr,title"` - Posts []*Post `jsonapi:"relation,posts"` - CurrentPost *Post `jsonapi:"relation,current_post"` - CurrentPostID int `jsonapi:"attr,current_post_id"` - CreatedAt time.Time `jsonapi:"attr,created_at"` - ViewCount int `jsonapi:"attr,view_count"` -} - -func (b *Blog) JSONAPILinks() *Links { - return &Links{ - "self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID), - "comments": Link{ - Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID), - Meta: map[string]interface{}{ - "counts": map[string]uint{ - "likes": 4, - "comments": 20, - }, - }, - }, - } -} - -func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { - if relation == "posts" { - return &Links{ - "related": Link{ - Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID), - Meta: map[string]interface{}{ - "count": len(b.Posts), - }, - }, - } - } - if relation == "current_post" { - return &Links{ - "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), - "related": Link{ - Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID), - }, - } - } - return nil -} - -func (blog Blog) JSONAPIMeta() *Meta { - return &Meta{ - "detail": "extra details regarding the blog", - } -} - -func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { - if relation == "posts" { - return &Meta{ - "detail": "extra posts detail", - } - } - if relation == "current_post" { - return &Meta{ - "detail": "extra current_post detail", - } - } - return nil -} - -type Post struct { - Blog - ID uint64 `jsonapi:"primary,posts"` - BlogID int `jsonapi:"attr,blog_id"` - ClientID string `jsonapi:"client-id"` - Title string `jsonapi:"attr,title"` - Body string `jsonapi:"attr,body"` - Comments []*Comment `jsonapi:"relation,comments"` - LatestComment *Comment `jsonapi:"relation,latest_comment"` -} - -type Comment struct { - ID int `jsonapi:"primary,comments"` - ClientID string `jsonapi:"client-id"` - PostID int `jsonapi:"attr,post_id"` - Body string `jsonapi:"attr,body"` -} - -type Book struct { - ID uint64 `jsonapi:"primary,books"` - Author string `jsonapi:"attr,author"` - ISBN string `jsonapi:"attr,isbn"` - Title string `jsonapi:"attr,title,omitempty"` - Description *string `jsonapi:"attr,description"` - Pages *uint `jsonapi:"attr,pages,omitempty"` - PublishedAt time.Time - Tags []string `jsonapi:"attr,tags"` -} - func TestMarshall_attrStringSlice(t *testing.T) { tags := []string{"fiction", "sale"} b := &Book{ID: 1, Tags: tags} @@ -264,30 +166,6 @@ func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) { } } -type Timestamp struct { - ID int `jsonapi:"primary,timestamps"` - Time time.Time `jsonapi:"attr,timestamp,iso8601"` - Next *time.Time `jsonapi:"attr,next,iso8601"` -} - -type Car struct { - ID *string `jsonapi:"primary,cars"` - Make *string `jsonapi:"attr,make,omitempty"` - Model *string `jsonapi:"attr,model,omitempty"` - Year *uint `jsonapi:"attr,year,omitempty"` -} - -type BadComment struct { - ID uint64 `jsonapi:"primary,bad-comment"` - Body string `jsonapi:"attr,body"` -} - -func (bc *BadComment) JSONAPILinks() *Links { - return &Links{ - "self": []string{"invalid", "should error"}, - } -} - func TestMarshalIDPtr(t *testing.T) { id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang" car := &Car{ @@ -529,7 +407,7 @@ func TestSupportsLinkable(t *testing.T) { data := resp.Data if data.Links == nil { - t.Fatal("Expected links") + t.Fatal("Expected data.links") } links := *data.Links @@ -566,7 +444,9 @@ func TestSupportsLinkable(t *testing.T) { if !isMap { t.Fatal("Expected 'comments' to contain a map") } - countsMap, isMap := commentsMetaMap["counts"].(map[string]interface{}) + + commentsMetaObject := Meta(commentsMetaMap) + countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{}) if !isMap { t.Fatal("Expected 'counts' to contain a map") } @@ -577,6 +457,18 @@ func TestSupportsLinkable(t *testing.T) { } } +func TestInvalidLinkable(t *testing.T) { + testModel := &BadComment{ + ID: 5, + Body: "Hello World", + } + + out := bytes.NewBuffer(nil) + if err := MarshalOnePayload(out, testModel); err == nil { + t.Fatal("Was expecting an error") + } +} + func TestSupportsMetable(t *testing.T) { testModel := &Blog{ ID: 5, @@ -595,21 +487,13 @@ func TestSupportsMetable(t *testing.T) { } data := resp.Data - if data.Meta == nil { - t.Fatalf("Expected 'details' meta") - } -} - -func TestInvalidLinkable(t *testing.T) { - testModel := &BadComment{ - ID: 5, - Body: "Hello World", + t.Fatalf("Expected data.meta") } - out := bytes.NewBuffer(nil) - if err := MarshalOnePayload(out, testModel); err == nil { - t.Fatal("Was expecting an error") + meta := Meta(*data.Meta) + if e, a := "extra details regarding the blog", meta["detail"]; e != a { + t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a) } }