Skip to content

Commit

Permalink
Add support for unmarshalling embedded structs with relationships
Browse files Browse the repository at this point in the history
This commit adds better support for unmarshaling embedded structs that contain relationship fields
  • Loading branch information
kevinconaway committed Mar 13, 2024
1 parent 3a88ca9 commit 49bd16e
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 9 deletions.
34 changes: 29 additions & 5 deletions jsonapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ var (
authorBWithMeta = Author{ID: "2", Name: "B", Meta: map[string]any{"count": 10.0}}

// comments
commentA = Comment{ID: "1", Body: "A"}
commentB = Comment{ID: "2", Body: "B"}
commentAWithAuthor = Comment{ID: "1", Body: "A", Author: &authorA}
commentArchived = Comment{ID: "1", Body: "A", Archived: true}
commentsAB = []*Comment{&commentA, &commentB}
commentA = Comment{ID: "1", Body: "A"}
commentB = Comment{ID: "2", Body: "B"}
commentAWithAuthor = Comment{ID: "1", Body: "A", Author: &authorA}
commentArchived = Comment{ID: "1", Body: "A", Archived: true}
commentsAB = []*Comment{&commentA, &commentB}
commentEmbeddedFields = CommentFields{Body: "A", Author: Author{ID: "1"}}
commentEmbeddedFieldsB = CommentFields{Body: "B", Author: Author{ID: "1"}}
commentEmbedded = CommentEmbedded{ID: "1", CommentFields: commentEmbeddedFields}
commentEmbeddedB = CommentEmbedded{ID: "2", CommentFields: commentEmbeddedFieldsB}

// articles
articleA = Article{ID: "1", Title: "A"}
Expand Down Expand Up @@ -80,6 +84,7 @@ var (
articleRelatedCommentsArchived = ArticleRelated{ID: "1", Title: "A", Comments: []*Comment{&commentArchived}}
articleRelatedCommentsNested = ArticleRelated{ID: "1", Title: "A", Comments: []*Comment{&commentAWithAuthor}}
articleRelatedComplete = ArticleRelated{ID: "1", Title: "A", Author: &authorAWithMeta, Comments: commentsAB}
articleRelatedEmbedded = ArticleRelatedEmbedded{ID: "1", Title: "A", Comments: []CommentEmbedded{commentEmbedded, commentEmbeddedB}}
articlesRelatedComplex = []*ArticleRelated{
{
ID: "1",
Expand Down Expand Up @@ -157,6 +162,7 @@ var (
articleNullWithToplevelMetaBody = `{"data":null,"meta":{"foo":"bar"}}`
articleEmptyArrayWithToplevelMetaBody = `{"data":[],"meta":{"foo":"bar"}}`
articleEmbeddedBody = `{"data":{"type":"articles","id":"1","attributes":{"title":"A","lastModified":"1989-06-15T00:00:00Z"}}}`
commentEmbeddedBody = `{"data":{"id":"1","type":"comments","attributes":{"body":"A"},"relationships":{"author":{"data":{"id":"1","type":"author"}}}}}`

// articles with relationships bodies
articleRelatedInvalidEmptyRelationshipBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"relationships":{"author":{}}}}`
Expand All @@ -175,6 +181,7 @@ var (
articleRelatedAuthorLinksOnlyBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"relationships":{"author":{"links":{"self":"http://example.com/articles/1/relationships/author","related":"http://example.com/articles/1/author"}}}}}`
articleRelatedAuthorMetaOnlyBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"relationships":{"author":{"meta":{"foo":"bar"}}}}}`
articlesRelatedComplexBody = `{"data":[{"id":"1","type":"articles","attributes":{"title":"Bazel 101"},"relationships":{"author":{"data":{"id":"1","type":"author"},"links":{"self":"http://example.com/articles/1/relationships/author","related":"http://example.com/articles/1/author"}},"comments":{"data":[{"id":"11","type":"comments"},{"id":"12","type":"comments"},{"id":"13","type":"comments"}],"links":{"self":"http://example.com/articles/1/relationships/comments","related":"http://example.com/articles/1/comments"}}}},{"id":"2","type":"articles","attributes":{"title":"Why Should I Use JSON:API?"},"relationships":{"author":{"data":{"id":"2","type":"author"},"meta":{"count":10},"links":{"self":"http://example.com/articles/2/relationships/author","related":"http://example.com/articles/2/author"}},"comments":{"data":[{"id":"21","type":"comments"}],"links":{"self":"http://example.com/articles/2/relationships/comments","related":"http://example.com/articles/2/comments"}}}},{"id":"3","type":"articles","attributes":{"title":"Internal Test Article Created In Production For Some Reason"},"relationships":{"comments":{"data":[{"id":"31","type":"comments"},{"id":"32","type":"comments"}],"links":{"self":"http://example.com/articles/3/relationships/comments","related":"http://example.com/articles/3/comments"}}}},{"id":"4","type":"articles","attributes":{"title":"How to Rewrite Everything in Rust"},"relationships":{"author":{"data":{"id":"1","type":"author"},"links":{"self":"http://example.com/articles/4/relationships/author","related":"http://example.com/articles/4/author"}}}}],"meta":{"meta_kind":"document-level meta"},"jsonapi":{"version":"1.0","meta":{"meta_kind":"jsonapi meta"}},"included":[{"id":"1","type":"author","attributes":{"name":"A"}},{"id":"2","type":"author","attributes":{"name":"B"},"meta":{"count":10}},{"id":"11","type":"comments","attributes":{"archived":true,"body":"Why is Bazel so slow on my computerr?"},"relationships":{"author":{"data":{"id":"2","type":"author"},"meta":{"count":10},"links":{"self":"http://example.com/comments/11/relationships/author","related":"http://example.com/comments/11/author"}}}},{"id":"12","type":"comments","attributes":{"body":"Why is Bazel so slow on my computer?"},"relationships":{"author":{"data":{"id":"2","type":"author"},"meta":{"count":10},"links":{"self":"http://example.com/comments/12/relationships/author","related":"http://example.com/comments/12/author"}}}},{"id":"13","type":"comments","attributes":{"body":"Just use an Apple M1"},"relationships":{"author":{"data":{"id":"1","type":"author"},"links":{"self":"http://example.com/comments/13/relationships/author","related":"http://example.com/comments/13/author"}}}},{"id":"21","type":"comments","attributes":{"body":"I wish they changed the name..."},"relationships":{"author":{"data":{"id":"2","type":"author"},"meta":{"count":10},"links":{"self":"http://example.com/comments/21/relationships/author","related":"http://example.com/comments/21/author"}}}},{"id":"31","type":"comments","attributes":{"body":"test1"}},{"id":"32","type":"comments","attributes":{"body":"test2"}}]}`
articleRelatedEmbeddedBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"relationships":{"comments":{"data":[{"id":"1","type":"comments"},{"id":"2","type":"comments"}]}}},"included":[{"id":"1","type":"comments","attributes":{"body":"A"},"relationships":{"author":{"data":{"id":"1","type":"author"}}}},{"id":"2","type":"comments","attributes":{"body":"B"},"relationships":{"author":{"data":{"id":"1","type":"author"}}}},{"id":"1","type":"author","attributes":{"name":""}},{"id":"1","type":"author","attributes":{"name":""}}]}`

// articles with non-conforming member name bodies
authorWithInvalidTypeNameBody = `{"data":{"id":"1","type":"aut%hor"}}`
Expand Down Expand Up @@ -437,6 +444,12 @@ func (a *ArticleRelated) LinkRelation(relation string) *Link {
}
}

