forked from Mirrors/jsonapi
optimizations, refactors
This commit is contained in:
parent
ae3568a7b2
commit
d4c1f40f1d
77
request.go
77
request.go
|
@ -12,6 +12,13 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
|
||||
var (
|
||||
ErrTypeMismatch = errors.New("Trying to Unmarshal a type that does not match")
|
||||
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
|
||||
)
|
||||
|
||||
// Convert an io into a struct instance using jsonapi tags on struct fields.
|
||||
// Method supports single request payloads only, at the moment. Bulk creates and updates
|
||||
// are not supported yet.
|
||||
|
@ -120,45 +127,37 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
var er error
|
||||
|
||||
var i = 0
|
||||
modelType.FieldByNameFunc(func(name string) bool {
|
||||
if er != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < modelValue.NumField(); i++ {
|
||||
fieldType := modelType.Field(i)
|
||||
tag := fieldType.Tag.Get("jsonapi")
|
||||
if tag == "" {
|
||||
i += 1
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := modelValue.Field(i)
|
||||
|
||||
i += 1
|
||||
|
||||
args := strings.Split(tag, ",")
|
||||
|
||||
if len(args) < 1 {
|
||||
er = BadJSONAPIStructTag{fieldType.Name}
|
||||
return false
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
annotation := args[0]
|
||||
|
||||
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) != 2) {
|
||||
er = BadJSONAPIStructTag{fieldType.Name}
|
||||
return false
|
||||
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) < 2) {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
if annotation == "primary" {
|
||||
if data.Id == "" {
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if data.Type != args[1] {
|
||||
er = errors.New("Trying to Unmarshal a type that does not match")
|
||||
return false
|
||||
er = ErrTypeMismatch
|
||||
break
|
||||
}
|
||||
|
||||
if fieldValue.Kind() == reflect.String {
|
||||
|
@ -167,30 +166,30 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
id, err := strconv.Atoi(data.Id)
|
||||
if err != nil {
|
||||
er = err
|
||||
return false
|
||||
break
|
||||
}
|
||||
fieldValue.SetInt(int64(id))
|
||||
} else {
|
||||
er = errors.New("Unsuppored data type for primary key, not int or string")
|
||||
return false
|
||||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
} else if annotation == "client-id" {
|
||||
if data.ClientId == "" {
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(data.ClientId))
|
||||
} else if annotation == "attr" {
|
||||
attributes := data.Attributes
|
||||
if attributes == nil {
|
||||
return false
|
||||
if attributes == nil || len(data.Attributes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
val := attributes[args[1]]
|
||||
|
||||
// next if the attribute was not included in the request
|
||||
// continue if the attribute was not included in the request
|
||||
if val == nil {
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(val)
|
||||
|
@ -203,15 +202,15 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
} else if v.Kind() == reflect.Int {
|
||||
at = v.Int()
|
||||
} else {
|
||||
er = errors.New("Only numbers can be parsed as dates, unix timestamps")
|
||||
return false
|
||||
er = ErrInvalidTime
|
||||
break
|
||||
}
|
||||
|
||||
t := time.Unix(at, 0)
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(t))
|
||||
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldValue.Type() == reflect.TypeOf([]string(nil)) {
|
||||
|
@ -222,7 +221,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
fieldValue.Set(reflect.ValueOf(values))
|
||||
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
|
@ -233,8 +232,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
} else if v.Kind() == reflect.Int {
|
||||
at = v.Int()
|
||||
} else {
|
||||
er = errors.New("Only numbers can be parsed as dates, unix timestamps")
|
||||
return false
|
||||
er = ErrInvalidTime
|
||||
break
|
||||
}
|
||||
|
||||
v := time.Unix(at, 0)
|
||||
|
@ -242,7 +241,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
fieldValue.Set(reflect.ValueOf(t))
|
||||
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldValue.Kind() == reflect.Int && v.Kind() == reflect.Float64 {
|
||||
|
@ -254,7 +253,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
|
||||
if data.Relationships == nil || data.Relationships[args[1]] == nil {
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if isSlice {
|
||||
|
@ -273,7 +272,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
if err := unmarshalNode(fullNode(n, included), m, included); err != nil {
|
||||
er = err
|
||||
return false
|
||||
break
|
||||
}
|
||||
|
||||
models = reflect.Append(models, m)
|
||||
|
@ -292,18 +291,16 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
if err := unmarshalNode(fullNode(relationship.Data, included), m, included); err != nil {
|
||||
er = err
|
||||
return false
|
||||
break
|
||||
}
|
||||
|
||||
fieldValue.Set(m)
|
||||
}
|
||||
|
||||
} else {
|
||||
er = errors.New(fmt.Sprintf("Unsupported jsonapi tag annotation, %s", annotation))
|
||||
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if er != nil {
|
||||
return er
|
||||
|
|
|
@ -3,9 +3,7 @@ package jsonapi
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -18,16 +16,9 @@ type BadModel struct {
|
|||
func TestMalformedTag(t *testing.T) {
|
||||
out := new(BadModel)
|
||||
err := UnmarshalPayload(samplePayload(), out)
|
||||
if err == nil {
|
||||
if err == nil || err != ErrBadJSONAPIStructTag {
|
||||
t.Fatalf("Did not error out with wrong number of arguments in tag")
|
||||
}
|
||||
|
||||
fmt.Println(err.Error())
|
||||
r := regexp.MustCompile(`too few arguments`)
|
||||
|
||||
if !r.Match([]byte(err.Error())) {
|
||||
t.Fatalf("The err was not due too few arguments in a tag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalInvalidJSON(t *testing.T) {
|
||||
|
|
227
response.go
227
response.go
|
@ -2,20 +2,19 @@ package jsonapi
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"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)
|
||||
}
|
||||
var (
|
||||
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
|
||||
ErrBadJSONAPIID = errors.New("id should be either string or int")
|
||||
)
|
||||
|
||||
// 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
|
||||
|
@ -41,13 +40,15 @@ func MarshalOnePayload(w io.Writer, model interface{}) error {
|
|||
// 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)
|
||||
included := make(map[string]*Node)
|
||||
|
||||
rootNode, err := visitModelNode(model, &included, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := &OnePayload{Data: rootNode}
|
||||
|
||||
payload.Included = uniqueByTypeAndID(included)
|
||||
payload.Included = nodeMapValues(&included)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
@ -92,25 +93,26 @@ func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
|||
// 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 data []*Node
|
||||
included := make(map[string]*Node)
|
||||
|
||||
var incl []*Node
|
||||
for i := 0; i < len(models); i++ {
|
||||
model := models[i]
|
||||
|
||||
for i := 0; i < modelsValues.Len(); i++ {
|
||||
model := modelsValues.Index(i).Interface()
|
||||
|
||||
node, included, err := visitModelNode(model, true)
|
||||
node, err := visitModelNode(model, &included, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, node)
|
||||
incl = append(incl, included...)
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
data = make([]*Node, 0)
|
||||
}
|
||||
|
||||
payload := &ManyPayload{
|
||||
Data: data,
|
||||
Included: uniqueByTypeAndID(incl),
|
||||
Included: nodeMapValues(&included),
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
|
@ -129,7 +131,7 @@ func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
|||
//
|
||||
// model interface{} should be a pointer to a struct.
|
||||
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
||||
rootNode, _, err := visitModelNode(model, false)
|
||||
rootNode, err := visitModelNode(model, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -143,110 +145,127 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func visitModelNode(model interface{}, sideload bool) (*Node, []*Node, error) {
|
||||
func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*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)
|
||||
for i := 0; i < modelValue.NumField(); i++ {
|
||||
structField := modelValue.Type().Field(i)
|
||||
tag := structField.Tag.Get("jsonapi")
|
||||
if tag == "" {
|
||||
i++
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := modelValue.Field(i)
|
||||
|
||||
i++
|
||||
|
||||
args := strings.Split(tag, ",")
|
||||
|
||||
if len(args) < 1 {
|
||||
er = BadJSONAPIStructTag{structField.Name}
|
||||
return false
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
annotation := args[0]
|
||||
|
||||
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) != 2) {
|
||||
er = BadJSONAPIStructTag{structField.Name}
|
||||
return false
|
||||
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) < 2) {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
if annotation == "primary" {
|
||||
node.Id = fmt.Sprintf("%v", fieldValue.Interface())
|
||||
id := fieldValue.Interface()
|
||||
|
||||
str, ok := id.(string)
|
||||
if ok {
|
||||
node.Id = str
|
||||
} else {
|
||||
j, ok := id.(int)
|
||||
if ok {
|
||||
node.Id = strconv.Itoa(j)
|
||||
} else {
|
||||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
node.Type = args[1]
|
||||
} else if annotation == "client-id" {
|
||||
node.ClientId = fieldValue.String()
|
||||
clientID := fieldValue.String()
|
||||
if clientID != "" {
|
||||
node.ClientId = clientID
|
||||
}
|
||||
} else if annotation == "attr" {
|
||||
var omitEmpty bool
|
||||
|
||||
if len(args) > 2 {
|
||||
omitEmpty = args[2] == "omitempty"
|
||||
}
|
||||
|
||||
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
|
||||
t := fieldValue.Interface().(time.Time)
|
||||
|
||||
if t.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
unix := fieldValue.MethodByName("Unix")
|
||||
val := unix.Call(make([]reflect.Value, 0))[0]
|
||||
node.Attributes[args[1]] = val.Int()
|
||||
node.Attributes[args[1]] = t.Unix()
|
||||
} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
// A time pointer may be nil
|
||||
t := reflect.ValueOf(fieldValue.Interface())
|
||||
if t.IsNil() {
|
||||
node.Attributes[args[1]] = nil
|
||||
} else {
|
||||
isZeroMethod := fieldValue.MethodByName("IsZero")
|
||||
isZero := isZeroMethod.Call(make([]reflect.Value, 0))[0].Interface().(bool)
|
||||
if isZero {
|
||||
return false
|
||||
if fieldValue.IsNil() {
|
||||
if omitEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
unix := fieldValue.MethodByName("Unix")
|
||||
val := unix.Call(make([]reflect.Value, 0))[0]
|
||||
node.Attributes[args[1]] = val.Int()
|
||||
node.Attributes[args[1]] = nil
|
||||
} else {
|
||||
tm := fieldValue.Interface().(*time.Time)
|
||||
|
||||
if tm.IsZero() && omitEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
node.Attributes[args[1]] = tm.Unix()
|
||||
}
|
||||
} else {
|
||||
node.Attributes[args[1]] = fieldValue.Interface()
|
||||
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()
|
||||
}
|
||||
}
|
||||
} else if annotation == "relation" {
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
|
||||
if (isSlice && fieldValue.Len() < 1) || (!isSlice && fieldValue.IsNil()) {
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
relationship, err := visitModelNodeRelationships(args[1], fieldValue, included, 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))
|
||||
|
||||
for _, n := range d {
|
||||
appendIncluded(included, n)
|
||||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||
}
|
||||
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
|
||||
|
@ -255,37 +274,34 @@ func visitModelNode(model interface{}, sideload bool) (*Node, []*Node, error) {
|
|||
}
|
||||
} else {
|
||||
er = err
|
||||
return false
|
||||
break
|
||||
}
|
||||
} else {
|
||||
relationship, incl, err := visitModelNode(fieldValue.Interface(), sideload)
|
||||
relationship, err := visitModelNode(fieldValue.Interface(), included, sideload)
|
||||
if err == nil {
|
||||
if sideload {
|
||||
included = append(included, incl...)
|
||||
included = append(included, relationship)
|
||||
appendIncluded(included, relationship)
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
|
||||
} else {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
|
||||
}
|
||||
} else {
|
||||
er = err
|
||||
return false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
er = fmt.Errorf("Unsupported jsonapi tag annotation, %s", annotation)
|
||||
return false
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if er != nil {
|
||||
return nil, nil, er
|
||||
}
|
||||
|
||||
return node, included, nil
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func toShallowNode(node *Node) *Node {
|
||||
|
@ -295,47 +311,48 @@ func toShallowNode(node *Node) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func visitModelNodeRelationships(relationName string, models reflect.Value, sideload bool) (*RelationshipManyNode, []*Node, error) {
|
||||
func visitModelNodeRelationships(relationName string, models reflect.Value, included *map[string]*Node, sideload bool) (*RelationshipManyNode, 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)
|
||||
n := models.Index(i).Interface()
|
||||
node, err := visitModelNode(n, included, sideload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
included = append(included, incl...)
|
||||
}
|
||||
|
||||
included = append(included, nodes...)
|
||||
|
||||
n := &RelationshipManyNode{Data: nodes}
|
||||
|
||||
return n, included, nil
|
||||
return &RelationshipManyNode{Data: nodes}, nil
|
||||
}
|
||||
|
||||
func uniqueByTypeAndID(nodes []*Node) []*Node {
|
||||
uniqueIncluded := make(map[string]*Node)
|
||||
func appendIncluded(m *map[string]*Node, nodes ...*Node) {
|
||||
included := *m
|
||||
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
n := nodes[i]
|
||||
for _, n := range nodes {
|
||||
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--
|
||||
|
||||
if _, hasNode := included[k]; hasNode {
|
||||
continue
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
|
||||
return nodes
|
||||
|
|
Loading…
Reference in New Issue