Skip to content

Commit 2b01775

Browse files
committed
Merge branch 'feature/meta'
2 parents 6958c3a + 5700835 commit 2b01775

File tree

8 files changed

+296
-136
lines changed

8 files changed

+296
-136
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![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)
44

5-
A serializer/deserializer for json payloads that comply to the
5+
A serializer/deserializer for JSON payloads that comply to the
66
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
77

88
## Installation
@@ -364,6 +364,38 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *Links {
364364
}
365365
```
366366

367+
### Meta
368+
369+
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:
370+
371+
```go
372+
func (post Post) JSONAPIMeta() *Meta {
373+
return &Meta{
374+
"details": "sample details here",
375+
}
376+
}
377+
378+
// Invoked for each relationship defined on the Post struct when marshaled
379+
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
380+
if relation == "comments" {
381+
return &Meta{
382+
"this": map[string]interface{}{
383+
"can": map[string]interface{}{
384+
"go": []interface{}{
385+
"as",
386+
"deep",
387+
map[string]interface{}{
388+
"as": "required",
389+
},
390+
},
391+
},
392+
},
393+
}
394+
}
395+
return nil
396+
}
397+
```
398+
367399
### Errors
368400
This package also implements support for JSON API compatible `errors` payloads using the following types.
369401

examples/app.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func exerciseHandler() {
4343
// list
4444
req, _ := http.NewRequest(http.MethodGet, "/blogs", nil)
4545

46-
req.Header.Set("Accept", jsonapi.MediaType)
46+
req.Header.Set(headerAccept, jsonapi.MediaType)
4747

4848
w := httptest.NewRecorder()
4949

@@ -60,7 +60,7 @@ func exerciseHandler() {
6060
// show
6161
req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil)
6262

63-
req.Header.Set("Accept", jsonapi.MediaType)
63+
req.Header.Set(headerAccept, jsonapi.MediaType)
6464

6565
w = httptest.NewRecorder()
6666

@@ -81,7 +81,7 @@ func exerciseHandler() {
8181

8282
req, _ = http.NewRequest(http.MethodPost, "/blogs", in)
8383

84-
req.Header.Set("Accept", jsonapi.MediaType)
84+
req.Header.Set(headerAccept, jsonapi.MediaType)
8585

8686
w = httptest.NewRecorder()
8787

@@ -107,7 +107,7 @@ func exerciseHandler() {
107107

108108
req, _ = http.NewRequest(http.MethodPut, "/blogs", in)
109109

110-
req.Header.Set("Accept", jsonapi.MediaType)
110+
req.Header.Set(headerAccept, jsonapi.MediaType)
111111

112112
w = httptest.NewRecorder()
113113

examples/models.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package main
33
import (
44
"fmt"
55
"time"
6+
7+
"github.com/google/jsonapi"
68
)
79

810
type Blog struct {
@@ -30,22 +32,43 @@ type Comment struct {
3032
}
3133

3234
// Blog Links
33-
func (blog Blog) JSONAPILinks() *map[string]interface{} {
34-
return &map[string]interface{}{
35+
func (blog Blog) JSONAPILinks() *jsonapi.Links {
36+
return &jsonapi.Links{
3537
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID),
3638
}
3739
}
3840

39-
func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} {
41+
func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links {
4042
if relation == "posts" {
41-
return &map[string]interface{}{
43+
return &jsonapi.Links{
4244
"related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID),
4345
}
4446
}
4547
if relation == "current_post" {
46-
return &map[string]interface{}{
48+
return &jsonapi.Links{
4749
"related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID),
4850
}
4951
}
5052
return nil
5153
}
54+
55+
// Blog Meta
56+
func (blog Blog) JSONAPIMeta() *jsonapi.Meta {
57+
return &jsonapi.Meta{
58+
"detail": "extra details regarding the blog",
59+
}
60+
}
61+
62+
func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta {
63+
if relation == "posts" {
64+
return &jsonapi.Meta{
65+
"detail": "posts meta information",
66+
}
67+
}
68+
if relation == "current_post" {
69+
return &jsonapi.Meta{
70+
"detail": "current post meta information",
71+
}
72+
}
73+
return nil
74+
}

models_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package jsonapi
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
type BadModel struct {
9+
ID int `jsonapi:"primary"`
10+
}
11+
12+
type ModelBadTypes struct {
13+
ID string `jsonapi:"primary,badtypes"`
14+
StringField string `jsonapi:"attr,string_field"`
15+
FloatField float64 `jsonapi:"attr,float_field"`
16+
TimeField time.Time `jsonapi:"attr,time_field"`
17+
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
18+
}
19+
20+
type WithPointer struct {
21+
ID *uint64 `jsonapi:"primary,with-pointers"`
22+
Name *string `jsonapi:"attr,name"`
23+
IsActive *bool `jsonapi:"attr,is-active"`
24+
IntVal *int `jsonapi:"attr,int-val"`
25+
FloatVal *float32 `jsonapi:"attr,float-val"`
26+
}
27+
28+
type Timestamp struct {
29+
ID int `jsonapi:"primary,timestamps"`
30+
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
31+
Next *time.Time `jsonapi:"attr,next,iso8601"`
32+
}
33+
34+
type Car struct {
35+
ID *string `jsonapi:"primary,cars"`
36+
Make *string `jsonapi:"attr,make,omitempty"`
37+
Model *string `jsonapi:"attr,model,omitempty"`
38+
Year *uint `jsonapi:"attr,year,omitempty"`
39+
}
40+
41+
type Post struct {
42+
Blog
43+
ID uint64 `jsonapi:"primary,posts"`
44+
BlogID int `jsonapi:"attr,blog_id"`
45+
ClientID string `jsonapi:"client-id"`
46+
Title string `jsonapi:"attr,title"`
47+
Body string `jsonapi:"attr,body"`
48+
Comments []*Comment `jsonapi:"relation,comments"`
49+
LatestComment *Comment `jsonapi:"relation,latest_comment"`
50+
}
51+
52+
type Comment struct {
53+
ID int `jsonapi:"primary,comments"`
54+
ClientID string `jsonapi:"client-id"`
55+
PostID int `jsonapi:"attr,post_id"`
56+
Body string `jsonapi:"attr,body"`
57+
}
58+
59+
type Book struct {
60+
ID uint64 `jsonapi:"primary,books"`
61+
Author string `jsonapi:"attr,author"`
62+
ISBN string `jsonapi:"attr,isbn"`
63+
Title string `jsonapi:"attr,title,omitempty"`
64+
Description *string `jsonapi:"attr,description"`
65+
Pages *uint `jsonapi:"attr,pages,omitempty"`
66+
PublishedAt time.Time
67+
Tags []string `jsonapi:"attr,tags"`
68+
}
69+
70+
type Blog struct {
71+
ID int `jsonapi:"primary,blogs"`
72+
ClientID string `jsonapi:"client-id"`
73+
Title string `jsonapi:"attr,title"`
74+
Posts []*Post `jsonapi:"relation,posts"`
75+
CurrentPost *Post `jsonapi:"relation,current_post"`
76+
CurrentPostID int `jsonapi:"attr,current_post_id"`
77+
CreatedAt time.Time `jsonapi:"attr,created_at"`
78+
ViewCount int `jsonapi:"attr,view_count"`
79+
}
80+
81+
func (b *Blog) JSONAPILinks() *Links {
82+
return &Links{
83+
"self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID),
84+
"comments": Link{
85+
Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID),
86+
Meta: Meta{
87+
"counts": map[string]uint{
88+
"likes": 4,
89+
"comments": 20,
90+
},
91+
},
92+
},
93+
}
94+
}
95+
96+
func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links {
97+
if relation == "posts" {
98+
return &Links{
99+
"related": Link{
100+
Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID),
101+
Meta: Meta{
102+
"count": len(b.Posts),
103+
},
104+
},
105+
}
106+
}
107+
if relation == "current_post" {
108+
return &Links{
109+
"self": fmt.Sprintf("https://example.com/api/posts/%s", "3"),
110+
"related": Link{
111+
Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID),
112+
},
113+
}
114+
}
115+
return nil
116+
}
117+
118+
func (b *Blog) JSONAPIMeta() *Meta {
119+
return &Meta{
120+
"detail": "extra details regarding the blog",
121+
}
122+
}
123+
124+
func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta {
125+
if relation == "posts" {
126+
return &Meta{
127+
"this": map[string]interface{}{
128+
"can": map[string]interface{}{
129+
"go": []interface{}{
130+
"as",
131+
"deep",
132+
map[string]interface{}{
133+
"as": "required",
134+
},
135+
},
136+
},
137+
},
138+
}
139+
}
140+
if relation == "current_post" {
141+
return &Meta{
142+
"detail": "extra current_post detail",
143+
}
144+
}
145+
return nil
146+
}
147+
148+
type BadComment struct {
149+
ID uint64 `jsonapi:"primary,bad-comment"`
150+
Body string `jsonapi:"attr,body"`
151+
}
152+
153+
func (bc *BadComment) JSONAPILinks() *Links {
154+
return &Links{
155+
"self": []string{"invalid", "should error"},
156+
}
157+
}

node.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type OnePayload struct {
88
Data *Node `json:"data"`
99
Included []*Node `json:"included,omitempty"`
1010
Links *Links `json:"links,omitempty"`
11+
Meta *Meta `json:"meta,omitempty"`
1112
}
1213

1314
// ManyPayload is used to represent a generic JSON API payload where many
@@ -16,6 +17,7 @@ type ManyPayload struct {
1617
Data []*Node `json:"data"`
1718
Included []*Node `json:"included,omitempty"`
1819
Links *Links `json:"links,omitempty"`
20+
Meta *Meta `json:"meta,omitempty"`
1921
}
2022

2123
// Node is used to represent a generic JSON API Resource
@@ -26,19 +28,22 @@ type Node struct {
2628
Attributes map[string]interface{} `json:"attributes,omitempty"`
2729
Relationships map[string]interface{} `json:"relationships,omitempty"`
2830
Links *Links `json:"links,omitempty"`
31+
Meta *Meta `json:"meta,omitempty"`
2932
}
3033

3134
// RelationshipOneNode is used to represent a generic has one JSON API relation
3235
type RelationshipOneNode struct {
3336
Data *Node `json:"data"`
3437
Links *Links `json:"links,omitempty"`
38+
Meta *Meta `json:"meta,omitempty"`
3539
}
3640

3741
// RelationshipManyNode is used to represent a generic has many JSON API
3842
// relation
3943
type RelationshipManyNode struct {
4044
Data []*Node `json:"data"`
4145
Links *Links `json:"links,omitempty"`
46+
Meta *Meta `json:"meta,omitempty"`
4247
}
4348

4449
// Links is used to represent a `links` object.
@@ -69,8 +74,8 @@ func (l *Links) validate() (err error) {
6974

7075
// Link is used to represent a member of the `links` object.
7176
type Link struct {
72-
Href string `json:"href"`
73-
Meta map[string]interface{} `json:"meta,omitempty"`
77+
Href string `json:"href"`
78+
Meta Meta `json:"meta,omitempty"`
7479
}
7580

7681
// Linkable is used to include document links in response data
@@ -85,3 +90,19 @@ type RelationshipLinkable interface {
8590
// JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
8691
JSONAPIRelationshipLinks(relation string) *Links
8792
}
93+
94+
// Meta is used to represent a `meta` object.
95+
// http://jsonapi.org/format/#document-meta
96+
type Meta map[string]interface{}
97+
98+
// Metable is used to include document meta in response data
99+
// e.g. {"foo": "bar"}
100+
type Metable interface {
101+
JSONAPIMeta() *Meta
102+
}
103+
104+
// RelationshipMetable is used to include relationship meta in response data
105+
type RelationshipMetable interface {
106+
// JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
107+
JSONAPIRelationshipMeta(relation string) *Meta
108+
}

request_test.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,6 @@ import (
1111
"time"
1212
)
1313

14-
type BadModel struct {
15-
ID int `jsonapi:"primary"`
16-
}
17-
18-
type WithPointer struct {
19-
ID *uint64 `jsonapi:"primary,with-pointers"`
20-
Name *string `jsonapi:"attr,name"`
21-
IsActive *bool `jsonapi:"attr,is-active"`
22-
IntVal *int `jsonapi:"attr,int-val"`
23-
FloatVal *float32 `jsonapi:"attr,float-val"`
24-
}
25-
26-
type ModelBadTypes struct {
27-
ID string `jsonapi:"primary,badtypes"`
28-
StringField string `jsonapi:"attr,string_field"`
29-
FloatField float64 `jsonapi:"attr,float_field"`
30-
TimeField time.Time `jsonapi:"attr,time_field"`
31-
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
32-
}
33-
3414
func TestUnmarshall_attrStringSlice(t *testing.T) {
3515
out := &Book{}
3616
tags := []string{"fiction", "sale"}

0 commit comments

Comments
 (0)