type ArticleRelatedEmbedded struct {
ID string `jsonapi:"primary,articles"`
Title string `jsonapi:"attribute" json:"title"`
Comments []CommentEmbedded `jsonapi:"relationship" json:"comments,omitempty"`
}

type ArticleRelatedNoOmitEmpty struct {
ID string `jsonapi:"primary,articles"`
Title string `jsonapi:"attribute" json:"title"`
Expand All @@ -454,6 +467,17 @@ type Metadata struct {
LastModified time.Time `jsonapi:"attribute" json:"lastModified"`
}

type CommentFields struct {
Body string `jsonapi:"attribute" json:"body"`
Archived bool `jsonapi:"attribute" json:"archived,omitempty"`
Author Author `jsonapi:"relationship" json:"author,omitempty"`
}

type CommentEmbedded struct {
ID string `jsonapi:"primary,comments"`
CommentFields
}

type ArticleEmbedded struct {
Metadata

Expand Down
17 changes: 13 additions & 4 deletions unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,18 @@ func (ro *resourceObject) unmarshal(v any, m *Unmarshaler) error {
return &TypeError{Actual: vt.String(), Expected: []string{"struct"}}
}

if err := ro.unmarshalFields(v, m); err != nil {
rv := derefValue(reflect.ValueOf(v))
rt := reflect.TypeOf(rv.Interface())
if err := ro.unmarshalFields(v, rv, rt, m); err != nil {
return err
}

return ro.unmarshalAttributes(v)
}

// unmarshalFields unmarshals a resource object into all non-attribute struct fields
func (ro *resourceObject) unmarshalFields(v any, m *Unmarshaler) error {
func (ro *resourceObject) unmarshalFields(v any, rv reflect.Value, rt reflect.Type, m *Unmarshaler) error {
setPrimary := false
rv := derefValue(reflect.ValueOf(v))
rt := reflect.TypeOf(rv.Interface())

for i := 0; i < rv.NumField(); i++ {
fv := rv.Field(i)
Expand All @@ -191,6 +191,15 @@ func (ro *resourceObject) unmarshalFields(v any, m *Unmarshaler) error {
if err != nil {
return err
}
// If there is an embedded struct we want to process the fields in that struct recursively
// only if there isn't a relationship defined on the field.
if fv.Kind() == reflect.Struct && jsonapiTag == nil {
if err := ro.unmarshalFields(v, fv, reflect.TypeOf(fv.Interface()), m); err != nil {
return err
}
continue
}

if jsonapiTag == nil {
continue
}
Expand Down
21 changes: 21 additions & 0 deletions unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,27 @@ func TestUnmarshal(t *testing.T) {
},
expect: &Article{},
expectError: ErrErrorUnmarshalingNotImplemented,
}, {
description: "CommentEmbedded",
given: commentEmbeddedBody,
do: func(body []byte) (any, error) {
var a CommentEmbedded
err := Unmarshal(body, &a)
return &a, err
},
expect: &commentEmbedded,
expectError: nil,
},
{
description: "ArticleRelatedEmbedded",
given: articleRelatedEmbeddedBody,
do: func(body []byte) (any, error) {
var a ArticleRelatedEmbedded
err := Unmarshal(body, &a)
return &a, err
},
expect: &articleRelatedEmbedded,
expectError: nil,
},
}

Expand Down

0 comments on commit 49bd16e

Please sign in to comment.