From 0a2decba4344ef02b42092609f9be8d7b610783b Mon Sep 17 00:00:00 2001 From: Aren Patel Date: Thu, 16 Feb 2017 20:25:19 -0800 Subject: [PATCH] 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) } }