2015-07-05 11:59:30 -04:00
|
|
|
package jsonapi
|
|
|
|
|
|
|
|
import (
|
2015-07-10 11:57:27 -04:00
|
|
|
"encoding/json"
|
2016-01-05 16:13:24 -05:00
|
|
|
"errors"
|
2015-07-05 11:59:30 -04:00
|
|
|
"fmt"
|
2015-07-10 11:57:27 -04:00
|
|
|
"io"
|
2015-07-05 11:59:30 -04:00
|
|
|
"reflect"
|
2016-01-05 16:13:24 -05:00
|
|
|
"strconv"
|
2015-07-05 11:59:30 -04:00
|
|
|
"strings"
|
2015-07-06 17:35:17 -04:00
|
|
|
"time"
|
2015-07-05 11:59:30 -04:00
|
|
|
)
|
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
var (
|
2016-09-13 01:12:42 -04:00
|
|
|
// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
|
|
|
|
// annotation is invalid.
|
2016-01-05 16:13:24 -05:00
|
|
|
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
|
2016-09-13 01:12:42 -04:00
|
|
|
// ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field
|
|
|
|
// was not a valid numeric type.
|
Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)
* Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field.
* No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests.
2016-09-22 18:02:30 -04:00
|
|
|
ErrBadJSONAPIID = errors.New(
|
|
|
|
"id should be either string, int(8,16,32,64) or uint(8,16,32,64)")
|
2016-09-13 01:12:42 -04:00
|
|
|
// ErrExpectedSlice is returned when a variable or arugment was expected to
|
|
|
|
// be a slice of *Structs; MarshalMany will return this error when its
|
|
|
|
// interface{} argument is invalid.
|
|
|
|
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
|
2016-01-05 16:13:24 -05:00
|
|
|
)
|
2015-09-10 18:55:51 -04:00
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
// MarshalOnePayload writes a jsonapi response with one, with related records
|
|
|
|
// sideloaded, into "included" array. This method encodes a response for a
|
|
|
|
// single record only. Hence, data will be a single record rather than an array
|
|
|
|
// of records. If you want to serialize many records, see, MarshalManyPayload.
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
|
|
|
// See UnmarshalPayload for usage example.
|
|
|
|
//
|
|
|
|
// model interface{} should be a pointer to a struct.
|
2015-07-10 20:16:26 -04:00
|
|
|
func MarshalOnePayload(w io.Writer, model interface{}) error {
|
2015-08-07 18:18:10 -04:00
|
|
|
payload, err := MarshalOne(model)
|
2015-07-10 20:16:26 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-07-13 21:32:04 -04:00
|
|
|
// MarshalOnePayloadWithoutIncluded writes a jsonapi response with one object,
|
|
|
|
// without the related records sideloaded into "included" array. If you want to
|
2017-01-24 17:53:31 -05:00
|
|
|
// serialize the relations into the "included" array see MarshalOnePayload.
|
2016-07-13 21:32:04 -04:00
|
|
|
//
|
|
|
|
// model interface{} should be a pointer to a struct.
|
|
|
|
func MarshalOnePayloadWithoutIncluded(w io.Writer, model interface{}) error {
|
|
|
|
included := make(map[string]*Node)
|
|
|
|
|
|
|
|
rootNode, err := visitModelNode(model, &included, true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(&OnePayload{Data: rootNode}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
// MarshalOne does the same as MarshalOnePayload except it just returns the
|
|
|
|
// payload and doesn't write out results. Useful is you use your JSON rendering
|
|
|
|
// library.
|
2015-08-07 18:18:10 -04:00
|
|
|
func MarshalOne(model interface{}) (*OnePayload, error) {
|
2016-01-05 16:13:24 -05:00
|
|
|
included := make(map[string]*Node)
|
|
|
|
|
|
|
|
rootNode, err := visitModelNode(model, &included, true)
|
2015-08-07 18:18:10 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
payload := &OnePayload{Data: rootNode}
|
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
payload.Included = nodeMapValues(&included)
|
2015-08-07 18:18:10 -04:00
|
|
|
|
|
|
|
return payload, nil
|
|
|
|
}
|
|
|
|
|
2017-01-24 17:53:31 -05:00
|
|
|
// MarshalManyPayloadWithoutIncluded writes a jsonapi response with many records,
|
|
|
|
// without the related records sideloaded into "included" array. If you want to
|
|
|
|
// serialize the relations into the "included" array see MarshalManyPayload.
|
|
|
|
//
|
|
|
|
// models interface{} should be a slice of struct pointers.
|
|
|
|
func MarshalManyPayloadWithoutIncluded(w io.Writer, models interface{}) error {
|
|
|
|
m, err := convertToSliceInterface(&models)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
payload, err := MarshalMany(m)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty the included
|
|
|
|
payload.Included = []*Node{}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
// MarshalManyPayload writes a jsonapi response with many records, with related
|
|
|
|
// records sideloaded, into "included" array. This method encodes a response for
|
|
|
|
// a slice of records, hence data will be an array of records rather than a
|
|
|
|
// single record. To serialize a single record, see MarshalOnePayload
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2016-09-13 01:12:42 -04:00
|
|
|
// For example you could pass it, w, your http.ResponseWriter, and, models, a
|
|
|
|
// slice of Blog struct instance pointers as interface{}'s to write to the
|
|
|
|
// response,
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2015-07-13 18:04:21 -04:00
|
|
|
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// // ... fetch your blogs and filter, offset, limit, etc ...
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2015-07-13 18:04:21 -04:00
|
|
|
// blogs := testBlogsForList()
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2017-01-20 21:06:44 -05:00
|
|
|
// w.WriteHeader(http.StatusOK)
|
|
|
|
// w.Header().Set("Content-Type", jsonapi.MediaType)
|
2015-07-13 18:04:21 -04:00
|
|
|
// if err := jsonapi.MarshalManyPayload(w, blogs); err != nil {
|
2017-01-20 21:06:44 -05:00
|
|
|
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
2015-07-13 18:04:21 -04:00
|
|
|
// }
|
|
|
|
// }
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
|
|
|
//
|
2016-07-22 21:31:33 -04:00
|
|
|
// Visit https://github.com/google/jsonapi#list for more info.
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2016-09-13 01:12:42 -04:00
|
|
|
// models interface{} should be a slice of struct pointers.
|
|
|
|
func MarshalManyPayload(w io.Writer, models interface{}) error {
|
|
|
|
m, err := convertToSliceInterface(&models)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
payload, err := MarshalMany(m)
|
2015-08-07 18:18:10 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
// MarshalMany does the same as MarshalManyPayload except it just returns the
|
|
|
|
// payload and doesn't write out results. Useful is you use your JSON rendering
|
|
|
|
// library.
|
2015-08-07 18:18:10 -04:00
|
|
|
func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
2017-01-17 19:04:07 -05:00
|
|
|
payload := &ManyPayload{
|
|
|
|
Data: []*Node{},
|
|
|
|
}
|
|
|
|
included := map[string]*Node{}
|
2015-07-12 14:37:31 -04:00
|
|
|
|
2017-01-17 19:04:07 -05:00
|
|
|
for _, model := range models {
|
2016-01-05 16:13:24 -05:00
|
|
|
node, err := visitModelNode(model, &included, true)
|
2015-07-07 12:52:38 -04:00
|
|
|
if err != nil {
|
2015-08-07 18:18:10 -04:00
|
|
|
return nil, err
|
2015-07-07 12:52:38 -04:00
|
|
|
}
|
2017-01-17 19:04:07 -05:00
|
|
|
payload.Data = append(payload.Data, node)
|
2015-07-10 11:57:27 -04:00
|
|
|
}
|
2017-01-17 19:04:07 -05:00
|
|
|
payload.Included = nodeMapValues(&included)
|
2015-07-10 11:57:27 -04:00
|
|
|
|
2015-08-07 18:18:10 -04:00
|
|
|
return payload, nil
|
2015-07-07 12:52:38 -04:00
|
|
|
}
|
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
// MarshalOnePayloadEmbedded - This method not meant to for use in
|
|
|
|
// implementation code, although feel free. The purpose of this method is for
|
|
|
|
// use in tests. In most cases, your request payloads for create will be
|
|
|
|
// embedded rather than sideloaded for related records. This method will
|
|
|
|
// serialize a single struct pointer into an embedded json response. In other
|
|
|
|
// words, there will be no, "included", array in the json all relationships will
|
|
|
|
// be serailized inline in the data.
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
2016-09-13 01:12:42 -04:00
|
|
|
// However, in tests, you may want to construct payloads to post to create
|
|
|
|
// methods that are embedded to most closely resemble the payloads that will be
|
|
|
|
// produced by the client. This is what this method is intended for.
|
2015-07-13 14:23:03 -04:00
|
|
|
//
|
|
|
|
// model interface{} should be a pointer to a struct.
|
2015-07-10 20:34:04 -04:00
|
|
|
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
2016-01-05 16:13:24 -05:00
|
|
|
rootNode, err := visitModelNode(model, nil, false)
|
2015-07-08 16:28:38 -04:00
|
|
|
if err != nil {
|
2015-07-10 20:34:04 -04:00
|
|
|
return err
|
2015-07-08 16:28:38 -04:00
|
|
|
}
|
|
|
|
|
2015-07-10 20:34:04 -04:00
|
|
|
payload := &OnePayload{Data: rootNode}
|
2015-07-08 16:28:38 -04:00
|
|
|
|
2015-07-10 20:34:04 -04:00
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-07-08 16:28:38 -04:00
|
|
|
|
2015-07-10 20:34:04 -04:00
|
|
|
return nil
|
2015-07-08 16:28:38 -04:00
|
|
|
}
|
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
func visitModelNode(model interface{}, included *map[string]*Node,
|
|
|
|
sideload bool) (*Node, error) {
|
2015-07-10 12:07:12 -04:00
|
|
|
node := new(Node)
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2015-07-06 17:39:24 -04:00
|
|
|
var er error
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
modelValue := reflect.ValueOf(model).Elem()
|
Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)
* Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field.
* No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests.
2016-09-22 18:02:30 -04:00
|
|
|
modelType := reflect.ValueOf(model).Type().Elem()
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
for i := 0; i < modelValue.NumField(); i++ {
|
|
|
|
structField := modelValue.Type().Field(i)
|
2017-01-20 19:13:04 -05:00
|
|
|
tag := structField.Tag.Get(annotationJSONAPI)
|
2015-07-08 16:11:03 -04:00
|
|
|
if tag == "" {
|
2016-01-05 16:13:24 -05:00
|
|
|
continue
|
2015-07-08 16:11:03 -04:00
|
|
|
}
|
|
|
|
|
2015-07-20 19:10:52 -04:00
|
|
|
fieldValue := modelValue.Field(i)
|
Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)
* Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field.
* No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests.
2016-09-22 18:02:30 -04:00
|
|
|
fieldType := modelType.Field(i)
|
2015-07-20 19:10:52 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
args := strings.Split(tag, annotationSeperator)
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
if len(args) < 1 {
|
2016-01-05 16:13:24 -05:00
|
|
|
er = ErrBadJSONAPIStructTag
|
|
|
|
break
|
2015-07-08 14:49:36 -04:00
|
|
|
}
|
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
annotation := args[0]
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
if (annotation == annotationClientID && len(args) != 1) ||
|
|
|
|
(annotation != annotationClientID && len(args) < 2) {
|
2016-01-05 16:13:24 -05:00
|
|
|
er = ErrBadJSONAPIStructTag
|
|
|
|
break
|
2015-09-10 18:55:51 -04:00
|
|
|
}
|
2015-07-10 11:20:49 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
if annotation == annotationPrimary {
|
Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)
* Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field.
* No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests.
2016-09-22 18:02:30 -04:00
|
|
|
v := fieldValue
|
|
|
|
|
|
|
|
// Deal with PTRS
|
|
|
|
var kind reflect.Kind
|
|
|
|
if fieldValue.Kind() == reflect.Ptr {
|
|
|
|
kind = fieldType.Type.Elem().Kind()
|
|
|
|
v = reflect.Indirect(fieldValue)
|
|
|
|
} else {
|
|
|
|
kind = fieldType.Type.Kind()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle allowed types
|
|
|
|
switch kind {
|
|
|
|
case reflect.String:
|
|
|
|
node.ID = v.Interface().(string)
|
|
|
|
case reflect.Int:
|
|
|
|
node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10)
|
|
|
|
case reflect.Int8:
|
|
|
|
node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10)
|
|
|
|
case reflect.Int16:
|
|
|
|
node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10)
|
|
|
|
case reflect.Int32:
|
|
|
|
node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10)
|
|
|
|
case reflect.Int64:
|
|
|
|
node.ID = strconv.FormatInt(v.Interface().(int64), 10)
|
|
|
|
case reflect.Uint:
|
|
|
|
node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10)
|
|
|
|
case reflect.Uint8:
|
|
|
|
node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10)
|
|
|
|
case reflect.Uint16:
|
|
|
|
node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10)
|
|
|
|
case reflect.Uint32:
|
|
|
|
node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10)
|
|
|
|
case reflect.Uint64:
|
|
|
|
node.ID = strconv.FormatUint(v.Interface().(uint64), 10)
|
2016-01-07 15:14:57 -05:00
|
|
|
default:
|
Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)
* Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field.
* No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests.
2016-09-22 18:02:30 -04:00
|
|
|
// We had a JSON float (numeric), but our field was not one of the
|
|
|
|
// allowed numeric types
|
2016-01-07 15:14:57 -05:00
|
|
|
er = ErrBadJSONAPIID
|
|
|
|
break
|
2016-01-05 16:13:24 -05:00
|
|
|
}
|
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
node.Type = args[1]
|
2017-01-20 19:13:04 -05:00
|
|
|
} else if annotation == annotationClientID {
|
2016-01-05 16:13:24 -05:00
|
|
|
clientID := fieldValue.String()
|
|
|
|
if clientID != "" {
|
2016-07-05 21:32:15 -04:00
|
|
|
node.ClientID = clientID
|
2016-01-05 16:13:24 -05:00
|
|
|
}
|
2017-01-20 19:13:04 -05:00
|
|
|
} else if annotation == annotationAttribute {
|
2016-09-22 16:58:07 -04:00
|
|
|
var omitEmpty, iso8601 bool
|
2016-01-05 16:13:24 -05:00
|
|
|
|
|
|
|
if len(args) > 2 {
|
2016-09-22 16:58:07 -04:00
|
|
|
for _, arg := range args[2:] {
|
|
|
|
switch arg {
|
2017-01-20 19:13:04 -05:00
|
|
|
case annotationOmitEmpty:
|
2016-09-22 16:58:07 -04:00
|
|
|
omitEmpty = true
|
2017-01-20 19:13:04 -05:00
|
|
|
case annotationISO8601:
|
2016-09-22 16:58:07 -04:00
|
|
|
iso8601 = true
|
|
|
|
}
|
|
|
|
}
|
2016-01-05 16:13:24 -05:00
|
|
|
}
|
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
if node.Attributes == nil {
|
|
|
|
node.Attributes = make(map[string]interface{})
|
|
|
|
}
|
2015-07-06 13:38:42 -04:00
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
2016-01-05 16:13:24 -05:00
|
|
|
t := fieldValue.Interface().(time.Time)
|
|
|
|
|
|
|
|
if t.IsZero() {
|
|
|
|
continue
|
2015-07-06 13:38:42 -04:00
|
|
|
}
|
|
|
|
|
2016-09-22 16:58:07 -04:00
|
|
|
if iso8601 {
|
|
|
|
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
|
|
|
} else {
|
|
|
|
node.Attributes[args[1]] = t.Unix()
|
|
|
|
}
|
2015-10-13 12:10:10 -04:00
|
|
|
} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
|
|
|
// A time pointer may be nil
|
2016-01-05 16:13:24 -05:00
|
|
|
if fieldValue.IsNil() {
|
|
|
|
if omitEmpty {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2015-10-13 12:10:10 -04:00
|
|
|
node.Attributes[args[1]] = nil
|
|
|
|
} else {
|
2016-01-05 16:13:24 -05:00
|
|
|
tm := fieldValue.Interface().(*time.Time)
|
|
|
|
|
|
|
|
if tm.IsZero() && omitEmpty {
|
|
|
|
continue
|
2015-10-13 12:10:10 -04:00
|
|
|
}
|
|
|
|
|
2016-09-22 16:58:07 -04:00
|
|
|
if iso8601 {
|
|
|
|
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
|
|
|
} else {
|
|
|
|
node.Attributes[args[1]] = tm.Unix()
|
|
|
|
}
|
2015-10-13 12:10:10 -04:00
|
|
|
}
|
2015-09-10 18:55:51 -04:00
|
|
|
} else {
|
2016-07-16 11:24:21 -04:00
|
|
|
// Dealing with a fieldValue that is not a time
|
|
|
|
emptyValue := reflect.Zero(fieldValue.Type())
|
2016-01-05 16:13:24 -05:00
|
|
|
|
2016-07-16 11:24:21 -04:00
|
|
|
// See if we need to omit this field
|
|
|
|
if omitEmpty && fieldValue.Interface() == emptyValue.Interface() {
|
2016-01-05 16:13:24 -05:00
|
|
|
continue
|
2016-07-16 11:24:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
strAttr, ok := fieldValue.Interface().(string)
|
|
|
|
if ok {
|
2016-01-05 16:13:24 -05:00
|
|
|
node.Attributes[args[1]] = strAttr
|
|
|
|
} else {
|
|
|
|
node.Attributes[args[1]] = fieldValue.Interface()
|
|
|
|
}
|
2015-09-10 18:55:51 -04:00
|
|
|
}
|
2017-01-20 19:13:04 -05:00
|
|
|
} else if annotation == annotationRelation {
|
|
|
|
var omitEmpty bool
|
|
|
|
|
|
|
|
//add support for 'omitempty' struct tag for marshaling as absent
|
|
|
|
if len(args) > 2 {
|
|
|
|
omitEmpty = args[2] == annotationOmitEmpty
|
|
|
|
}
|
2015-07-06 13:38:42 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
|
|
|
if omitEmpty &&
|
|
|
|
(isSlice && fieldValue.Len() < 1 ||
|
|
|
|
(!isSlice && fieldValue.IsNil())) {
|
2016-01-05 16:13:24 -05:00
|
|
|
continue
|
2015-09-10 18:55:51 -04:00
|
|
|
}
|
2015-07-06 13:38:42 -04:00
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
if node.Relationships == nil {
|
|
|
|
node.Relationships = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
2017-01-20 20:55:59 -05:00
|
|
|
var relLinks *Links
|
|
|
|
if linkableModel, ok := model.(RelationshipLinkable); ok {
|
|
|
|
relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
|
|
|
|
}
|
|
|
|
|
2015-09-10 18:55:51 -04:00
|
|
|
if isSlice {
|
2017-01-20 19:13:04 -05:00
|
|
|
// to-many relationship
|
2016-09-13 01:12:42 -04:00
|
|
|
relationship, err := visitModelNodeRelationships(
|
|
|
|
fieldValue,
|
|
|
|
included,
|
|
|
|
sideload,
|
|
|
|
)
|
2017-01-20 19:13:04 -05:00
|
|
|
if err != nil {
|
|
|
|
er = err
|
|
|
|
break
|
|
|
|
}
|
2017-01-20 20:55:59 -05:00
|
|
|
relationship.Links = relLinks
|
2015-09-10 18:55:51 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
if sideload {
|
|
|
|
shallowNodes := []*Node{}
|
|
|
|
for _, n := range relationship.Data {
|
|
|
|
appendIncluded(included, n)
|
|
|
|
shallowNodes = append(shallowNodes, toShallowNode(n))
|
|
|
|
}
|
2015-09-10 18:55:51 -04:00
|
|
|
|
2017-01-20 19:13:04 -05:00
|
|
|
node.Relationships[args[1]] = &RelationshipManyNode{
|
2017-01-20 20:55:59 -05:00
|
|
|
Data: shallowNodes,
|
|
|
|
Links: relationship.Links,
|
2015-07-06 13:38:42 -04:00
|
|
|
}
|
|
|
|
} else {
|
2017-01-20 19:13:04 -05:00
|
|
|
node.Relationships[args[1]] = relationship
|
2015-09-10 18:55:51 -04:00
|
|
|
}
|
|
|
|
} else {
|
2017-01-20 19:13:04 -05:00
|
|
|
// to-one relationships
|
|
|
|
|
|
|
|
// Handle null relationship case
|
|
|
|
if fieldValue.IsNil() {
|
|
|
|
node.Relationships[args[1]] = &RelationshipOneNode{Data: nil}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-09-13 01:12:42 -04:00
|
|
|
relationship, err := visitModelNode(
|
|
|
|
fieldValue.Interface(),
|
|
|
|
included,
|
2017-01-20 19:13:04 -05:00
|
|
|
sideload,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2015-09-10 18:55:51 -04:00
|
|
|
er = err
|
2016-01-05 16:13:24 -05:00
|
|
|
break
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2017-01-20 19:13:04 -05:00
|
|
|
|
|
|
|
if sideload {
|
|
|
|
appendIncluded(included, relationship)
|
|
|
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
2017-01-20 20:55:59 -05:00
|
|
|
Data: toShallowNode(relationship),
|
|
|
|
Links: relLinks,
|
2017-01-20 19:13:04 -05:00
|
|
|
}
|
|
|
|
} else {
|
2017-01-20 20:55:59 -05:00
|
|
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
|
|
|
Data: relationship,
|
|
|
|
Links: relLinks,
|
|
|
|
}
|
2017-01-20 19:13:04 -05:00
|
|
|
}
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2015-09-10 18:55:51 -04:00
|
|
|
|
|
|
|
} else {
|
2016-01-05 16:13:24 -05:00
|
|
|
er = ErrBadJSONAPIStructTag
|
|
|
|
break
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2016-01-05 16:13:24 -05:00
|
|
|
}
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2015-07-06 17:39:24 -04:00
|
|
|
if er != nil {
|
2016-01-05 16:13:24 -05:00
|
|
|
return nil, er
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
2017-01-20 20:55:59 -05:00
|
|
|
|
|
|
|
if linkableModel, isLinkable := model.(Linkable); isLinkable {
|
|
|
|
jl := linkableModel.JSONAPILinks()
|
|
|
|
if er := jl.validate(); er != nil {
|
|
|
|
return nil, er
|
|
|
|
}
|
|
|
|
node.Links = linkableModel.JSONAPILinks()
|
|
|
|
}
|
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
return node, nil
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
|
|
|
|
2015-07-10 14:41:54 -04:00
|
|
|
func toShallowNode(node *Node) *Node {
|
|
|
|
return &Node{
|
2016-07-05 21:32:15 -04:00
|
|
|
ID: node.ID,
|
2015-07-10 14:41:54 -04:00
|
|
|
Type: node.Type,
|
|
|
|
}
|
2015-07-10 11:25:24 -04:00
|
|
|
}
|
|
|
|
|
2017-01-28 22:43:47 -05:00
|
|
|
func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node,
|
|
|
|
sideload bool) (*RelationshipManyNode, error) {
|
2017-01-20 19:13:04 -05:00
|
|
|
nodes := []*Node{}
|
2015-10-12 14:05:06 -04:00
|
|
|
|
2015-07-05 13:59:35 -04:00
|
|
|
for i := 0; i < models.Len(); i++ {
|
2016-01-05 16:13:24 -05:00
|
|
|
n := models.Index(i).Interface()
|
2017-01-20 19:13:04 -05:00
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
node, err := visitModelNode(n, included, sideload)
|
2015-07-05 13:59:35 -04:00
|
|
|
if err != nil {
|
2016-01-05 16:13:24 -05:00
|
|
|
return nil, err
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
nodes = append(nodes, node)
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
return &RelationshipManyNode{Data: nodes}, nil
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2015-07-07 12:52:38 -04:00
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
func appendIncluded(m *map[string]*Node, nodes ...*Node) {
|
|
|
|
included := *m
|
2015-07-12 10:59:37 -04:00
|
|
|
|
2016-01-05 16:13:24 -05:00
|
|
|
for _, n := range nodes {
|
2016-07-05 21:32:15 -04:00
|
|
|
k := fmt.Sprintf("%s,%s", n.Type, n.ID)
|
2016-01-05 16:13:24 -05:00
|
|
|
|
|
|
|
if _, hasNode := included[k]; hasNode {
|
|
|
|
continue
|
2015-07-12 10:59:37 -04:00
|
|
|
}
|
2016-01-05 16:13:24 -05:00
|
|
|
|
|
|
|
included[k] = n
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func nodeMapValues(m *map[string]*Node) []*Node {
|
|
|
|
mp := *m
|
|
|
|
nodes := make([]*Node, len(mp))
|
|
|
|
|
|
|
|
i := 0
|
|
|
|
for _, n := range mp {
|
|
|
|
nodes[i] = n
|
|
|
|
i++
|
2015-07-12 10:59:37 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return nodes
|
|
|
|
}
|
2016-09-13 01:12:42 -04:00
|
|
|
|
|
|
|
func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
|
|
|
|
vals := reflect.ValueOf(*i)
|
|
|
|
if vals.Kind() != reflect.Slice {
|
|
|
|
return nil, ErrExpectedSlice
|
|
|
|
}
|
|
|
|
var response []interface{}
|
|
|
|
for x := 0; x < vals.Len(); x++ {
|
|
|
|
response = append(response, vals.Index(x).Interface())
|
|
|
|
}
|
|
|
|
return response, nil
|
|
|
|
}
|