diff --git a/jsonapi_test.go b/jsonapi_test.go index 016ba3e..8887f65 100644 --- a/jsonapi_test.go +++ b/jsonapi_test.go @@ -53,6 +53,8 @@ var ( articleAIntIDID = ArticleIntIDID{ID: IntID(1), Title: "A"} articleBIntIDID = ArticleIntIDID{ID: IntID(2), Title: "B"} articlesIntIDIDABPtr = []*ArticleIntIDID{&articleAIntIDID, &articleBIntIDID} + articleEmbedded = ArticleEmbedded{ID: "1", Title: "A", Metadata: Metadata{LastModified: time.Date(1989, 06, 15, 0, 0, 0, 0, time.UTC)}} + articleEmbeddedPointer = ArticleEmbeddedPointer{ID: "1", Title: "A", Metadata: &Metadata{LastModified: time.Date(1989, 06, 15, 0, 0, 0, 0, time.UTC)}} // articles with optional meta articleAWithMeta = ArticleWithMeta{ID: "1", Title: "A", Meta: &ArticleMetrics{Views: 10, Reads: 4}} @@ -86,6 +88,7 @@ var ( articleLinkedOnlySelfBody = `{"data":{"id":"1","type":"articles","links":{"self":"https://example.com/articles/1"}}}` articleWithResourceObjectMetaBody = `{"data":{"type":"articles","id":"1","attributes":{"title":"A"},"meta":{"count":10}}}` articleAWithMetaBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"meta":{"views":10,"reads":4}}}` + articleEmbeddedBody = `{"data":{"type":"articles","id":"1","attributes":{"title":"A","lastModified":"1989-06-15T00:00:00Z"}}}` // articles with relationships bodies articleRelatedNoOmitEmptyBody = `{"data":{"id":"1","type":"articles","attributes":{"title":"A"},"relationships":{"author":{"data":null},"comments":{"data":[]}}}}` @@ -327,3 +330,21 @@ type ArticleDoubleID struct { Title string `jsonapi:"attribute" json:"title"` OtherID string `jsonapi:"primary,article"` } + +type Metadata struct { + LastModified time.Time `jsonapi:"attribute" json:"lastModified"` +} + +type ArticleEmbedded struct { + Metadata + + ID string `jsonapi:"primary,articles"` + Title string `jsonapi:"attribute" json:"title"` +} + +type ArticleEmbeddedPointer struct { + *Metadata + + ID string `jsonapi:"primary,articles"` + Title string `jsonapi:"attribute" json:"title"` +} diff --git a/marshal.go b/marshal.go index 3e07559..912cd1c 100644 --- a/marshal.go +++ b/marshal.go @@ -298,16 +298,16 @@ func makeResourceObject(v any, vt reflect.Type, m *Marshaler, isRelationship boo Relationships: make(map[string]*document, 0), } - rv := derefValue(reflect.ValueOf(v)) - rt := reflect.TypeOf(rv.Interface()) + // get fields from embedded structs + fields := getFlattenedFields(v) var foundPrimary bool - for i := 0; i < rv.NumField(); i++ { + for _, field := range fields { // for each field in the struct we'll parse the jsonapi struct tag // this will determine where it goes in the resource object (e.g. id,type,attributes,...) - f := rv.Field(i) - ft := rt.Field(i) + f := field.v + ft := field.f tag, err := parseJSONAPITag(ft) if err != nil { @@ -434,6 +434,35 @@ func makeResourceObject(v any, vt reflect.Type, m *Marshaler, isRelationship boo return ro, nil } +func getFlattenedFields(iface interface{}) []struct { + v reflect.Value + f reflect.StructField +} { + rv := derefValue(reflect.ValueOf(iface)) + rt := reflect.TypeOf(rv.Interface()) + + fields := make([]struct { + v reflect.Value + f reflect.StructField + }, 0) + + for i := 0; i < rv.NumField(); i++ { + v := rv.Field(i) + f := rt.Field(i) + + if f.Anonymous && (v.Kind() == reflect.Struct || v.Kind() == reflect.Pointer) { + fields = append(fields, getFlattenedFields(v.Interface())...) + } else { + fields = append(fields, struct { + v reflect.Value + f reflect.StructField + }{v, f}) + } + } + + return fields +} + func addOptionalDocumentFields(d *document, m *Marshaler) error { // optionally include Document.meta (may be nil, which will be omitted) if err := checkMeta(m.meta); err != nil { diff --git a/marshal_test.go b/marshal_test.go index c77f00c..5f836a0 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -185,6 +185,16 @@ func TestMarshal(t *testing.T) { given: &articleWithoutResourceObjectMeta, expect: articleABody, expectError: nil, + }, { + description: "ArticleEmbedded", + given: &articleEmbedded, + expect: articleEmbeddedBody, + expectError: nil, + }, { + description: "ArticleEmbeddedPointer", + given: &articleEmbeddedPointer, + expect: articleEmbeddedBody, + expectError: nil, }, { description: "Error simple", given: errorsSimpleStruct, diff --git a/unmarshal_test.go b/unmarshal_test.go index cd6c06c..1f7ce13 100644 --- a/unmarshal_test.go +++ b/unmarshal_test.go @@ -129,6 +129,26 @@ func TestUnmarshal(t *testing.T) { }, expect: &articleAWithMeta, expectError: nil, + }, { + description: "ArticleEmbedded", + given: articleEmbeddedBody, + do: func(body []byte) (any, error) { + var a ArticleEmbedded + err := Unmarshal(body, &a) + return &a, err + }, + expect: &articleEmbedded, + expectError: nil, + }, { + description: "ArticleEmbeddedPointer", + given: articleEmbeddedBody, + do: func(body []byte) (any, error) { + var a ArticleEmbeddedPointer + err := Unmarshal(body, &a) + return &a, err + }, + expect: &articleEmbeddedPointer, + expectError: nil, }, { description: "nil", given: "",