Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ You can implement the following on the field types themselves if they are not al
3. Use the value directly if it is a string
4. Fail

## Resource Identifier Metadata

JSON:API permits a resource identifier object in relationship data to contain a `meta` object. Implement `jsonapi.MarshalResourceIdentifierMeta` on the related resource type to add this object.

```go
type Comment struct {
ID string `jsonapi:"primary,comments"`
Index int
}

func (c Comment) MarshalResourceIdentifierMeta() any {
return map[string]any{"index": c.Index}
}
```

The marshaler uses this interface only for relationship data. Return `nil` to omit the `meta` member. The `jsonapi:"meta"` directive continues to control metadata on the relationship object.

## Links

[Links](https://jsonapi.org/format/1.0/#document-links) are supported via two interfaces and the [Link](https://pkg.go.dev/github.com/DataDog/jsonapi#Link) type. To include links you must implement one or both of the following interfaces.
Expand Down
8 changes: 8 additions & 0 deletions jsonapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,14 @@ type LinkableRelation interface {
LinkRelation(relation string) *Link
}

// MarshalResourceIdentifierMeta can be implemented to add meta to a resource identifier object.
// The marshaler uses this interface only when it marshals a resource as relationship data.
// The method must return a map or struct.
// Return nil to omit the meta member.
Comment thread
ryanscottaudio marked this conversation as resolved.
type MarshalResourceIdentifierMeta interface {
MarshalResourceIdentifierMeta() any
}

// MarshalIdentifier can be optionally implemented to control marshaling of the primary field to a string.
//
// The order of operations for marshaling the primary field is:
Expand Down
32 changes: 32 additions & 0 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,16 @@ func (d *document) makeResourceObject(v any, vt reflect.Type, m *Marshaler) (*re
return nil, ErrEmptyPrimaryField
}

// Resource identifier objects can have their own meta. Keep this separate from
// the relationship document meta that the jsonapi:"meta" directive controls.
if d.isRelationship {
metaObject, err := marshalResourceIdentifierMeta(v)
if err != nil {
return nil, err
}
ro.Meta = metaObject
}

// if Linkable is implemented include ResourceObject.Links
if lv, ok := v.(Linkable); ok {
link := lv.Link()
Expand All @@ -485,6 +495,28 @@ func (d *document) makeResourceObject(v any, vt reflect.Type, m *Marshaler) (*re
return ro, nil
}

func marshalResourceIdentifierMeta(v any) (any, error) {
vm, ok := v.(MarshalResourceIdentifierMeta)
if !ok {
return nil, nil
}

metaObject := vm.MarshalResourceIdentifierMeta()
if err := checkMeta(metaObject); err != nil {
return nil, err
}
if metaObject == nil {
return nil, nil
}

metaValue := reflect.ValueOf(metaObject)
if canBeNil(metaValue) && metaValue.IsNil() {
return nil, nil
}

return metaObject, nil
}

func getFlattenedFields(iface interface{}) []struct {
v reflect.Value
f reflect.StructField
Expand Down
137 changes: 137 additions & 0 deletions marshal_resource_identifier_meta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package jsonapi

import (
"testing"

"github.com/DataDog/jsonapi/internal/is"
)

type resourceIdentifierMetaTestNode struct {
ID string `jsonapi:"primary,nodes"`
IdentifierMeta any
RelationshipMeta any `jsonapi:"meta"`
}

func (n resourceIdentifierMetaTestNode) MarshalResourceIdentifierMeta() any {
return n.IdentifierMeta
}

type resourceIdentifierMetaTestLayer struct {
ID string `jsonapi:"primary,layers"`
Node *resourceIdentifierMetaTestNode `jsonapi:"relationship" json:"node,omitempty"`
Nodes []resourceIdentifierMetaTestNode `jsonapi:"relationship" json:"nodes,omitempty"`
}

func TestMarshalResourceIdentifierMeta(t *testing.T) {
t.Parallel()

tests := []struct {
description string
given any
expect string
expectError error
}{
{
description: "to-one relationship",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: map[string]any{"index": 0},
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"node":{"data":{"id":"node-a","type":"nodes","meta":{"index":0}}}}}}`,
},
{
description: "to-many relationship with distinct metadata",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Nodes: []resourceIdentifierMetaTestNode{
{ID: "node-a", IdentifierMeta: map[string]any{"index": 0}},
{ID: "node-b", IdentifierMeta: map[string]any{"index": 1}},
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"nodes":{"data":[{"id":"node-a","type":"nodes","meta":{"index":0}},{"id":"node-b","type":"nodes","meta":{"index":1}}]}}}}`,
},
{
description: "nil metadata",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"node":{"data":{"id":"node-a","type":"nodes"}}}}}`,
},
{
description: "typed nil metadata",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: (*struct{})(nil),
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"node":{"data":{"id":"node-a","type":"nodes"}}}}}`,
},
{
description: "empty metadata object",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: map[string]any{},
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"node":{"data":{"id":"node-a","type":"nodes","meta":{}}}}}}`,
},
{
description: "invalid metadata",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: "invalid",
},
},
expectError: &TypeError{Actual: "string", Expected: []string{"struct", "map"}},
},
{
description: "resource identifier and relationship metadata",
given: &resourceIdentifierMetaTestLayer{
ID: "layer-a",
Node: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: map[string]any{"index": 0},
RelationshipMeta: map[string]any{"total": 2},
},
},
expect: `{"data":{"id":"layer-a","type":"layers","relationships":{"node":{"data":{"id":"node-a","type":"nodes","meta":{"index":0}},"meta":{"total":2}}}}}`,
},
{
description: "top-level resource ignores resource identifier metadata",
given: &resourceIdentifierMetaTestNode{
ID: "node-a",
IdentifierMeta: map[string]any{"index": 0},
},
expect: `{"data":{"id":"node-a","type":"nodes"}}`,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.description, func(t *testing.T) {
t.Parallel()

actual, err := Marshal(tc.given)
if tc.expectError != nil {
is.EqualError(t, tc.expectError, err)
is.Nil(t, actual)
return
}

is.MustNoError(t, err)
is.EqualJSON(t, tc.expect, string(actual))
})
}
}
Loading