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 5cd1535
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 9 deletions.
38 changes: 33 additions & 5 deletions jsonapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ 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"}}
commentEmbedded = CommentEmbedded{ID: "1", CommentFields: commentEmbeddedFields}

commentEmbeddedFieldsPointer = CommentFieldsPointer{Body: "A", Author: &Author{ID: "1"}}
commentEmbeddedPointer = CommentEmbeddedPointer{ID: "1", CommentFieldsPointer: commentEmbeddedFieldsPointer}

// articles
articleA = Article{ID: "1", Title: "A"}
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 Down Expand Up @@ -454,6 +460,28 @@ 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 CommentFieldsPointer 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 CommentEmbeddedPointer struct {
ID string `jsonapi:"primary,comments"`
CommentFieldsPointer
}

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: "CommentEmbeddedPointer",
given: commentEmbeddedBody,
do: func(body []byte) (any, error) {
var a CommentEmbeddedPointer
err := Unmarshal(body, &a)
return &a, err
},
expect: &commentEmbeddedPointer,
expectError: nil,
},
}

Expand Down

0 comments on commit 5cd1535

Please sign in to comment.