2015-07-05 11:59:30 -04:00
|
|
|
package jsonapi
|
|
|
|
|
|
|
|
import (
|
2015-07-10 11:57:27 -04:00
|
|
|
"encoding/json"
|
2015-07-05 11:59:30 -04:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2015-07-10 11:57:27 -04:00
|
|
|
"io"
|
2015-07-05 11:59:30 -04:00
|
|
|
"reflect"
|
|
|
|
"strings"
|
2015-07-06 17:35:17 -04:00
|
|
|
"time"
|
2015-07-05 11:59:30 -04:00
|
|
|
)
|
|
|
|
|
2015-07-13 14:23:03 -04:00
|
|
|
// Wrties 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.
|
|
|
|
//
|
|
|
|
// 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 {
|
|
|
|
rootNode, included, err := visitModelNode(model, true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
payload := &OnePayload{Data: rootNode}
|
|
|
|
|
2015-07-12 10:59:37 -04:00
|
|
|
payload.Included = uniqueByTypeAndId(included)
|
2015-07-10 20:16:26 -04:00
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-07-13 14:23:03 -04:00
|
|
|
// Wrties 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
|
|
|
|
//
|
|
|
|
// 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,
|
|
|
|
//
|
|
|
|
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// // ... fetch your blogs and filter, offset, limit, etc ...
|
|
|
|
//
|
|
|
|
// blogs := testBlogsForList()
|
|
|
|
//
|
|
|
|
// w.WriteHeader(200)
|
|
|
|
// w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
|
|
// if err := jsonapi.MarshalManyPayload(w, blogs); err != nil {
|
|
|
|
// http.Error(w, err.Error(), 500)
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Visit https://github.com/shwoodard/jsonapi#list for more info.
|
|
|
|
//
|
|
|
|
// models []interface{} should be a slice of struct pointers.
|
2015-07-12 14:45:03 -04:00
|
|
|
func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
|
|
|
modelsValues := reflect.ValueOf(models)
|
|
|
|
data := make([]*Node, 0, modelsValues.Len())
|
2015-07-07 12:52:38 -04:00
|
|
|
|
2015-07-10 12:07:12 -04:00
|
|
|
incl := make([]*Node, 0)
|
2015-07-07 12:52:38 -04:00
|
|
|
|
2015-07-12 14:45:03 -04:00
|
|
|
for i := 0; i < modelsValues.Len(); i += 1 {
|
|
|
|
model := modelsValues.Index(i).Interface()
|
2015-07-12 14:37:31 -04:00
|
|
|
|
2015-07-08 16:28:38 -04:00
|
|
|
node, included, err := visitModelNode(model, true)
|
2015-07-07 12:52:38 -04:00
|
|
|
if err != nil {
|
2015-07-10 11:57:27 -04:00
|
|
|
return err
|
2015-07-07 12:52:38 -04:00
|
|
|
}
|
|
|
|
data = append(data, node)
|
|
|
|
incl = append(incl, included...)
|
|
|
|
}
|
|
|
|
|
2015-07-10 12:07:12 -04:00
|
|
|
payload := &ManyPayload{
|
2015-07-07 12:52:38 -04:00
|
|
|
Data: data,
|
2015-07-12 10:59:37 -04:00
|
|
|
Included: uniqueByTypeAndId(incl),
|
2015-07-10 11:57:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2015-07-07 12:52:38 -04:00
|
|
|
}
|
|
|
|
|
2015-07-13 14:23:03 -04:00
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
// However, in tests, you may want to construct payloads to post to create methods
|
|
|
|
// that are embedded to most closely resember the payloads that will be produced by
|
|
|
|
// the client. This is what this method is intended for.
|
|
|
|
//
|
|
|
|
// 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 {
|
2015-07-08 16:28:38 -04:00
|
|
|
rootNode, _, err := visitModelNode(model, false)
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2015-07-10 12:07:12 -04:00
|
|
|
func visitModelNode(model interface{}, sideload bool) (*Node, []*Node, error) {
|
|
|
|
node := new(Node)
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2015-07-06 17:39:24 -04:00
|
|
|
var er error
|
2015-07-10 12:07:12 -04:00
|
|
|
var included []*Node
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
modelType := reflect.TypeOf(model).Elem()
|
|
|
|
modelValue := reflect.ValueOf(model).Elem()
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
var i = 0
|
2015-07-05 13:59:35 -04:00
|
|
|
modelType.FieldByNameFunc(func(name string) bool {
|
2015-07-06 13:38:42 -04:00
|
|
|
fieldValue := modelValue.Field(i)
|
2015-07-06 17:35:17 -04:00
|
|
|
structField := modelType.Field(i)
|
2015-07-05 11:59:30 -04:00
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
i += 1
|
|
|
|
|
2015-07-06 17:35:17 -04:00
|
|
|
tag := structField.Tag.Get("jsonapi")
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-08 16:11:03 -04:00
|
|
|
if tag == "" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
args := strings.Split(tag, ",")
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-08 14:49:36 -04:00
|
|
|
if len(args) != 2 {
|
|
|
|
er = errors.New(fmt.Sprintf("jsonapi tag, on %s, had two few arguments", structField.Name))
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
if len(args) >= 1 && args[0] != "" {
|
|
|
|
annotation := args[0]
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-06 13:38:42 -04:00
|
|
|
if annotation == "primary" {
|
2015-07-10 11:20:49 -04:00
|
|
|
node.Id = fmt.Sprintf("%v", fieldValue.Interface())
|
|
|
|
node.Type = args[1]
|
2015-07-06 13:38:42 -04:00
|
|
|
} else if annotation == "attr" {
|
|
|
|
if node.Attributes == nil {
|
|
|
|
node.Attributes = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
2015-07-10 11:20:49 -04:00
|
|
|
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
|
|
|
isZeroMethod := fieldValue.MethodByName("IsZero")
|
|
|
|
isZero := isZeroMethod.Call(make([]reflect.Value, 0))[0].Interface().(bool)
|
|
|
|
if isZero {
|
|
|
|
return false
|
2015-07-06 17:35:17 -04:00
|
|
|
}
|
2015-07-10 11:20:49 -04:00
|
|
|
|
|
|
|
unix := fieldValue.MethodByName("Unix")
|
|
|
|
val := unix.Call(make([]reflect.Value, 0))[0]
|
|
|
|
node.Attributes[args[1]] = val.Int()
|
2015-07-06 13:38:42 -04:00
|
|
|
} else {
|
2015-07-10 11:20:49 -04:00
|
|
|
node.Attributes[args[1]] = fieldValue.Interface()
|
2015-07-06 13:38:42 -04:00
|
|
|
}
|
|
|
|
} else if annotation == "relation" {
|
|
|
|
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
|
|
|
|
|
|
|
if (isSlice && fieldValue.Len() < 1) || (!isSlice && fieldValue.IsNil()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Relationships == nil {
|
|
|
|
node.Relationships = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
2015-07-12 11:02:29 -04:00
|
|
|
if sideload && included == nil {
|
2015-07-10 12:07:12 -04:00
|
|
|
included = make([]*Node, 0)
|
2015-07-06 13:38:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if isSlice {
|
2015-07-12 10:42:52 -04:00
|
|
|
relationship, incl, err := visitModelNodeRelationships(args[1], fieldValue, sideload)
|
|
|
|
d := relationship.Data
|
2015-07-06 13:38:42 -04:00
|
|
|
|
|
|
|
if err == nil {
|
2015-07-08 16:28:38 -04:00
|
|
|
if sideload {
|
2015-07-12 10:42:52 -04:00
|
|
|
included = append(included, incl...)
|
2015-07-10 12:07:12 -04:00
|
|
|
shallowNodes := make([]*Node, 0)
|
2015-07-08 16:28:38 -04:00
|
|
|
for _, node := range d {
|
2015-07-10 14:41:54 -04:00
|
|
|
shallowNodes = append(shallowNodes, toShallowNode(node))
|
2015-07-08 16:28:38 -04:00
|
|
|
}
|
2015-07-06 20:04:26 -04:00
|
|
|
|
2015-07-10 12:07:12 -04:00
|
|
|
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
|
2015-07-08 16:28:38 -04:00
|
|
|
} else {
|
2015-07-12 10:42:52 -04:00
|
|
|
node.Relationships[args[1]] = relationship
|
2015-07-08 16:28:38 -04:00
|
|
|
}
|
2015-07-05 13:59:35 -04:00
|
|
|
} else {
|
2015-07-06 17:39:24 -04:00
|
|
|
er = err
|
|
|
|
return false
|
2015-07-06 13:38:42 -04:00
|
|
|
}
|
|
|
|
} else {
|
2015-07-12 10:42:52 -04:00
|
|
|
relationship, incl, err := visitModelNode(fieldValue.Interface(), sideload)
|
2015-07-06 13:38:42 -04:00
|
|
|
if err == nil {
|
2015-07-08 16:28:38 -04:00
|
|
|
if sideload {
|
2015-07-12 10:42:52 -04:00
|
|
|
included = append(included, incl...)
|
|
|
|
included = append(included, relationship)
|
2015-07-10 14:41:54 -04:00
|
|
|
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
|
2015-07-08 16:28:38 -04:00
|
|
|
} else {
|
2015-07-10 12:07:12 -04:00
|
|
|
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
|
2015-07-08 16:28:38 -04:00
|
|
|
}
|
2015-07-06 13:38:42 -04:00
|
|
|
} else {
|
2015-07-06 17:39:24 -04:00
|
|
|
er = err
|
|
|
|
return false
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2015-07-06 13:38:42 -04:00
|
|
|
|
|
|
|
} else {
|
2015-07-06 17:39:24 -04:00
|
|
|
er = errors.New(fmt.Sprintf("Unsupported jsonapi tag annotation, %s", annotation))
|
|
|
|
return false
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
|
2015-07-06 17:39:24 -04:00
|
|
|
if er != nil {
|
|
|
|
return nil, nil, er
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return node, included, nil
|
|
|
|
}
|
|
|
|
|
2015-07-10 14:41:54 -04:00
|
|
|
func toShallowNode(node *Node) *Node {
|
|
|
|
return &Node{
|
|
|
|
Id: node.Id,
|
|
|
|
Type: node.Type,
|
|
|
|
}
|
2015-07-10 11:25:24 -04:00
|
|
|
}
|
|
|
|
|
2015-07-12 10:42:52 -04:00
|
|
|
func visitModelNodeRelationships(relationName string, models reflect.Value, sideload bool) (*RelationshipManyNode, []*Node, error) {
|
2015-07-10 12:07:12 -04:00
|
|
|
nodes := make([]*Node, 0)
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-12 10:42:52 -04:00
|
|
|
var included []*Node
|
|
|
|
if sideload {
|
|
|
|
included = make([]*Node, 0)
|
|
|
|
}
|
|
|
|
|
2015-07-05 13:59:35 -04:00
|
|
|
for i := 0; i < models.Len(); i++ {
|
2015-07-12 10:42:52 -04:00
|
|
|
node, incl, err := visitModelNode(models.Index(i).Interface(), sideload)
|
2015-07-05 13:59:35 -04:00
|
|
|
if err != nil {
|
2015-07-12 10:42:52 -04:00
|
|
|
return nil, nil, err
|
2015-07-05 13:59:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
nodes = append(nodes, node)
|
2015-07-12 10:42:52 -04:00
|
|
|
included = append(included, incl...)
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
|
|
|
|
2015-07-12 10:42:52 -04:00
|
|
|
included = append(included, nodes...)
|
|
|
|
|
|
|
|
n := &RelationshipManyNode{Data: nodes}
|
2015-07-05 13:59:35 -04:00
|
|
|
|
2015-07-12 10:42:52 -04:00
|
|
|
return n, included, nil
|
2015-07-05 11:59:30 -04:00
|
|
|
}
|
2015-07-07 12:52:38 -04:00
|
|
|
|
2015-07-12 10:59:37 -04:00
|
|
|
func uniqueByTypeAndId(nodes []*Node) []*Node {
|
|
|
|
uniqueIncluded := make(map[string]*Node)
|
|
|
|
|
2015-07-12 11:46:30 -04:00
|
|
|
for i := 0; i < len(nodes); i += 1 {
|
|
|
|
n := nodes[i]
|
2015-07-12 10:59:37 -04:00
|
|
|
k := fmt.Sprintf("%s,%s", n.Type, n.Id)
|
|
|
|
if uniqueIncluded[k] == nil {
|
|
|
|
uniqueIncluded[k] = n
|
|
|
|
} else {
|
2015-07-12 11:46:30 -04:00
|
|
|
nodes = append(nodes[:i], nodes[i+1:]...)
|
|
|
|
i--
|
2015-07-12 10:59:37 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes
|
|
|
|
}
|