Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unmarshal Links type to 'links' annotated struct fields #10

Merged
merged 2 commits into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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