Skip to content

Commit

Permalink
Merge pull request #10 from hashicorp/add-links-unmarshaling
Browse files Browse the repository at this point in the history
Unmarshal Links type to 'links' annotated struct fields
  • Loading branch information
chrisarcand authored May 18, 2021
2 parents edf82c9 + 4ff67af commit 1e50d74
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 1 deletion.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ used as the key in the `relationships` hash for the record. The optional
third argument is `omitempty` - if present will prevent non existent to-one and
to-many from being serialized.

#### `links`

*Note: This annotation is an added feature independent of the canonical google/jsonapi package*

```
`jsonapi:"links,omitempty"`
```

A field annotated with `links` will have the links members of the request unmarshaled to it. Note
that this field should _always_ be annotated with `omitempty`, as marshaling of links members is
instead handled by the `Linkable` interface (see `Links` below).

## Methods Reference

**All `Marshal` and `Unmarshal` methods expect pointers to struct
Expand Down
5 changes: 5 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const (
annotationClientID = "client-id"
annotationAttribute = "attr"
annotationRelation = "relation"
annotationLinks = "links"
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationRFC3339 = "rfc3339"
Expand Down Expand Up @@ -53,4 +54,8 @@ const (
// QueryParamPageCursor is a JSON API query parameter used with a cursor-based
// strategy
QueryParamPageCursor = "page[cursor]"

// KeySelfLink is the key within a top-level links object that denotes the link that
// generated the current response document.
KeySelfLink = "self"
)
6 changes: 6 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ type Post struct {
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
LatestComment *Comment `jsonapi:"relation,latest_comment"`

Links Links `jsonapi:"links,omitempty"`
}

type Comment struct {
ID int `jsonapi:"primary,comments"`
ClientID string `jsonapi:"client-id"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`

Links Links `jsonapi:"links,omitempty"`
}

type Book struct {
Expand All @@ -80,6 +84,8 @@ type Blog struct {
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`

Links Links `jsonapi:"links,omitempty"`
}

func (b *Blog) JSONAPILinks() *Links {
Expand Down
40 changes: 40 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,46 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)

}

} else if annotation == annotationLinks {
if data.Links == nil {
continue
}

links := make(Links, len(*data.Links))

for k, v := range *data.Links {
link := v // default case (including string urls)

// Unmarshal link objects to Link
if t, ok := v.(map[string]interface{}); ok {
unmarshaledHref := ""
href, ok := t["href"].(string)
if ok {
unmarshaledHref = href
}

unmarshaledMeta := make(Meta)
if meta, ok := t["meta"].(map[string]interface{}); ok {
for metaK, metaV := range meta {
unmarshaledMeta[metaK] = metaV
}
}

link = Link{
Href: unmarshaledHref,
Meta: unmarshaledMeta,
}
}

links[k] = link
}

if err != nil {
er = err
break
}

assign(fieldValue, reflect.ValueOf(links))
} else {
er = fmt.Errorf(unsupportedStructTagMsg, annotation)
}
Expand Down
78 changes: 78 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,35 @@ func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) {
}
}

func TestUnmarshalLinks(t *testing.T) {
model := new(Blog)

if err := UnmarshalPayload(samplePayload(), model); err != nil {
t.Fatal(err)
}

if model.Links == nil {
t.Fatalf("Expected Links field on model to be set")
}

if e, a := "http://somesite.com/blogs/1", model.Links[KeySelfLink]; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeySelfLink, e, a)
}

if e, a := "http://somesite.com/posts/1", model.Posts[0].Links[KeySelfLink]; e != a {
t.Fatalf("Was expecting posts.0.links.%s to have a value of %s, got %s", KeySelfLink, e, a)
}

expectedLinkObject := Link{Href: "http://somesite.com/posts/2", Meta: Meta{"foo": "bar"}}
if e, a := expectedLinkObject, model.CurrentPost.Links[KeySelfLink]; !reflect.DeepEqual(e, a) {
t.Fatalf("Was expecting posts.0.links.%s to have a value of %s, got %s", KeySelfLink, e, a)
}

if e, a := "http://somesite.com/comments/1", model.CurrentPost.Comments[0].Links[KeySelfLink]; e != a {
t.Fatalf("Was expecting posts.0.links.%s to have a value of %s, got %s", KeySelfLink, e, a)
}
}

func unmarshalSamplePayload() (*Blog, error) {
in := samplePayload()
out := new(Blog)
Expand Down Expand Up @@ -801,6 +830,32 @@ func TestUnmarshalManyPayload(t *testing.T) {
}
}

func TestOnePayload_withLinks(t *testing.T) {
rawJSON := []byte("{\"data\": { \"type\": \"posts\", \"id\": \"1\", \"attributes\": { \"body\": \"First\", \"title\": \"Post\" } }, \"links\": { \"self\": \"http://somesite.com/posts/1\" } }")

in := bytes.NewReader(rawJSON)

payload := new(OnePayload)
if err := json.NewDecoder(in).Decode(payload); err != nil {
t.Fatal(err)
}

if payload.Links == nil {
t.Fatal("Was expecting a non nil ptr Link field")
}

links := *payload.Links

self, ok := links[KeySelfLink]
if !ok {
t.Fatal("Was expecting a non nil 'self' link field")
}
if e, a := "http://somesite.com/posts/1", self; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeySelfLink, e, a)
}

}

func TestManyPayload_withLinks(t *testing.T) {
firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50"
prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0"
Expand Down Expand Up @@ -1016,6 +1071,9 @@ func samplePayload() io.Reader {
"body": "Bar",
},
ClientID: "1",
Links: &Links{
"self": "http://somesite.com/posts/1",
},
},
{
Type: "posts",
Expand All @@ -1024,6 +1082,9 @@ func samplePayload() io.Reader {
"body": "Y",
},
ClientID: "2",
Links: &Links{
"self": "http://somesite.com/posts/2",
},
},
},
},
Expand All @@ -1044,20 +1105,37 @@ func samplePayload() io.Reader {
"body": "Great post!",
},
ClientID: "4",
Links: &Links{
"self": "http://somesite.com/comments/1",
},
},
{
Type: "comments",
Attributes: map[string]interface{}{
"body": "Needs some work!",
},
ClientID: "5",
Links: &Links{
"self": "http://somesite.com/comments/2",
},
},
},
},
},
Links: &Links{
"self": &Link{
Href: "http://somesite.com/posts/2",
Meta: Meta{
"foo": "bar",
},
},
},
},
},
},
Links: &Links{
"self": "http://somesite.com/blogs/1",
},
},
}

Expand Down
4 changes: 3 additions & 1 deletion response.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,9 @@ func visitModelNode(model interface{}, included *map[string]*Node,
}
}
}

} else if annotation == annotationLinks {
// Nothing. Ignore this field, as Links fields are only for unmarshaling requests.
// The Linkable interface methods are used for marshaling data in a response.
} else {
er = ErrBadJSONAPIStructTag
break
Expand Down

0 comments on commit 1e50d74

Please sign in to comment.