From 776433d17de0b308a9bb0e407e7b3b10736f76d6 Mon Sep 17 00:00:00 2001 From: Samantha Belkin Date: Thu, 16 Feb 2017 18:40:50 -0700 Subject: [PATCH] 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 {