Skip to content

Commit

Permalink
Add unmarshalling of Links (#4)
Browse files Browse the repository at this point in the history
* Add links support
* Handle single relationship only link
  • Loading branch information
omarismail authored Apr 1, 2021
1 parent 166ae20 commit 95d5725
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 6 deletions.
1 change: 1 addition & 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
14 changes: 10 additions & 4 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,16 @@ type Post struct {
}

type Comment struct {
ID int `jsonapi:"primary,comments"`
ClientID string `jsonapi:"client-id"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
ID int `jsonapi:"primary,comments"`
ClientID string `jsonapi:"client-id"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
Impressions *Impressions `jsonapi:"relation,impressions"`
URL *Links `jsonapi:"links,omitempty"`
}

type Impressions struct {
URL *Links `jsonapi:"links,omitempty"`
}

type Book struct {
Expand Down
84 changes: 82 additions & 2 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
json.NewDecoder(buf).Decode(relationship)

data := relationship.Data
links := relationship.Links
models := reflect.New(fieldValue.Type()).Elem()

for _, n := range data {
Expand All @@ -291,6 +292,37 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
models = reflect.Append(models, m)
}

if links != nil {
var assignedValue bool
linkModel := reflect.New(fieldValue.Type().Elem().Elem())
linkModelValue := linkModel.Elem()
linkModelType := linkModelValue.Type()

for i := 0; i < linkModelValue.NumField(); i++ {
fieldType := linkModelType.Field(i)
tag := fieldType.Tag.Get("jsonapi")
if tag == "" {
continue
}
fieldValue := linkModelValue.Field(i)
args := strings.Split(tag, ",")
annotation := args[0]
if annotation == annotationLinks {
value, err := unmarshalAttribute(*links, args, fieldType, fieldValue)
if err != nil {
er = err
break
}

assign(fieldValue, value)
assignedValue = true
}
}
if assignedValue {
models = reflect.Append(models, linkModel)
}
}

fieldValue.Set(models)
} else {
// to-one relationships
Expand All @@ -309,13 +341,23 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
relationship can have a data node set to null (e.g. to disassociate the relationship)
so unmarshal and set fieldValue only if data obj is not null
*/
if relationship.Data == nil {

if relationship.Data == nil && relationship.Links == nil {
continue
}

node := &Node{}
if relationship.Data != nil {
node = relationship.Data
}

if relationship.Links != nil {
node.Links = relationship.Links
}

m := reflect.New(fieldValue.Type().Elem())
if err := unmarshalNode(
fullNode(relationship.Data, included),
fullNode(node, included),
m,
included,
); err != nil {
Expand All @@ -324,9 +366,23 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}

fieldValue.Set(m)
}

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

structField := fieldType
value, err := unmarshalAttribute(*links, args, structField, fieldValue)
if err != nil {
er = err
break
}

assign(fieldValue, value)

} else {
er = fmt.Errorf(unsupportedStructTagMsg, annotation)
}
Expand Down Expand Up @@ -409,6 +465,11 @@ func unmarshalAttribute(
return
}

if fieldValue.Type() == reflect.TypeOf(&Links{}) {
value, err = handleLinks(attribute, args, fieldValue)
return
}

// Handle field of type struct
if fieldValue.Type().Kind() == reflect.Struct {
value, err = handleStruct(attribute, fieldValue)
Expand Down Expand Up @@ -466,6 +527,25 @@ func handleMapStringSlice(attribute interface{}, fieldValue reflect.Value) (refl
return reflect.ValueOf(values), nil
}

func handleLinks(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
b, err := json.Marshal(attribute)
if err != nil {
return reflect.Value{}, err
}
var v interface{}
err = json.Unmarshal(b, &v)
if err != nil {
return reflect.Value{}, err
}

var values = map[string]interface{}{}
for k, v := range v.(map[string]interface{}) {
values[k] = v.(interface{})
}

return reflect.ValueOf(values), nil
}

func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
var isIso8601 bool
var isRFC3339 bool
Expand Down
144 changes: 144 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,150 @@ func TestUnmarshalManyPayload(t *testing.T) {
}
}

func TestOnePayload_WithRelationLinks(t *testing.T) {
selfLink := "http://example.com/articles/1/relationships/author"
relatedLink := "http://example.com/articles/1/author"
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "posts",
"id": "1",
"attributes": map[string]interface{}{
"body": "First",
"title": "Post",
},
"relationships": map[string]interface{}{
"comments": map[string]interface{}{
"links": map[string]string{
"self": selfLink,
"related": relatedLink,
},
},
},
},
}

data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)

out := new(Post)

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

if len(out.Comments) == 0 {
t.Fatal("Was expecting non-empty Comments")
}

if out.Comments[0].URL == nil {
t.Fatal("Was expecting a non nil ptr Link field")
}

links := *out.Comments[0].URL
if links["self"] != selfLink {
t.Fatalf("Was expecting self Link to equal %s, but got %s", selfLink, links["self"])
}

if links["related"] != relatedLink {
t.Fatalf("Was expecting related Link to equal %s, but got %s", relatedLink, links["related"])
}
}

func TestOnePayload_WithLinks(t *testing.T) {
selfLink := "http://example.com/articles/1/relationships/author"
relatedLink := "http://example.com/articles/1/author"
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "comments",
"id": "1",
"attributes": map[string]interface{}{
"body": "First",
"title": "Post",
},
"links": map[string]string{
"self": selfLink,
"related": relatedLink,
},
},
}

data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)

out := new(Comment)

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

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

links := *out.URL
if links["self"] != selfLink {
t.Fatalf("Was expecting self Link to equal %s, but got %s", selfLink, links["self"])
}

if links["related"] != relatedLink {
t.Fatalf("Was expecting related Link to equal %s, but got %s", relatedLink, links["related"])
}
}

func TestOnePayload_RelationshipLinks(t *testing.T) {
selfLink := "http://example.com/articles/1/relationships/author"
relatedLink := "http://example.com/articles/1/author"
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "comments",
"id": "1",
"attributes": map[string]interface{}{
"body": "First",
"title": "Post",
},
"relationships": map[string]interface{}{
"impressions": map[string]interface{}{
"links": map[string]string{
"self": selfLink,
"related": relatedLink,
},
},
},
},
}

data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)

out := new(Comment)

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

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

links := *out.Impressions.URL
if links["self"] != selfLink {
t.Fatalf("Was expecting self Link to equal %s, but got %s", selfLink, links["self"])
}

if links["related"] != relatedLink {
t.Fatalf("Was expecting related Link to equal %s, but got %s", relatedLink, links["related"])
}
}

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
7 changes: 7 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,13 @@ func visitModelNode(model interface{}, included *map[string]*Node,
}
}
}
} else if annotation == annotationLinks {
// This is for marshalling.
// This is left blank intentionally as the handling
// of `jsonapi:"links"` is done below via the
// Linkable interface.
// And the logic for Marshalling links should be done
// via that interface.

} else {
er = ErrBadJSONAPIStructTag
Expand Down

0 comments on commit 95d5725

Please sign in to comment.