jsonapi/response.go

327 lines
8.3 KiB
Go

package jsonapi
import (
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"time"
)
type BadJSONAPIStructTag struct {
fieldTypeName string
}
func (e BadJSONAPIStructTag) Error() string {
return fmt.Sprintf("jsonapi tag, on %s, had too few arguments", e.fieldTypeName)
}
// 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.
//
// See UnmarshalPayload for usage example.
//
// model interface{} should be a pointer to a struct.
func MarshalOnePayload(w io.Writer, model interface{}) error {
payload, err := MarshalOne(model)
if err != nil {
return err
}
if err := json.NewEncoder(w).Encode(payload); 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) {
rootNode, included, err := visitModelNode(model, true)
if err != nil {
return nil, err
}
payload := &OnePayload{Data: rootNode}
payload.Included = uniqueByTypeAndID(included)
return payload, nil
}
// 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
//
// 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.
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) {
modelsValues := reflect.ValueOf(models)
data := make([]*Node, 0, modelsValues.Len())
var incl []*Node
for i := 0; i < modelsValues.Len(); i++ {
model := modelsValues.Index(i).Interface()
node, included, err := visitModelNode(model, true)
if err != nil {
return nil, err
}
data = append(data, node)
incl = append(incl, included...)
}
payload := &ManyPayload{
Data: data,
Included: uniqueByTypeAndID(incl),
}
return payload, nil
}
// 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.
//
// 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.
//
// model interface{} should be a pointer to a struct.
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
rootNode, _, err := visitModelNode(model, false)
if err != nil {
return err
}
payload := &OnePayload{Data: rootNode}
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
func visitModelNode(model interface{}, sideload bool) (*Node, []*Node, error) {
node := new(Node)
var er error
var included []*Node
modelType := reflect.TypeOf(model).Elem()
modelValue := reflect.ValueOf(model).Elem()
var i = 0
modelType.FieldByNameFunc(func(name string) bool {
if er != nil {
return false
}
structField := modelType.Field(i)
tag := structField.Tag.Get("jsonapi")
if tag == "" {
i++
return false
}
fieldValue := modelValue.Field(i)
i++
args := strings.Split(tag, ",")
if len(args) < 1 {
er = BadJSONAPIStructTag{structField.Name}
return false
}
annotation := args[0]
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) != 2) {
er = BadJSONAPIStructTag{structField.Name}
return false
}
if annotation == "primary" {
node.Id = fmt.Sprintf("%v", fieldValue.Interface())
node.Type = args[1]
} else if annotation == "client-id" {
node.ClientId = fieldValue.String()
} else if annotation == "attr" {
if node.Attributes == nil {
node.Attributes = make(map[string]interface{})
}
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
}
unix := fieldValue.MethodByName("Unix")
val := unix.Call(make([]reflect.Value, 0))[0]
node.Attributes[args[1]] = val.Int()
} else {
node.Attributes[args[1]] = fieldValue.Interface()
}
} 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{})
}
if sideload && included == nil {
included = make([]*Node, 0)
}
if isSlice {
relationship, incl, err := visitModelNodeRelationships(args[1], fieldValue, sideload)
if err == nil {
d := relationship.Data
if sideload {
included = append(included, incl...)
var shallowNodes []*Node
for _, node := range d {
shallowNodes = append(shallowNodes, toShallowNode(node))
}
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
} else {
node.Relationships[args[1]] = relationship
}
} else {
er = err
return false
}
} else {
relationship, incl, err := visitModelNode(fieldValue.Interface(), sideload)
if err == nil {
if sideload {
included = append(included, incl...)
included = append(included, relationship)
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
} else {
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
}
} else {
er = err
return false
}
}
} else {
er = fmt.Errorf("Unsupported jsonapi tag annotation, %s", annotation)
return false
}
return false
})
if er != nil {
return nil, nil, er
}
return node, included, nil
}
func toShallowNode(node *Node) *Node {
return &Node{
Id: node.Id,
Type: node.Type,
}
}
func visitModelNodeRelationships(relationName string, models reflect.Value, sideload bool) (*RelationshipManyNode, []*Node, error) {
var nodes []*Node
var included []*Node
if sideload {
included = make([]*Node, 0)
}
if models.Len() == 0 {
nodes = make([]*Node, 0)
}
for i := 0; i < models.Len(); i++ {
node, incl, err := visitModelNode(models.Index(i).Interface(), sideload)
if err != nil {
return nil, nil, err
}
nodes = append(nodes, node)
included = append(included, incl...)
}
included = append(included, nodes...)
n := &RelationshipManyNode{Data: nodes}
return n, included, nil
}
func uniqueByTypeAndID(nodes []*Node) []*Node {
uniqueIncluded := make(map[string]*Node)
for i := 0; i < len(nodes); i++ {
n := nodes[i]
k := fmt.Sprintf("%s,%s", n.Type, n.Id)
if uniqueIncluded[k] == nil {
uniqueIncluded[k] = n
} else {
nodes = append(nodes[:i], nodes[i+1:]...)
i--
}
}
return nodes
}