Skip to content

Commit 4d7520e

Browse files
committed
Initial commit
1 parent 3203d59 commit 4d7520e

22 files changed

+4167
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/

.travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: go
2+
go:
3+
- 1.6
4+
- 1.7.x
5+
- master
6+
install:
7+
- go get github.com/Masterminds/glide
8+
- glide install
9+
script:
10+
- go test -v $(glide nv)
11+
matrix:
12+
allow_failures:
13+
- go: master

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## JSONAPI support for go
2+
3+
This is a blatant copy of api2go's JSONAPI implementation adding support for sparse fieldset right in the marshaller.
4+
5+
For the original code, see [api2go's repo](https://github.com/manyminds/api2go).

data_structs.go

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package jsonapi
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"net/url"
8+
"regexp"
9+
"strings"
10+
)
11+
12+
var objectSuffix = []byte("{")
13+
var arraySuffix = []byte("[")
14+
15+
// A Document represents a JSON API document as specified here: http://jsonapi.org.
16+
type Document struct {
17+
Links *Links `json:"links,omitempty"`
18+
Data *DataContainer `json:"data"`
19+
Included []Data `json:"included,omitempty"`
20+
Meta map[string]interface{} `json:"meta,omitempty"`
21+
}
22+
23+
// A DataContainer is used to marshal and unmarshal single objects and arrays
24+
// of objects.
25+
type DataContainer struct {
26+
DataObject *Data
27+
DataArray []Data
28+
}
29+
30+
// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
31+
// root element is an object or to the DataArray field for arrays.
32+
func (c *DataContainer) UnmarshalJSON(payload []byte) error {
33+
if bytes.HasPrefix(payload, objectSuffix) {
34+
return json.Unmarshal(payload, &c.DataObject)
35+
}
36+
37+
if bytes.HasPrefix(payload, arraySuffix) {
38+
return json.Unmarshal(payload, &c.DataArray)
39+
}
40+
41+
return errors.New("expected a JSON encoded object or array")
42+
}
43+
44+
// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
45+
// field. It will return "null" if neither of them is set.
46+
func (c *DataContainer) MarshalJSON() ([]byte, error) {
47+
if c.DataArray != nil {
48+
return json.Marshal(c.DataArray)
49+
}
50+
51+
return json.Marshal(c.DataObject)
52+
}
53+
54+
// Links is a general struct for document links and relationship links.
55+
type Links struct {
56+
Self string `json:"self,omitempty"`
57+
Related string `json:"related,omitempty"`
58+
First string `json:"first,omitempty"`
59+
Previous string `json:"prev,omitempty"`
60+
Next string `json:"next,omitempty"`
61+
Last string `json:"last,omitempty"`
62+
}
63+
64+
// Data is a general struct for document data and included data.
65+
type Data struct {
66+
Type string `json:"type"`
67+
ID string `json:"id"`
68+
Attributes json.RawMessage `json:"attributes"`
69+
Relationships map[string]Relationship `json:"relationships,omitempty"`
70+
Links *Links `json:"links,omitempty"`
71+
}
72+
73+
// Relationship contains reference IDs to the related structs
74+
type Relationship struct {
75+
Links *Links `json:"links,omitempty"`
76+
Data *RelationshipDataContainer `json:"data,omitempty"`
77+
Meta map[string]interface{} `json:"meta,omitempty"`
78+
}
79+
80+
// A RelationshipDataContainer is used to marshal and unmarshal single relationship
81+
// objects and arrays of relationship objects.
82+
type RelationshipDataContainer struct {
83+
DataObject *RelationshipData
84+
DataArray []RelationshipData
85+
}
86+
87+
// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
88+
// root element is an object or to the DataArray field for arrays.
89+
func (c *RelationshipDataContainer) UnmarshalJSON(payload []byte) error {
90+
if bytes.HasPrefix(payload, objectSuffix) {
91+
// payload is an object
92+
return json.Unmarshal(payload, &c.DataObject)
93+
}
94+
95+
if bytes.HasPrefix(payload, arraySuffix) {
96+
// payload is an array
97+
return json.Unmarshal(payload, &c.DataArray)
98+
}
99+
100+
return errors.New("Invalid json for relationship data array/object")
101+
}
102+
103+
// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
104+
// field. It will return "null" if neither of them is set.
105+
func (c *RelationshipDataContainer) MarshalJSON() ([]byte, error) {
106+
if c.DataArray != nil {
107+
return json.Marshal(c.DataArray)
108+
}
109+
return json.Marshal(c.DataObject)
110+
}
111+
112+
// RelationshipData represents one specific reference ID.
113+
type RelationshipData struct {
114+
Type string `json:"type"`
115+
ID string `json:"id"`
116+
}
117+
118+
type CustomObject struct {
119+
Fields []string
120+
Object interface{}
121+
}
122+
123+
type FilterFields map[string][]string
124+
125+
func (f FilterFields) ParseQuery(q url.Values) {
126+
rpm := regexp.MustCompile(`(?i)^fields\[([^\]]+)]$`)
127+
128+
for k, v := range q {
129+
matches := rpm.FindStringSubmatch(k)
130+
if len(matches) > 0 {
131+
f[matches[1]] = strings.Split(strings.Join(v, ","), ",")
132+
}
133+
}
134+
}
135+
136+
type ObjectAttributes map[string]interface{}
137+
138+
func (co CustomObject) JSONToStruct() map[string]string {
139+
rpm := regexp.MustCompile(`(?i)^([^,]+)(,|$)`)
140+
res := map[string]string{}
141+
ref := getType(co.Object)
142+
143+
for i := 0; i < ref.NumField(); i++ {
144+
f := ref.Field(i)
145+
tag, ok := f.Tag.Lookup("json")
146+
if ok {
147+
matches := rpm.FindStringSubmatch(tag)
148+
if len(matches) > 0 && matches[1] != "-" {
149+
res[matches[1]] = f.Name
150+
}
151+
}
152+
}
153+
return res
154+
}
155+
156+
func (co CustomObject) MarshalJSON() ([]byte, error) {
157+
obj := ObjectAttributes{}
158+
dict := co.JSONToStruct()
159+
ref := getValue(co.Object)
160+
161+
for _, f := range co.Fields {
162+
if dict[f] != "" {
163+
obj[f] = ref.FieldByName(dict[f]).Interface()
164+
}
165+
}
166+
167+
b, err := json.Marshal(&obj)
168+
169+
return b, err
170+
}

