forked from Mirrors/jsonapi
Merge branch 'feature/meta'
This commit is contained in:
commit
2b01775d0f
34
README.md
34
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)
|
[![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
|
||||||
|
@ -364,6 +364,38 @@ 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{
|
||||||
|
"this": map[string]interface{}{
|
||||||
|
"can": map[string]interface{}{
|
||||||
|
"go": []interface{}{
|
||||||
|
"as",
|
||||||
|
"deep",
|
||||||
|
map[string]interface{}{
|
||||||
|
"as": "required",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"},
|
||||||
|
}
|
||||||
|
}
|
25
node.go
25
node.go
|
@ -8,6 +8,7 @@ type OnePayload struct {
|
||||||
Data *Node `json:"data"`
|
Data *Node `json:"data"`
|
||||||
Included []*Node `json:"included,omitempty"`
|
Included []*Node `json:"included,omitempty"`
|
||||||
Links *Links `json:"links,omitempty"`
|
Links *Links `json:"links,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManyPayload is used to represent a generic JSON API payload where many
|
// ManyPayload is used to represent a generic JSON API payload where many
|
||||||
|
@ -16,6 +17,7 @@ type ManyPayload struct {
|
||||||
Data []*Node `json:"data"`
|
Data []*Node `json:"data"`
|
||||||
Included []*Node `json:"included,omitempty"`
|
Included []*Node `json:"included,omitempty"`
|
||||||
Links *Links `json:"links,omitempty"`
|
Links *Links `json:"links,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node is used to represent a generic JSON API Resource
|
// Node is used to represent a generic JSON API Resource
|
||||||
|
@ -26,12 +28,14 @@ type Node struct {
|
||||||
Attributes map[string]interface{} `json:"attributes,omitempty"`
|
Attributes map[string]interface{} `json:"attributes,omitempty"`
|
||||||
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
||||||
Links *Links `json:"links,omitempty"`
|
Links *Links `json:"links,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
||||||
type RelationshipOneNode struct {
|
type RelationshipOneNode struct {
|
||||||
Data *Node `json:"data"`
|
Data *Node `json:"data"`
|
||||||
Links *Links `json:"links,omitempty"`
|
Links *Links `json:"links,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelationshipManyNode is used to represent a generic has many JSON API
|
// RelationshipManyNode is used to represent a generic has many JSON API
|
||||||
|
@ -39,6 +43,7 @@ type RelationshipOneNode struct {
|
||||||
type RelationshipManyNode struct {
|
type RelationshipManyNode struct {
|
||||||
Data []*Node `json:"data"`
|
Data []*Node `json:"data"`
|
||||||
Links *Links `json:"links,omitempty"`
|
Links *Links `json:"links,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Links is used to represent a `links` object.
|
// Links is used to represent a `links` object.
|
||||||
|
@ -69,8 +74,8 @@ 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
|
||||||
|
@ -85,3 +90,19 @@ type RelationshipLinkable interface {
|
||||||
// JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
// JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
||||||
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
13
response.go
13
response.go
|
@ -373,6 +373,11 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
|
relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relMeta *Meta
|
||||||
|
if metableModel, ok := model.(RelationshipMetable); ok {
|
||||||
|
relMeta = metableModel.JSONAPIRelationshipMeta(args[1])
|
||||||
|
}
|
||||||
|
|
||||||
if isSlice {
|
if isSlice {
|
||||||
// to-many relationship
|
// to-many relationship
|
||||||
relationship, err := visitModelNodeRelationships(
|
relationship, err := visitModelNodeRelationships(
|
||||||
|
@ -385,6 +390,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
relationship.Links = relLinks
|
relationship.Links = relLinks
|
||||||
|
relationship.Meta = relMeta
|
||||||
|
|
||||||
if sideload {
|
if sideload {
|
||||||
shallowNodes := []*Node{}
|
shallowNodes := []*Node{}
|
||||||
|
@ -396,6 +402,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
node.Relationships[args[1]] = &RelationshipManyNode{
|
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||||
Data: shallowNodes,
|
Data: shallowNodes,
|
||||||
Links: relationship.Links,
|
Links: relationship.Links,
|
||||||
|
Meta: relationship.Meta,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
node.Relationships[args[1]] = relationship
|
node.Relationships[args[1]] = relationship
|
||||||
|
@ -424,11 +431,13 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||||
Data: toShallowNode(relationship),
|
Data: toShallowNode(relationship),
|
||||||
Links: relLinks,
|
Links: relLinks,
|
||||||
|
Meta: relMeta,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||||
Data: relationship,
|
Data: relationship,
|
||||||
Links: relLinks,
|
Links: relLinks,
|
||||||
|
Meta: relMeta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,6 +460,10 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
node.Links = linkableModel.JSONAPILinks()
|
node.Links = linkableModel.JSONAPILinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if metableModel, ok := model.(Metable); ok {
|
||||||
|
node.Meta = metableModel.JSONAPIMeta()
|
||||||
|
}
|
||||||
|
|
||||||
return node, nil
|
return node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
142
response_test.go
142
response_test.go
|
@ -3,90 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
||||||
|
@ -244,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{
|
||||||
|
@ -509,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
|
||||||
|
|
||||||
|
@ -546,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")
|
||||||
}
|
}
|
||||||
|
@ -569,6 +469,34 @@ func TestInvalidLinkable(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 data.meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRelations(t *testing.T) {
|
func TestRelations(t *testing.T) {
|
||||||
testModel := testBlog()
|
testModel := testBlog()
|
||||||
|
|
||||||
|
@ -594,6 +522,9 @@ func TestRelations(t *testing.T) {
|
||||||
if relations["posts"].(map[string]interface{})["links"] == nil {
|
if relations["posts"].(map[string]interface{})["links"] == nil {
|
||||||
t.Fatalf("Posts relationship links were not materialized")
|
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 {
|
if relations["current_post"] == nil {
|
||||||
|
@ -602,6 +533,9 @@ func TestRelations(t *testing.T) {
|
||||||
if relations["current_post"].(map[string]interface{})["links"] == nil {
|
if relations["current_post"].(map[string]interface{})["links"] == nil {
|
||||||
t.Fatalf("Current post relationship links were not materialized")
|
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 {
|
if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 {
|
||||||
|
|
Loading…
Reference in New Issue