Skip to content

Commit d9bed28

Browse files
committed
Marshal polyrelation
polyrelation fields are marshaled as JSON to the first non-nil field within a choice type
1 parent b3c2954 commit d9bed28

File tree

2 files changed

+201
-3
lines changed

2 files changed

+201
-3
lines changed

response.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,31 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
193193
return json.NewEncoder(w).Encode(payload)
194194
}
195195

196+
func chooseFirstNonNilFieldValue(structValue reflect.Value) (reflect.Value, error) {
197+
for i := 0; i < structValue.NumField(); i++ {
198+
choiceFieldValue := structValue.Field(i)
199+
choiceTypeField := choiceFieldValue.Type()
200+
201+
// Must be a pointer
202+
if choiceTypeField.Kind() != reflect.Ptr {
203+
continue
204+
}
205+
206+
// Must not be nil
207+
if choiceFieldValue.IsNil() {
208+
continue
209+
}
210+
211+
subtype := choiceTypeField.Elem()
212+
_, err := jsonapiTypeOfModel(subtype)
213+
if err == nil {
214+
return choiceFieldValue, nil
215+
}
216+
}
217+
218+
return reflect.Value{}, errors.New("no non-nil choice field was found in the specified struct")
219+
}
220+
196221
func visitModelNode(model interface{}, included *map[string]*Node,
197222
sideload bool) (*Node, error) {
198223
node := new(Node)
@@ -207,13 +232,13 @@ func visitModelNode(model interface{}, included *map[string]*Node,
207232
modelType := value.Type().Elem()
208233

209234
for i := 0; i < modelValue.NumField(); i++ {
235+
fieldValue := modelValue.Field(i)
210236
structField := modelValue.Type().Field(i)
211237
tag := structField.Tag.Get(annotationJSONAPI)
212238
if tag == "" {
213239
continue
214240
}
215241

216-
fieldValue := modelValue.Field(i)
217242
fieldType := modelType.Field(i)
218243

219244
args := strings.Split(tag, annotationSeparator)
@@ -356,7 +381,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
356381
node.Attributes[args[1]] = fieldValue.Interface()
357382
}
358383
}
359-
} else if annotation == annotationRelation {
384+
} else if annotation == annotationRelation || annotation == annotationPolyRelation {
360385
var omitEmpty bool
361386

362387
//add support for 'omitempty' struct tag for marshaling as absent
@@ -371,6 +396,65 @@ func visitModelNode(model interface{}, included *map[string]*Node,
371396
continue
372397
}
373398

399+
if annotation == annotationPolyRelation {
400+
// for polyrelation, we'll snoop out the actual relation model
401+
// through the choice type value by choosing the first non-nil
402+
// field that has a jsonapi type annotation and overwriting
403+
// `fieldValue` so normal annotation-assisted marshaling
404+
// can continue
405+
if !isSlice {
406+
choiceValue := fieldValue
407+
408+
// must be a pointer type
409+
if choiceValue.Type().Kind() != reflect.Pointer {
410+
er = ErrUnexpectedType
411+
break
412+
}
413+
414+
if choiceValue.IsNil() {
415+
fieldValue = reflect.ValueOf(nil)
416+
}
417+
418+
structValue := choiceValue.Elem()
419+
if found, err := chooseFirstNonNilFieldValue(structValue); err == nil {
420+
fieldValue = found
421+
}
422+
} else {
423+
// A slice polyrelation field can be... polymorphic... meaning
424+
// that we might snoop different types within each slice element.
425+
// Each snooped value will added to this collection and then
426+
// the recursion will take care of the rest. The only special case
427+
// is nil. For that, we'll just choose the first
428+
collection := make([]interface{}, 0)
429+
430+
for i := 0; i < fieldValue.Len(); i++ {
431+
itemValue := fieldValue.Index(i)
432+
// Once again, must be a pointer type
433+
if itemValue.Type().Kind() != reflect.Pointer {
434+
er = ErrUnexpectedType
435+
break
436+
}
437+
438+
if itemValue.IsNil() {
439+
er = ErrUnexpectedNil
440+
break
441+
}
442+
443+
structValue := itemValue.Elem()
444+
445+
if found, err := chooseFirstNonNilFieldValue(structValue); err == nil {
446+
collection = append(collection, found.Interface())
447+
}
448+
}
449+
450+
if er != nil {
451+
break
452+
}
453+
454+
fieldValue = reflect.ValueOf(collection)
455+
}
456+
}
457+
374458
if node.Relationships == nil {
375459
node.Relationships = make(map[string]interface{})
376460
}

response_test.go

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,121 @@ func TestMarshalPayload(t *testing.T) {
3939
}
4040
}
4141

42-
func TestMarshalPayloadWithNulls(t *testing.T) {
42+
func TestMarshalPayloadWithOnePolyrelation(t *testing.T) {
43+
blog := &BlogPostWithPoly{
44+
ID: "1",
45+
Title: "Hello, World",
46+
Hero: &OneOfMedia{
47+
Image: &Image{
48+
ID: "2",
49+
},
50+
},
51+
}
52+
53+
out := bytes.NewBuffer(nil)
54+
if err := MarshalPayload(out, blog); err != nil {
55+
t.Fatal(err)
56+
}
57+
58+
var jsonData map[string]interface{}
59+
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{})
64+
if relationships == nil {
65+
t.Fatal("No relationships defined in unmarshaled JSON")
66+
}
67+
heroMedia := relationships["hero-media"].(map[string]interface{})["data"].(map[string]interface{})
68+
if heroMedia == nil {
69+
t.Fatal("No hero-media relationship defined in unmarshaled JSON")
70+
}
71+
72+
if heroMedia["id"] != "2" {
73+
t.Fatal("Expected ID \"2\" in unmarshaled JSON")
74+
}
75+
76+
if heroMedia["type"] != "images" {
77+
t.Fatal("Expected type \"images\" in unmarshaled JSON")
78+
}
79+
}
80+
81+
func TestMarshalPayloadWithManyPolyrelation(t *testing.T) {
82+
blog := &BlogPostWithPoly{
83+
ID: "1",
84+
Title: "Hello, World",
85+
Media: []*OneOfMedia{
86+
{
87+
Image: &Image{
88+
ID: "2",
89+
},
90+
},
91+
{
92+
Video: &Video{
93+
ID: "3",
94+
},
95+
},
96+
},
97+
}
98+
99+
out := bytes.NewBuffer(nil)
100+
if err := MarshalPayload(out, blog); err != nil {
101+
t.Fatal(err)
102+
}
103+
104+
var jsonData map[string]interface{}
105+
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
106+
t.Fatal(err)
107+
}
108+
109+
relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{})
110+
if relationships == nil {
111+
t.Fatal("No relationships defined in unmarshaled JSON")
112+
}
113+
114+
heroMedia := relationships["media"].(map[string]interface{})
115+
if heroMedia == nil {
116+
t.Fatal("No hero-media relationship defined in unmarshaled JSON")
117+
}
118+
119+
heroMediaData := heroMedia["data"].([]interface{})
120+
121+
if len(heroMediaData) != 2 {
122+
t.Fatal("Expected 2 items in unmarshaled JSON")
123+
}
124+
125+
imageData := heroMediaData[0].(map[string]interface{})
126+
videoData := heroMediaData[1].(map[string]interface{})
127+
128+
if imageData["id"] != "2" || imageData["type"] != "images" {
129+
t.Fatal("Expected images ID \"2\" in unmarshaled JSON")
130+
}
131+
132+
if videoData["id"] != "3" || videoData["type"] != "videos" {
133+
t.Fatal("Expected videos ID \"3\" in unmarshaled JSON")
134+
}
135+
}
136+
137+
func TestMarshalPayloadWithManyPolyrelationWithNils(t *testing.T) {
138+
blog := &BlogPostWithPoly{
139+
ID: "1",
140+
Title: "Hello, World",
141+
Media: []*OneOfMedia{
142+
nil,
143+
{
144+
Image: &Image{
145+
ID: "2",
146+
},
147+
},
148+
nil,
149+
},
150+
}
151+
152+
out := bytes.NewBuffer(nil)
153+
if err := MarshalPayload(out, blog); !errors.Is(err, ErrUnexpectedNil) {
154+
t.Fatal("expected error but got none")
155+
}
156+
}
43157

44158
func TestMarshalPayloadWithManyRelationWithNils(t *testing.T) {
45159
blog := &Blog{

0 commit comments

Comments
 (0)