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{}.

This commit is contained in:
Aren Patel 2017-02-16 20:25:19 -08:00
parent 9babeb5aea
commit 0a2decba43
7 changed files with 246 additions and 190 deletions

View File

@ -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) [![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. [JSON API - jsonapi.org](http://jsonapi.org) spec in go.
## Installation ## Installation
@ -369,22 +369,32 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *Links {
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: 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 ```go
func (post Post) JSONAPIMeta() *Meta { func (post Post) JSONAPIMeta() *Meta {
return &Meta{ return &Meta{
"details": "sample details here", "details": "sample details here",
} }
} }
// Invoked for each relationship defined on the Post struct when marshaled // Invoked for each relationship defined on the Post struct when marshaled
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
if relation == "comments" { if relation == "comments" {
return &Meta{ return &Meta{
"details": "comment meta details here", "this": map[string]interface{}{
"can": map[string]interface{}{
"go": []interface{}{
"as",
"deep",
map[string]interface{}{
"as": "required",
},
},
},
},
} }
} }
return nil return nil
} }
``` ```
### Errors ### Errors
This package also implements support for JSON API compatible `errors` payloads using the following types. This package also implements support for JSON API compatible `errors` payloads using the following types.

View File

@ -43,7 +43,7 @@ func exerciseHandler() {
// list // list
req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) req, _ := http.NewRequest(http.MethodGet, "/blogs", nil)
req.Header.Set("Accept", jsonapi.MediaType) req.Header.Set(headerAccept, jsonapi.MediaType)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -60,7 +60,7 @@ func exerciseHandler() {
// show // show
req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil) req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil)
req.Header.Set("Accept", jsonapi.MediaType) req.Header.Set(headerAccept, jsonapi.MediaType)
w = httptest.NewRecorder() w = httptest.NewRecorder()
@ -81,7 +81,7 @@ func exerciseHandler() {
req, _ = http.NewRequest(http.MethodPost, "/blogs", in) req, _ = http.NewRequest(http.MethodPost, "/blogs", in)
req.Header.Set("Accept", jsonapi.MediaType) req.Header.Set(headerAccept, jsonapi.MediaType)
w = httptest.NewRecorder() w = httptest.NewRecorder()
@ -107,7 +107,7 @@ func exerciseHandler() {
req, _ = http.NewRequest(http.MethodPut, "/blogs", in) req, _ = http.NewRequest(http.MethodPut, "/blogs", in)
req.Header.Set("Accept", jsonapi.MediaType) req.Header.Set(headerAccept, jsonapi.MediaType)
w = httptest.NewRecorder() w = httptest.NewRecorder()

View File

@ -3,6 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/google/jsonapi"
) )
type Blog struct { type Blog struct {
@ -30,22 +32,43 @@ type Comment struct {
} }
// Blog Links // Blog Links
func (blog Blog) JSONAPILinks() *map[string]interface{} { func (blog Blog) JSONAPILinks() *jsonapi.Links {
return &map[string]interface{}{ return &jsonapi.Links{
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), "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" { if relation == "posts" {
return &map[string]interface{}{ return &jsonapi.Links{
"related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID),
} }
} }
if relation == "current_post" { if relation == "current_post" {
return &map[string]interface{}{ return &jsonapi.Links{
"related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID),
} }
} }
return nil 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
}

157
models_test.go Normal file
View File

@ -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"},
}
}

12
node.go
View File

@ -50,10 +50,6 @@ type RelationshipManyNode struct {
// http://jsonapi.org/format/#document-links // http://jsonapi.org/format/#document-links
type Links map[string]interface{} 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) { func (l *Links) validate() (err error) {
// Each member of a links object is a “link”. A link MUST be represented as // Each member of a links object is a “link”. A link MUST be represented as
// either: // either:
@ -79,7 +75,7 @@ func (l *Links) validate() (err error) {
// Link is used to represent a member of the `links` object. // Link is used to represent a member of the `links` object.
type Link struct { type Link struct {
Href string `json:"href"` Href string `json:"href"`
Meta map[string]interface{} `json:"meta,omitempty"` Meta Meta `json:"meta,omitempty"`
} }
// Linkable is used to include document links in response data // Linkable is used to include document links in response data
@ -95,6 +91,12 @@ type RelationshipLinkable interface {
JSONAPIRelationshipLinks(relation string) *Links 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 { type Metable interface {
JSONAPIMeta() *Meta JSONAPIMeta() *Meta
} }

View File

@ -11,26 +11,6 @@ import (
"time" "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) { func TestUnmarshall_attrStringSlice(t *testing.T) {
out := &Book{} out := &Book{}
tags := []string{"fiction", "sale"} tags := []string{"fiction", "sale"}

View File

@ -3,110 +3,12 @@ package jsonapi
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
"time" "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) { func TestMarshall_attrStringSlice(t *testing.T) {
tags := []string{"fiction", "sale"} tags := []string{"fiction", "sale"}
b := &Book{ID: 1, Tags: tags} 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) { func TestMarshalIDPtr(t *testing.T) {
id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang" id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang"
car := &Car{ car := &Car{
@ -529,7 +407,7 @@ func TestSupportsLinkable(t *testing.T) {
data := resp.Data data := resp.Data
if data.Links == nil { if data.Links == nil {
t.Fatal("Expected links") t.Fatal("Expected data.links")
} }
links := *data.Links links := *data.Links
@ -566,7 +444,9 @@ func TestSupportsLinkable(t *testing.T) {
if !isMap { if !isMap {
t.Fatal("Expected 'comments' to contain a map") 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 { if !isMap {
t.Fatal("Expected 'counts' to contain a map") 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) { func TestSupportsMetable(t *testing.T) {
testModel := &Blog{ testModel := &Blog{
ID: 5, ID: 5,
@ -595,21 +487,13 @@ func TestSupportsMetable(t *testing.T) {
} }
data := resp.Data data := resp.Data
if data.Meta == nil { if data.Meta == nil {
t.Fatalf("Expected 'details' meta") t.Fatalf("Expected data.meta")
}
}
func TestInvalidLinkable(t *testing.T) {
testModel := &BadComment{
ID: 5,
Body: "Hello World",
} }
out := bytes.NewBuffer(nil) meta := Meta(*data.Meta)
if err := MarshalOnePayload(out, testModel); err == nil { if e, a := "extra details regarding the blog", meta["detail"]; e != a {
t.Fatal("Was expecting an error") t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a)
} }
} }