jsonapi/response.go

381 lines
9.2 KiB
Go
Raw Normal View History

2015-07-05 11:59:30 -04:00
package jsonapi
import (
"encoding/json"
2016-01-05 16:13:24 -05:00
"errors"
2015-07-05 11:59:30 -04:00
"fmt"
"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"
"time"
2015-07-05 11:59:30 -04:00
)
2016-01-05 16:13:24 -05:00
var (
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
ErrBadJSONAPIID = errors.New("id should be either string or int")
)
2015-09-10 18:55:51 -04:00
// MarshalOnePayload writes a jsonapi response with one, with related records sideloaded, into "included" array.
2015-07-13 14:23:03 -04:00
// 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 {
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
}
// MarshalOnePayloadWithoutIncluded writes a jsonapi response with one object,
// without the related records sideloaded into "included" array. If you want to
// serialzie the realtions into the "included" array see MarshalOnePayload.
//
// 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
}
// 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.
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)
if err != nil {
return nil, err
}
payload := &OnePayload{Data: rootNode}
2016-01-05 16:13:24 -05:00
payload.Included = nodeMapValues(&included)
return payload, nil
}
// MarshalManyPayload writes a jsonapi response with many records, with related records sideloaded, into "included" array.
2015-07-13 14:23:03 -04:00
// 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,
//
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
//
2015-07-13 18:04:21 -04:00
// 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)
// }
// }
2015-07-13 14:23:03 -04:00
//
//
// Visit https://github.com/shwoodard/jsonapi#list for more info.
//
// models []interface{} should be a slice of struct pointers.
func MarshalManyPayload(w io.Writer, models []interface{}) error {
payload, err := MarshalMany(models)
if err != nil {
return err
}
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
// 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.
func MarshalMany(models []interface{}) (*ManyPayload, error) {
2016-01-05 16:13:24 -05:00
var data []*Node
included := make(map[string]*Node)
2015-07-07 12:52:38 -04:00
2016-01-05 16:13:24 -05:00
for i := 0; i < len(models); i++ {
model := models[i]
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 {
return nil, err
2015-07-07 12:52:38 -04:00
}
data = append(data, node)
2016-01-05 16:13:24 -05:00
}
if len(models) == 0 {
data = make([]*Node, 0)
2015-07-07 12:52:38 -04:00
}
payload := &ManyPayload{
2015-07-07 12:52:38 -04:00
Data: data,
2016-01-05 16:13:24 -05:00
Included: nodeMapValues(&included),
}
return payload, nil
2015-07-07 12:52:38 -04:00
}
// MarshalOnePayloadEmbedded - This method not meant to for use in implementation code, although feel
2015-07-13 14:23:03 -04:00
// 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
2015-07-13 17:51:52 -04:00
// that are embedded to most closely resemble the payloads that will be produced by
2015-07-13 14:23:03 -04:00
// the client. This is what this method is intended for.
//
// model interface{} should be a pointer to a struct.
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
2016-01-05 16:13:24 -05:00
rootNode, err := visitModelNode(model, nil, false)
if err != nil {
return err
}
payload := &OnePayload{Data: rootNode}
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
2016-01-05 16:13:24 -05:00
func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*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-05 13:59:35 -04:00
2015-07-06 13:38:42 -04:00
modelValue := reflect.ValueOf(model).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)
tag := structField.Tag.Get("jsonapi")
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
}
fieldValue := modelValue.Field(i)
2015-07-06 13:38:42 -04:00
args := strings.Split(tag, ",")
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-09-10 18:55:51 -04:00
annotation := args[0]
2015-07-05 13:59:35 -04:00
2016-01-05 16:13:24 -05:00
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) < 2) {
er = ErrBadJSONAPIStructTag
break
2015-09-10 18:55:51 -04:00
}
2015-09-10 18:55:51 -04:00
if annotation == "primary" {
2016-01-05 16:13:24 -05:00
id := fieldValue.Interface()
2016-07-05 21:32:15 -04:00
switch nID := id.(type) {
case string:
2016-07-05 21:32:15 -04:00
node.ID = nID
case int:
2016-07-05 21:32:15 -04:00
node.ID = strconv.Itoa(nID)
case int64:
2016-07-05 21:32:15 -04:00
node.ID = strconv.FormatInt(nID, 10)
case uint64:
2016-07-05 21:32:15 -04:00
node.ID = strconv.FormatUint(nID, 10)
default:
er = ErrBadJSONAPIID
break
2016-01-05 16:13:24 -05:00
}
2015-09-10 18:55:51 -04:00
node.Type = args[1]
} else if annotation == "client-id" {
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
}
2015-09-10 18:55:51 -04:00
} else if annotation == "attr" {
2016-01-05 16:13:24 -05:00
var omitEmpty bool
if len(args) > 2 {
omitEmpty = args[2] == "omitempty"
}
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-01-05 16:13:24 -05:00
node.Attributes[args[1]] = t.Unix()
} 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
}
node.Attributes[args[1]] = nil
} else {
2016-01-05 16:13:24 -05:00
tm := fieldValue.Interface().(*time.Time)
if tm.IsZero() && omitEmpty {
continue
}
2016-01-05 16:13:24 -05:00
node.Attributes[args[1]] = tm.Unix()
}
2015-09-10 18:55:51 -04:00
} else {
2016-01-05 16:13:24 -05:00
strAttr, ok := fieldValue.Interface().(string)
if ok && strAttr == "" && omitEmpty {
continue
} else if ok {
node.Attributes[args[1]] = strAttr
} else {
node.Attributes[args[1]] = fieldValue.Interface()
}
2015-09-10 18:55:51 -04:00
}
} else if annotation == "relation" {
isSlice := fieldValue.Type().Kind() == reflect.Slice
2015-07-06 13:38:42 -04:00
2015-09-10 18:55:51 -04:00
if (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{})
}
if isSlice {
2016-01-05 16:13:24 -05:00
relationship, err := visitModelNodeRelationships(args[1], fieldValue, included, sideload)
2015-09-10 18:55:51 -04:00
if err == nil {
2015-10-12 14:05:06 -04:00
d := relationship.Data
2015-09-10 18:55:51 -04:00
if sideload {
var shallowNodes []*Node
2016-01-05 16:13:24 -05:00
for _, n := range d {
appendIncluded(included, n)
shallowNodes = append(shallowNodes, toShallowNode(n))
}
2015-09-10 18:55:51 -04:00
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
2015-07-05 13:59:35 -04:00
} else {
2015-09-10 18:55:51 -04:00
node.Relationships[args[1]] = relationship
2015-07-06 13:38:42 -04:00
}
} else {
2015-09-10 18:55:51 -04:00
er = err
2016-01-05 16:13:24 -05:00
break
2015-09-10 18:55:51 -04:00
}
} else {
2016-01-05 16:13:24 -05:00
relationship, err := visitModelNode(fieldValue.Interface(), included, sideload)
2015-09-10 18:55:51 -04:00
if err == nil {
if sideload {
2016-01-05 16:13:24 -05:00
appendIncluded(included, relationship)
2015-09-10 18:55:51 -04:00
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
2015-07-06 13:38:42 -04:00
} else {
2015-09-10 18:55:51 -04:00
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
2015-07-05 13:59:35 -04:00
}
2015-09-10 18:55:51 -04:00
} else {
er = err
2016-01-05 16:13:24 -05:00
break
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
}
2016-01-05 16:13:24 -05:00
return node, nil
2015-07-05 13:59:35 -04:00
}
func toShallowNode(node *Node) *Node {
return &Node{
2016-07-05 21:32:15 -04:00
ID: node.ID,
Type: node.Type,
}
2015-07-10 11:25:24 -04:00
}
2016-01-05 16:13:24 -05:00
func visitModelNodeRelationships(relationName string, models reflect.Value, included *map[string]*Node, sideload bool) (*RelationshipManyNode, error) {
var nodes []*Node
2015-10-12 14:05:06 -04:00
if models.Len() == 0 {
nodes = make([]*Node, 0)
}
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()
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
}