Skip to content

Commit cffec19

Browse files
icgoodwwwdata
authored andcommitted
Allow elements to implement custom links (#278)
* Custom marshaler for string links
1 parent 9ec7fc6 commit cffec19

File tree

6 files changed

+223
-40
lines changed

6 files changed

+223
-40
lines changed

api.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (p paginationQueryParams) isValid() bool {
7070
}
7171

7272
func (p paginationQueryParams) getLinks(r *http.Request, count uint, info information) (result jsonapi.Links, err error) {
73-
result = jsonapi.Links{}
73+
result = make(jsonapi.Links)
7474

7575
params := r.URL.Query()
7676
prefix := ""
@@ -91,11 +91,11 @@ func (p paginationQueryParams) getLinks(r *http.Request, count uint, info inform
9191
if p.number != "1" {
9292
params.Set("page[number]", "1")
9393
query, _ := url.QueryUnescape(params.Encode())
94-
result.First = fmt.Sprintf("%s?%s", requestURL, query)
94+
result["first"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
9595

9696
params.Set("page[number]", strconv.FormatUint(number-1, 10))
9797
query, _ = url.QueryUnescape(params.Encode())
98-
result.Previous = fmt.Sprintf("%s?%s", requestURL, query)
98+
result["prev"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
9999
}
100100

101101
// calculate last page number
@@ -113,11 +113,11 @@ func (p paginationQueryParams) getLinks(r *http.Request, count uint, info inform
113113
if number != totalPages {
114114
params.Set("page[number]", strconv.FormatUint(number+1, 10))
115115
query, _ := url.QueryUnescape(params.Encode())
116-
result.Next = fmt.Sprintf("%s?%s", requestURL, query)
116+
result["next"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
117117

118118
params.Set("page[number]", strconv.FormatUint(totalPages, 10))
119119
query, _ = url.QueryUnescape(params.Encode())
120-
result.Last = fmt.Sprintf("%s?%s", requestURL, query)
120+
result["last"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
121121
}
122122
} else {
123123
// we have offset & limit params
@@ -134,7 +134,7 @@ func (p paginationQueryParams) getLinks(r *http.Request, count uint, info inform
134134
if p.offset != "0" {
135135
params.Set("page[offset]", "0")
136136
query, _ := url.QueryUnescape(params.Encode())
137-
result.First = fmt.Sprintf("%s?%s", requestURL, query)
137+
result["first"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
138138

139139
var prevOffset uint64
140140
if limit > offset {
@@ -144,18 +144,18 @@ func (p paginationQueryParams) getLinks(r *http.Request, count uint, info inform
144144
}
145145
params.Set("page[offset]", strconv.FormatUint(prevOffset, 10))
146146
query, _ = url.QueryUnescape(params.Encode())
147-
result.Previous = fmt.Sprintf("%s?%s", requestURL, query)
147+
result["prev"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
148148
}
149149

150150
// check if there are more entries to be loaded
151151
if (offset + limit) < uint64(count) {
152152
params.Set("page[offset]", strconv.FormatUint(offset+limit, 10))
153153
query, _ := url.QueryUnescape(params.Encode())
154-
result.Next = fmt.Sprintf("%s?%s", requestURL, query)
154+
result["next"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
155155

156156
params.Set("page[offset]", strconv.FormatUint(uint64(count)-limit, 10))
157157
query, _ = url.QueryUnescape(params.Encode())
158-
result.Last = fmt.Sprintf("%s?%s", requestURL, query)
158+
result["last"] = jsonapi.Link{Href: fmt.Sprintf("%s?%s", requestURL, query)}
159159
}
160160
}
161161

@@ -926,7 +926,7 @@ func (res *resource) respondWithPagination(obj Responder, info information, stat
926926
return err
927927
}
928928

929-
data.Links = &links
929+
data.Links = links
930930
meta := obj.Metadata()
931931
if len(meta) > 0 {
932932
data.Meta = meta

jsonapi/data_structs.go

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import (
88

99
var objectSuffix = []byte("{")
1010
var arraySuffix = []byte("[")
11+
var stringSuffix = []byte(`"`)
1112

1213
// A Document represents a JSON API document as specified here: http://jsonapi.org.
1314
type Document struct {
14-
Links *Links `json:"links,omitempty"`
15+
Links Links `json:"links,omitempty"`
1516
Data *DataContainer `json:"data"`
1617
Included []Data `json:"included,omitempty"`
1718
Meta map[string]interface{} `json:"meta,omitempty"`
@@ -48,28 +49,64 @@ func (c *DataContainer) MarshalJSON() ([]byte, error) {
4849
return json.Marshal(c.DataObject)
4950
}
5051

51-
// Links is a general struct for document links and relationship links.
52-
type Links struct {
53-
Self string `json:"self,omitempty"`
54-
Related string `json:"related,omitempty"`
55-
First string `json:"first,omitempty"`
56-
Previous string `json:"prev,omitempty"`
57-
Next string `json:"next,omitempty"`
58-
Last string `json:"last,omitempty"`
52+
// Link represents a link for return in the document.
53+
type Link struct {
54+
Href string `json:"href"`
55+
Meta map[string]interface{} `json:"meta,omitempty"`
5956
}
6057

58+
// UnmarshalJSON marshals a string value into the Href field or marshals an
59+
// object value into the whole struct.
60+
func (l *Link) UnmarshalJSON(payload []byte) error {
61+
if bytes.HasPrefix(payload, stringSuffix) {
62+
return json.Unmarshal(payload, &l.Href)
63+
}
64+
65+
if bytes.HasPrefix(payload, objectSuffix) {
66+
obj := make(map[string]interface{})
67+
err := json.Unmarshal(payload, &obj)
68+
if err != nil {
69+
return err
70+
}
71+
var ok bool
72+
l.Href, ok = obj["href"].(string)
73+
if !ok {
74+
return errors.New(`link object expects a "href" key`)
75+
}
76+
l.Meta, _ = obj["meta"].(map[string]interface{})
77+
return nil
78+
}
79+
80+
return errors.New("expected a JSON encoded string or object")
81+
}
82+
83+
// MarshalJSON returns the JSON encoding of only the Href field if the Meta
84+
// field is empty, otherwise it marshals the whole struct.
85+
func (l Link) MarshalJSON() ([]byte, error) {
86+
if len(l.Meta) == 0 {
87+
return json.Marshal(l.Href)
88+
}
89+
return json.Marshal(map[string]interface{}{
90+
"href": l.Href,
91+
"meta": l.Meta,
92+
})
93+
}
94+
95+
// Links contains a map of custom Link objects as given by an element.
96+
type Links map[string]Link
97+
6198
// Data is a general struct for document data and included data.
6299
type Data struct {
63100
Type string `json:"type"`
64101
ID string `json:"id"`
65102
Attributes json.RawMessage `json:"attributes"`
66103
Relationships map[string]Relationship `json:"relationships,omitempty"`
67-
Links *Links `json:"links,omitempty"`
104+
Links Links `json:"links,omitempty"`
68105
}
69106

70107
// Relationship contains reference IDs to the related structs
71108
type Relationship struct {
72-
Links *Links `json:"links,omitempty"`
109+
Links Links `json:"links,omitempty"`
73110
Data *RelationshipDataContainer `json:"data,omitempty"`
74111
Meta map[string]interface{} `json:"meta,omitempty"`
75112
}

jsonapi/data_structs_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,76 @@ var _ = Describe("JSONAPI Struct tests", func() {
160160
Expect(err).ToNot(HaveOccurred())
161161
Expect(target.Data.DataArray).To(Equal([]Data{expectedData}))
162162
})
163+
164+
Context("Marshal and Unmarshal link structs", func() {
165+
It("marshals to a string with no metadata", func() {
166+
link := Link{Href: "test link"}
167+
ret, err := json.Marshal(&link)
168+
Expect(err).ToNot(HaveOccurred())
169+
Expect(ret).To(MatchJSON(`"test link"`))
170+
})
171+
172+
It("marshals to an object with metadata", func() {
173+
link := Link{
174+
Href: "test link",
175+
Meta: map[string]interface{}{
176+
"test": "data",
177+
},
178+
}
179+
ret, err := json.Marshal(&link)
180+
Expect(err).ToNot(HaveOccurred())
181+
Expect(ret).To(MatchJSON(`{
182+
"href": "test link",
183+
"meta": {"test": "data"}
184+
}`))
185+
})
186+
187+
It("unmarshals from a string", func() {
188+
expected := Link{Href: "test link"}
189+
target := Link{}
190+
err := json.Unmarshal([]byte(`"test link"`), &target)
191+
Expect(err).ToNot(HaveOccurred())
192+
Expect(target).To(Equal(expected))
193+
})
194+
195+
It("unmarshals from an object", func() {
196+
expected := Link{
197+
Href: "test link",
198+
Meta: map[string]interface{}{
199+
"test": "data",
200+
},
201+
}
202+
target := Link{}
203+
err := json.Unmarshal([]byte(`{
204+
"href": "test link",
205+
"meta": {"test": "data"}
206+
}`), &target)
207+
Expect(err).ToNot(HaveOccurred())
208+
Expect(target).To(Equal(expected))
209+
})
210+
211+
It("unmarshals with an error when href is missing", func() {
212+
err := json.Unmarshal([]byte(`{}`), &Link{})
213+
Expect(err).To(HaveOccurred())
214+
Expect(err.Error()).To(Equal(`link object expects a "href" key`))
215+
})
216+
217+
It("unmarshals with an error for syntax error", func() {
218+
badPayloads := []string{`{`, `"`}
219+
for _, payload := range badPayloads {
220+
err := json.Unmarshal([]byte(payload), &Link{})
221+
Expect(err).To(HaveOccurred())
222+
Expect(err.Error()).To(Equal("unexpected end of JSON input"))
223+
}
224+
})
225+
226+
It("unmarshals with an error for wrong types", func() {
227+
badPayloads := []string{`null`, `13`, `[]`}
228+
for _, payload := range badPayloads {
229+
err := json.Unmarshal([]byte(payload), &Link{})
230+
Expect(err).To(HaveOccurred())
231+
Expect(err.Error()).To(Equal("expected a JSON encoded string or object"))
232+
}
233+
})
234+
})
163235
})

jsonapi/fixtures_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,32 @@ func (i PrefixServerInformation) GetPrefix() string {
440440
return prefix
441441
}
442442

443+
type CustomLinksPost struct{}
444+
445+
func (n CustomLinksPost) GetID() string {
446+
return "someID"
447+
}
448+
449+
func (n *CustomLinksPost) SetID(ID string) error {
450+
return nil
451+
}
452+
453+
func (n CustomLinksPost) GetName() string {
454+
return "posts"
455+
}
456+
457+
func (n CustomLinksPost) GetCustomLinks(base string) Links {
458+
return Links{
459+
"someLink": Link{Href: base + `/someLink`},
460+
"otherLink": Link{
461+
Href: base + `/otherLink`,
462+
Meta: map[string]interface{}{
463+
"method": "GET",
464+
},
465+
},
466+
}
467+
}
468+
443469
type NoRelationshipPosts struct{}
444470

445471
func (n NoRelationshipPosts) GetID() string {

jsonapi/marshal.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ type MarshalIncludedRelations interface {
7373
GetReferencedStructs() []MarshalIdentifier
7474
}
7575

76+
// The MarshalCustomLinks interface can be implemented if the struct should
77+
// want any custom links.
78+
type MarshalCustomLinks interface {
79+
MarshalIdentifier
80+
GetCustomLinks(string) Links
81+
}
82+
7683
// A ServerInformation implementor can be passed to MarshalWithURLs to generate
7784
// the `self` and `related` urls inside `links`.
7885
type ServerInformation interface {
@@ -204,6 +211,20 @@ func marshalData(element MarshalIdentifier, data *Data, information ServerInform
204211
data.ID = element.GetID()
205212
data.Type = getStructType(element)
206213

214+
if information != nil {
215+
if customLinks, ok := element.(MarshalCustomLinks); ok {
216+
if data.Links == nil {
217+
data.Links = make(Links)
218+
}
219+
base := getLinkBaseURL(element, information)
220+
for k, v := range customLinks.GetCustomLinks(base) {
221+
if _, ok := data.Links[k]; !ok {
222+
data.Links[k] = v
223+
}
224+
}
225+
}
226+
}
227+
207228
if references, ok := element.(MarshalLinkedRelations); ok {
208229
data.Relationships = getStructRelationships(references, information)
209230
}
@@ -297,23 +318,28 @@ func getStructRelationships(relationer MarshalLinkedRelations, information Serve
297318
return relationships
298319
}
299320

300-
func getLinksForServerInformation(relationer MarshalLinkedRelations, name string, information ServerInformation) *Links {
301-
if information == nil {
302-
return nil
303-
}
304-
305-
links := &Links{}
306-
321+
func getLinkBaseURL(element MarshalIdentifier, information ServerInformation) string {
307322
prefix := strings.Trim(information.GetBaseURL(), "/")
308323
namespace := strings.Trim(information.GetPrefix(), "/")
309-
structType := getStructType(relationer)
324+
structType := getStructType(element)
310325

311326
if namespace != "" {
312327
prefix += "/" + namespace
313328
}
314329

315-
links.Self = fmt.Sprintf("%s/%s/%s/relationships/%s", prefix, structType, relationer.GetID(), name)
316-
links.Related = fmt.Sprintf("%s/%s/%s/%s", prefix, structType, relationer.GetID(), name)
330+
return fmt.Sprintf("%s/%s/%s", prefix, structType, element.GetID())
331+
}
332+
333+
func getLinksForServerInformation(relationer MarshalLinkedRelations, name string, information ServerInformation) Links {
334+
if information == nil {
335+
return nil
336+
}
337+
338+
links := make(Links)
339+
base := getLinkBaseURL(relationer, information)
340+
341+
links["self"] = Link{Href: fmt.Sprintf("%s/relationships/%s", base, name)}
342+
links["related"] = Link{Href: fmt.Sprintf("%s/%s", base, name)}
317343

318344
return links
319345
}

0 commit comments

Comments
 (0)