optimizations, refactors

This commit is contained in:
Sam Woodard 2016-01-05 13:13:24 -08:00
parent ae3568a7b2
commit d4c1f40f1d
3 changed files with 160 additions and 155 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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