data_structs_test.go

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package jsonapi
2+
3+
import (
4+
"encoding/json"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
var _ = Describe("JSONAPI Struct tests", func() {
11+
Context("Testing array and object data payload", func() {
12+
It("detects object payload", func() {
13+
sampleJSON := `{
14+
"data": {
15+
"type": "test",
16+
"id": "1",
17+
"attributes": {"foo": "bar"},
18+
"relationships": {
19+
"author": {
20+
"data": {"type": "author", "id": "1"}
21+
}
22+
}
23+
}
24+
}`
25+
26+
expectedData := &Data{
27+
Type: "test",
28+
ID: "1",
29+
Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)),
30+
Relationships: map[string]Relationship{
31+
"author": {
32+
Data: &RelationshipDataContainer{
33+
DataObject: &RelationshipData{
34+
Type: "author",
35+
ID: "1",
36+
},
37+
},
38+
},
39+
},
40+
}
41+
42+
target := Document{}
43+
44+
err := json.Unmarshal([]byte(sampleJSON), &target)
45+
Expect(err).ToNot(HaveOccurred())
46+
Expect(target.Data.DataObject).To(Equal(expectedData))
47+
})
48+
49+
It("detects array payload", func() {
50+
sampleJSON := `{
51+
"data": [
52+
{
53+
"type": "test",
54+
"id": "1",
55+
"attributes": {"foo": "bar"},
56+
"relationships": {
57+
"comments": {
58+
"data": [
59+
{"type": "comments", "id": "1"},
60+
{"type": "comments", "id": "2"}
61+
]
62+
}
63+
}
64+
}
65+
]
66+
}`
67+
68+
expectedData := Data{
69+
Type: "test",
70+
ID: "1",
71+
Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)),
72+
Relationships: map[string]Relationship{
73+
"comments": {
74+
Data: &RelationshipDataContainer{
75+
DataArray: []RelationshipData{
76+
{
77+
Type: "comments",
78+
ID: "1",
79+
},
80+
{
81+
Type: "comments",
82+
ID: "2",
83+
},
84+
},
85+
},
86+
},
87+
},
88+
}
89+
90+
target := Document{}
91+
92+
err := json.Unmarshal([]byte(sampleJSON), &target)
93+
Expect(err).ToNot(HaveOccurred())
94+
Expect(target.Data.DataArray).To(Equal([]Data{expectedData}))
95+
})
96+
})
97+
98+
It("return an error for invalid relationship data format", func() {
99+
sampleJSON := `
100+
{
101+
"data": [
102+
{
103+
"type": "test",
104+
"id": "1",
105+
"attributes": {"foo": "bar"},
106+
"relationships": {
107+
"comments": {
108+
"data": "foo"
109+
}
110+
}
111+
}
112+
]
113+
}`
114+
115+
target := Document{}
116+
117+
err := json.Unmarshal([]byte(sampleJSON), &target)
118+
Expect(err).To(HaveOccurred())
119+
Expect(err.Error()).To(Equal("Invalid json for relationship data array/object"))
120+
})
121+
122+
It("creates an empty slice for empty to-many relationships and nil for empty toOne", func() {
123+
sampleJSON := `{
124+
"data": [
125+
{
126+
"type": "test",
127+
"id": "1",
128+
"attributes": {"foo": "bar"},
129+
"relationships": {
130+
"comments": {
131+
"data": []
132+
},
133+
"author": {
134+
"data": null
135+
}
136+
}
137+
}
138+
]
139+
}`
140+
141+
expectedData := Data{
142+
Type: "test",
143+
ID: "1",
144+
Attributes: json.RawMessage([]byte(`{"foo": "bar"}`)),
145+
Relationships: map[string]Relationship{
146+
"comments": {
147+
Data: &RelationshipDataContainer{
148+
DataArray: []RelationshipData{},
149+
},
150+
},
151+
"author": {
152+
Data: nil,
153+
},
154+
},
155+
}
156+
157+
target := Document{}
158+
159+
err := json.Unmarshal([]byte(sampleJSON), &target)
160+
Expect(err).ToNot(HaveOccurred())
161+
Expect(target.Data.DataArray).To(Equal([]Data{expectedData}))
162+
})
163+
})

entity_namer.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package jsonapi
2+
3+
// The EntityNamer interface can be optionally implemented to directly return the
4+
// name of resource used for the "type" field.
5+
//
6+
// Note: By default the name is guessed from the struct name.
7+
type EntityNamer interface {
8+
GetName() string
9+
}

0 commit comments

Comments
 (0)