forked from Mirrors/jsonapi
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:
parent
9babeb5aea
commit
0a2decba43
48
README.md
48
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.
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
}
|
14
node.go
14
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
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
156
response_test.go
156
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue