Skip to content

Commit 473eb21

Browse files
committed
Decode polymorphic relationships
Creates a new annotation that invokes special handling for the associated field, assigning exactly one of a selection of models in an intermediate struct. (See README)
1 parent b6a3d21 commit 473eb21

File tree

4 files changed

+338
-42
lines changed

4 files changed

+338
-42
lines changed

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,56 @@ used as the key in the `relationships` hash for the record. The optional
179179
third argument is `omitempty` - if present will prevent non existent to-one and
180180
to-many from being serialized.
181181

182+
183+
#### `polyrelation`
184+
185+
```
186+
`jsonapi:"polyrelation,<key name in relationships hash>,<optional: omitempty>"`
187+
```
188+
189+
Polymorphic relations can be represented exactly as relations, except that
190+
an intermediate type is needed within your model struct that will be populated
191+
with exactly one value among all the fields in that struct.
192+
193+
Example:
194+
195+
```go
196+
type Video struct {
197+
ID int `jsonapi:"primary,videos"`
198+
SourceURL string `jsonapi:"attr,source-url"`
199+
CaptionsURL string `jsonapi:"attr,captions-url"`
200+
}
201+
202+
type Image struct {
203+
ID int `jsonapi:"primary,images"`
204+
SourceURL string `jsonapi:"attr,src"`
205+
AltText string `jsonapi:"attr,alt"`
206+
}
207+
208+
type OneOfMedia struct {
209+
Video *Video
210+
Image *Image
211+
}
212+
213+
type Post struct {
214+
ID int `jsonapi:"primary,posts"`
215+
Title string `jsonapi:"attr,title"`
216+
Body string `jsonapi:"attr,body"`
217+
Gallery []*OneOfMedia `jsonapi:"polyrelation,gallery"`
218+
Hero *OneOfMedia `jsonapi:"polyrelation,hero"`
219+
}
220+
```
221+
222+
During decoding, the `polyrelation` annotation instructs jsonapi to assign each relationship
223+
to either `Video` or `Image` within the value of the associated field. This value must be
224+
a pointer to a struct containing other pointer fields to jsonapi models. The actual field
225+
assignment depends on that type having a jsonapi "primary" annotation with a type matching
226+
the relationship type found in the response. All other fields will be remain nil.
227+
228+
During encoding, the very first non-nil field will be used to populate the payload. Others
229+
will be ignored. Therefore, it's critical to set the value of only one field within the join
230+
struct.
231+
182232
#### `links`
183233

184234
*Note: This annotation is an added feature independent of the canonical google/jsonapi package*

constants.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ package jsonapi
22

33
const (
44
// StructTag annotation strings
5-
annotationJSONAPI = "jsonapi"
6-
annotationPrimary = "primary"
7-
annotationClientID = "client-id"
8-
annotationAttribute = "attr"
9-
annotationRelation = "relation"
10-
annotationLinks = "links"
11-
annotationOmitEmpty = "omitempty"
12-
annotationISO8601 = "iso8601"
13-
annotationRFC3339 = "rfc3339"
14-
annotationSeperator = ","
5+
annotationJSONAPI = "jsonapi"
6+
annotationPrimary = "primary"
7+
annotationClientID = "client-id"
8+
annotationAttribute = "attr"
9+
annotationRelation = "relation"
10+
annotationPolyRelation = "polyrelation"
11+
annotationLinks = "links"
12+
annotationOmitEmpty = "omitempty"
13+
annotationISO8601 = "iso8601"
14+
annotationRFC3339 = "rfc3339"
15+
annotationSeperator = ","
1516

1617
iso8601TimeFormat = "2006-01-02T15:04:05Z"
1718

request.go

Lines changed: 163 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ var (
3232
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
3333
// ErrInvalidType is returned when the given type is incompatible with the expected type.
3434
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
35-
35+
// ErrBadJSONAPIJoinStruct is returned when the polyrelation type did not contain
36+
// an appropriate join type to contain the required jsonapi node.
37+
ErrBadJSONAPIJoinStruct = errors.New("Invalid join struct for polymorphic relation field")
3638
)
3739

3840
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
@@ -142,6 +144,131 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
142144
return models, nil
143145
}
144146

147+
// jsonapiTypeOfModel returns a jsonapi primary type string
148+
// given a struct type that has typical jsonapi struct tags
149+
//
150+
// Example:
151+
// For this type, "posts" is returned. An error is returned if
152+
// no properly-formatted "primary" tag is found for jsonapi
153+
// annotations
154+
//
155+
// type Post struct {
156+
// ID string `jsonapi:"primary,posts"`
157+
// }
158+
func jsonapiTypeOfModel(structModel reflect.Type) (string, error) {
159+
for i := 0; i < structModel.NumField(); i++ {
160+
fieldType := structModel.Field(i)
161+
args, err := getStructTags(fieldType)
162+
if err != nil || len(args) < 2 {
163+
continue
164+
}
165+
166+
if args[0] == annotationPrimary {
167+
return args[1], nil
168+
}
169+
}
170+
171+
return "", errors.New("no primary annotation found on model")
172+
}
173+
174+
// structFieldIndex holds a bit of information about a type found at a struct field index
175+
type structFieldIndex struct {
176+
Type reflect.Type
177+
FieldNum int
178+
}
179+
180+
// joinStructMapping reflects on a value that may be a slice
181+
// of join structs or a join struct. A join struct is a struct
182+
// comprising of pointers to other jsonapi models, only one of
183+
// which is populated with a value by the decoder. The join struct is
184+
// probed and a data structure is generated that maps the
185+
// underlying model type (its 'primary' type) to the field number
186+
// within the join struct.
187+
//
188+
// This data can then be used to correctly assign each data relationship
189+
// to the correct join struct field.
190+
func joinStructMapping(join reflect.Type) (result map[string]structFieldIndex, err error) {
191+
result = make(map[string]structFieldIndex)
192+
193+
for join.Kind() != reflect.Struct {
194+
join = join.Elem()
195+
}
196+
197+
for i := 0; i < join.NumField(); i++ {
198+
fieldType := join.Field(i)
199+
200+
if fieldType.Type.Kind() != reflect.Ptr {
201+
continue
202+
}
203+
204+
subtype := fieldType.Type.Elem()
205+
if t, err := jsonapiTypeOfModel(subtype); err == nil {
206+
result[t] = structFieldIndex{
207+
Type: subtype,
208+
FieldNum: i,
209+
}
210+
}
211+
}
212+
213+
return result, nil
214+
}
215+
216+
func getStructTags(field reflect.StructField) ([]string, error) {
217+
tag := field.Tag.Get("jsonapi")
218+
if tag == "" {
219+
return []string{}, nil
220+
}
221+
222+
args := strings.Split(tag, ",")
223+
if len(args) < 1 {
224+
return nil, ErrBadJSONAPIStructTag
225+
}
226+
227+
annotation := args[0]
228+
229+
if (annotation == annotationClientID && len(args) != 1) ||
230+
(annotation != annotationClientID && len(args) < 2) {
231+
return nil, ErrBadJSONAPIStructTag
232+
}
233+
234+
return args, nil
235+
}
236+
237+
// unmarshalNodeMaybeJoin populates a model that may or may not be
238+
// a join struct that corresponds to a polyrelation or relation
239+
func unmarshalNodeMaybeJoin(m *reflect.Value, data *Node, annotation string, joinMapping map[string]structFieldIndex, included *map[string]*Node) error {
240+
// This will hold either the value of the join model or the actual
241+
// model, depending on annotation
242+
var actualModel = *m
243+
var joinElem *structFieldIndex = nil
244+
245+
if annotation == annotationPolyRelation {
246+
j, ok := joinMapping[data.Type]
247+
if !ok {
248+
// There is no valid join field to assign this type of relation.
249+
return ErrBadJSONAPIJoinStruct
250+
}
251+
joinElem = &j
252+
actualModel = reflect.New(joinElem.Type)
253+
}
254+
255+
if err := unmarshalNode(
256+
fullNode(data, included),
257+
actualModel,
258+
included,
259+
); err != nil {
260+
return err
261+
}
262+
263+
if joinElem != nil {
264+
// actualModel is a pointer to the model type
265+
// m is a pointer to a struct that should hold the actualModel at joinElem.FieldNum
266+
v := m.Elem()
267+
v.Field(joinElem.FieldNum).Set(actualModel)
268+
}
269+
return nil
270+
}
271+
145272
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
146273
defer func() {
147274
if r := recover(); r != nil {
@@ -155,27 +282,18 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
155282
var er error
156283

157284
for i := 0; i < modelValue.NumField(); i++ {
158-
fieldType := modelType.Field(i)
159-
tag := fieldType.Tag.Get("jsonapi")
160-
if tag == "" {
161-
continue
162-
}
163-
164285
fieldValue := modelValue.Field(i)
286+
fieldType := modelType.Field(i)
165287

166-
args := strings.Split(tag, ",")
167-
if len(args) < 1 {
168-
er = ErrBadJSONAPIStructTag
288+
args, err := getStructTags(fieldType)
289+
if err != nil {
290+
er = err
169291
break
170292
}
171-
172-
annotation := args[0]
173-
174-
if (annotation == annotationClientID && len(args) != 1) ||
175-
(annotation != annotationClientID && len(args) < 2) {
176-
er = ErrBadJSONAPIStructTag
177-
break
293+
if len(args) == 0 {
294+
continue
178295
}
296+
annotation := args[0]
179297

180298
if annotation == annotationPrimary {
181299
// Check the JSON API Type
@@ -257,33 +375,48 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
257375
}
258376

259377
assign(fieldValue, value)
260-
} else if annotation == annotationRelation {
378+
} else if annotation == annotationRelation || annotation == annotationPolyRelation {
261379
isSlice := fieldValue.Type().Kind() == reflect.Slice
262380

381+
// No relations of the given name were provided
263382
if data.Relationships == nil || data.Relationships[args[1]] == nil {
264383
continue
265384
}
266385

386+
// If this is a polymorphic relation, each data relationship needs to be assigned
387+
// to it's appropriate join field and fieldValue should be a join field.
388+
var joinMapping map[string]structFieldIndex = nil
389+
if annotation == annotationPolyRelation {
390+
joinMapping, err = joinStructMapping(fieldValue.Type())
391+
if err != nil {
392+
er = err
393+
break
394+
}
395+
}
396+
267397
if isSlice {
268398
// to-many relationship
269399
relationship := new(RelationshipManyNode)
400+
sliceType := fieldValue.Type()
270401

271402
buf := bytes.NewBuffer(nil)
272403

273404
json.NewEncoder(buf).Encode(data.Relationships[args[1]])
274405
json.NewDecoder(buf).Decode(relationship)
275406

276407
data := relationship.Data
277-
models := reflect.New(fieldValue.Type()).Elem()
408+
409+
// This will hold either the value of the slice of join models or
410+
// the slice of models, depending on the annotation
411+
models := reflect.New(sliceType).Elem()
278412

279413
for _, n := range data {
280-
m := reflect.New(fieldValue.Type().Elem().Elem())
414+
// This will hold either the value of the join model or the actual
415+
// model, depending on annotation
416+
m := reflect.New(sliceType.Elem().Elem())
281417

282-
if err := unmarshalNode(
283-
fullNode(n, included),
284-
m,
285-
included,
286-
); err != nil {
418+
err = unmarshalNodeMaybeJoin(&m, n, annotation, joinMapping, included)
419+
if err != nil {
287420
er = err
288421
break
289422
}
@@ -313,20 +446,18 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
313446
continue
314447
}
315448

449+
// This will hold either the value of the join model or the actual
450+
// model, depending on annotation
316451
m := reflect.New(fieldValue.Type().Elem())
317-
if err := unmarshalNode(
318-
fullNode(relationship.Data, included),
319-
m,
320-
included,
321-
); err != nil {
452+
453+
err = unmarshalNodeMaybeJoin(&m, relationship.Data, annotation, joinMapping, included)
454+
if err != nil {
322455
er = err
323456
break
324457
}
325458

326459
fieldValue.Set(m)
327-
328460
}
329-
330461
} else if annotation == annotationLinks {
331462
if data.Links == nil {
332463
continue

0 commit comments

Comments
 (0)