From 5cd1535a8a4585f689d8d0721899cc469ab18f5f Mon Sep 17 00:00:00 2001 From: Kevin Conaway Date: Wed, 13 Mar 2024 15:03:18 -0400 Subject: [PATCH 1/2] Add support for unmarshalling embedded structs with relationships This commit adds better support for unmarshaling embedded structs that contain relationship fields --- jsonapi_test.go | 38 +++++++++++++++++++++++++++++++++----- unmarshal.go | 17 +++++++++++++---- unmarshal_test.go | 21 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/jsonapi_test.go b/jsonapi_test.go index 256d3e5..034d361 100644 --- a/jsonapi_test.go +++ b/jsonapi_test.go @@ -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"} @@ -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":{}}}}` @@ -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 diff --git a/unmarshal.go b/unmarshal.go index a4d0e54..0348174 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -170,7 +170,9 @@ 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 } @@ -178,10 +180,8 @@ func (ro *resourceObject) unmarshal(v any, m *Unmarshaler) error { } // 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) @@ -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 } diff --git a/unmarshal_test.go b/unmarshal_test.go index 4662af6..be2fb3b 100644 --- a/unmarshal_test.go +++ b/unmarshal_test.go @@ -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, }, } From da2f4289f2bb587b1ff6d3c269bb0caa66d0d413 Mon Sep 17 00:00:00 2001 From: Kevin Conaway Date: Thu, 14 Mar 2024 15:12:55 -0400 Subject: [PATCH 2/2] Ensure that the nested struct we look at is an anonymous one --- unmarshal.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/unmarshal.go b/unmarshal.go index 0348174..eab79fa 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -191,16 +191,12 @@ func (ro *resourceObject) unmarshalFields(v any, rv reflect.Value, rt reflect.Ty 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 { + if ft.Anonymous && fv.Kind() == reflect.Struct { + if err := ro.unmarshalFields(v, fv, reflect.TypeOf(fv.Interface()), m); err != nil { + return err + } + } continue }