forked from Mirrors/jsonapi
Compare commits
13 Commits
master
...
feature/em
Author | SHA1 | Date |
---|---|---|
Aren Patel | 7822e6f331 | |
Aren Patel | 0ec2f4e6b9 | |
Aren Patel | 8e4b2619cf | |
Aren Patel | cab68dab0e | |
Aren Patel | a92a3b75d9 | |
Aren Patel | d94776e6cc | |
Aren Patel | cf9619af15 | |
Aren Patel | 20c2b2286b | |
skimata | 662431e860 | |
Aren Patel | 5be05083d8 | |
Aren Patel | 003d45f589 | |
Aren Patel | 3e612cc977 | |
skimata | a6ac768a27 |
|
@ -10,6 +10,7 @@ const (
|
||||||
annotationOmitEmpty = "omitempty"
|
annotationOmitEmpty = "omitempty"
|
||||||
annotationISO8601 = "iso8601"
|
annotationISO8601 = "iso8601"
|
||||||
annotationSeperator = ","
|
annotationSeperator = ","
|
||||||
|
annotationIgnore = "-"
|
||||||
|
|
||||||
iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,88 @@
|
||||||
|
package jsonapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isJSONEqual(b1, b2 []byte) (bool, error) {
|
||||||
|
var i1, i2 interface{}
|
||||||
|
var result bool
|
||||||
|
var err error
|
||||||
|
if err = json.Unmarshal(b1, &i1); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(b2, &i2); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result = reflect.DeepEqual(i1, i2)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBlog() *Blog {
|
||||||
|
return &Blog{
|
||||||
|
ID: 5,
|
||||||
|
Title: "Title 1",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Posts: []*Post{
|
||||||
|
&Post{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Foo",
|
||||||
|
Body: "Bar",
|
||||||
|
Comments: []*Comment{
|
||||||
|
&Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
&Comment{
|
||||||
|
ID: 2,
|
||||||
|
Body: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LatestComment: &Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Post{
|
||||||
|
ID: 2,
|
||||||
|
Title: "Fuubar",
|
||||||
|
Body: "Bas",
|
||||||
|
Comments: []*Comment{
|
||||||
|
&Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
&Comment{
|
||||||
|
ID: 3,
|
||||||
|
Body: "bas",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LatestComment: &Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CurrentPost: &Post{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Foo",
|
||||||
|
Body: "Bar",
|
||||||
|
Comments: []*Comment{
|
||||||
|
&Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
&Comment{
|
||||||
|
ID: 2,
|
||||||
|
Body: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LatestComment: &Comment{
|
||||||
|
ID: 1,
|
||||||
|
Body: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -155,3 +155,22 @@ func (bc *BadComment) JSONAPILinks() *Links {
|
||||||
"self": []string{"invalid", "should error"},
|
"self": []string{"invalid", "should error"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Embedded Struct Models
|
||||||
|
type Engine struct {
|
||||||
|
NumberOfCylinders uint `jsonapi:"attr,cylinders"`
|
||||||
|
HorsePower uint `jsonapi:"attr,hp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockHeater struct {
|
||||||
|
Watts uint `jsonapi:"attr,watts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vehicle struct {
|
||||||
|
ID uint `json:"id" jsonapi:"primary,car"`
|
||||||
|
Make string `jsonapi:"attr,make"`
|
||||||
|
Model string `jsonapi:"attr,model"`
|
||||||
|
Year uint `jsonapi:"attr,year"`
|
||||||
|
Engine // every car must have an engine
|
||||||
|
*BlockHeater // not every car will have a block heater
|
||||||
|
}
|
||||||
|
|
64
node.go
64
node.go
|
@ -44,6 +44,38 @@ type Node struct {
|
||||||
Meta *Meta `json:"meta,omitempty"`
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Node) merge(node *Node) {
|
||||||
|
if node.Type != "" {
|
||||||
|
n.Type = node.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.ID != "" {
|
||||||
|
n.ID = node.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.ClientID != "" {
|
||||||
|
n.ClientID = node.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Attributes == nil && node.Attributes != nil {
|
||||||
|
n.Attributes = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
for k, v := range node.Attributes {
|
||||||
|
n.Attributes[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Relationships == nil && node.Relationships != nil {
|
||||||
|
n.Relationships = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
for k, v := range node.Relationships {
|
||||||
|
n.Relationships[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Links != nil {
|
||||||
|
n.Links = node.Links
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
||||||
type RelationshipOneNode struct {
|
type RelationshipOneNode struct {
|
||||||
Data *Node `json:"data"`
|
Data *Node `json:"data"`
|
||||||
|
@ -119,3 +151,35 @@ type RelationshipMetable interface {
|
||||||
// JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
// JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
|
||||||
JSONAPIRelationshipMeta(relation string) *Meta
|
JSONAPIRelationshipMeta(relation string) *Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derefs the arg, and clones the map-type attributes
|
||||||
|
// note: maps are reference types, so they need an explicit copy.
|
||||||
|
func deepCopyNode(n *Node) *Node {
|
||||||
|
if n == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
copyMap := func(m map[string]interface{}) map[string]interface{} {
|
||||||
|
if m == nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
cp := make(map[string]interface{})
|
||||||
|
for k, v := range m {
|
||||||
|
cp[k] = v
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
copy := *n
|
||||||
|
copy.Attributes = copyMap(copy.Attributes)
|
||||||
|
copy.Relationships = copyMap(copy.Relationships)
|
||||||
|
if copy.Links != nil {
|
||||||
|
tmp := Links(copyMap(map[string]interface{}(*copy.Links)))
|
||||||
|
copy.Links = &tmp
|
||||||
|
}
|
||||||
|
if copy.Meta != nil {
|
||||||
|
tmp := Meta(copyMap(map[string]interface{}(*copy.Meta)))
|
||||||
|
copy.Meta = &tmp
|
||||||
|
}
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
383
request.go
383
request.go
|
@ -117,6 +117,11 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
||||||
return models, nil
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unmarshalNode handles embedded struct models from top to down.
|
||||||
|
// it loops through the struct fields, handles attributes/relations at that level first
|
||||||
|
// the handling the embedded structs are done last, so that you get the expected composition behavior
|
||||||
|
// data (*Node) attributes are cleared on each success.
|
||||||
|
// relations/sideloaded models use deeply copied Nodes (since those sideloaded models can be referenced in multiple relations)
|
||||||
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
|
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -127,50 +132,137 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
modelValue := model.Elem()
|
modelValue := model.Elem()
|
||||||
modelType := model.Type().Elem()
|
modelType := model.Type().Elem()
|
||||||
|
|
||||||
var er error
|
type embedded struct {
|
||||||
|
structField, model reflect.Value
|
||||||
|
}
|
||||||
|
embeddeds := []*embedded{}
|
||||||
|
|
||||||
for i := 0; i < modelValue.NumField(); i++ {
|
for i := 0; i < modelValue.NumField(); i++ {
|
||||||
fieldType := modelType.Field(i)
|
fieldType := modelType.Field(i)
|
||||||
tag := fieldType.Tag.Get("jsonapi")
|
fieldValue := modelValue.Field(i)
|
||||||
|
tag := fieldType.Tag.Get(annotationJSONAPI)
|
||||||
|
|
||||||
|
// handle explicit ignore annotation
|
||||||
|
if shouldIgnoreField(tag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles embedded structs
|
||||||
|
if isEmbeddedStruct(fieldType) {
|
||||||
|
embeddeds = append(embeddeds,
|
||||||
|
&embedded{
|
||||||
|
model: reflect.ValueOf(fieldValue.Addr().Interface()),
|
||||||
|
structField: fieldValue,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles pointers to embedded structs
|
||||||
|
if isEmbeddedStructPtr(fieldType) {
|
||||||
|
embeddeds = append(embeddeds,
|
||||||
|
&embedded{
|
||||||
|
model: reflect.ValueOf(fieldValue.Interface()),
|
||||||
|
structField: fieldValue,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle tagless; after handling embedded structs (which could be tagless)
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue := modelValue.Field(i)
|
args := strings.Split(tag, annotationSeperator)
|
||||||
|
// require atleast 1
|
||||||
args := strings.Split(tag, ",")
|
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
er = ErrBadJSONAPIStructTag
|
return ErrBadJSONAPIStructTag
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
annotation := args[0]
|
// args[0] == annotation
|
||||||
|
switch args[0] {
|
||||||
if (annotation == annotationClientID && len(args) != 1) ||
|
case annotationClientID:
|
||||||
(annotation != annotationClientID && len(args) < 2) {
|
if err := handleClientIDUnmarshal(data, args, fieldValue); err != nil {
|
||||||
er = ErrBadJSONAPIStructTag
|
return err
|
||||||
break
|
}
|
||||||
|
case annotationPrimary:
|
||||||
|
if err := handlePrimaryUnmarshal(data, args, fieldType, fieldValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case annotationAttribute:
|
||||||
|
if err := handleAttributeUnmarshal(data, args, fieldType, fieldValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case annotationRelation:
|
||||||
|
if err := handleRelationUnmarshal(data, args, fieldValue, included); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(unsuportedStructTagMsg, args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle embedded last
|
||||||
|
for _, em := range embeddeds {
|
||||||
|
// if nil, need to construct and rollback accordingly
|
||||||
|
if em.model.IsNil() {
|
||||||
|
copy := deepCopyNode(data)
|
||||||
|
tmp := reflect.New(em.model.Type().Elem())
|
||||||
|
if err := unmarshalNode(copy, tmp, included); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// had changes; assign value to struct field, replace orig node (data) w/ mutated copy
|
||||||
|
if !reflect.DeepEqual(copy, data) {
|
||||||
|
assign(em.structField, tmp)
|
||||||
|
data = copy
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handle non-nil scenarios
|
||||||
|
if err := unmarshalNode(data, em.model, included); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleClientIDUnmarshal(data *Node, args []string, fieldValue reflect.Value) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return ErrBadJSONAPIStructTag
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ClientID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set value and clear clientID to denote it's already been processed
|
||||||
|
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
||||||
|
data.ClientID = ""
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePrimaryUnmarshal(data *Node, args []string, fieldType reflect.StructField, fieldValue reflect.Value) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return ErrBadJSONAPIStructTag
|
||||||
}
|
}
|
||||||
|
|
||||||
if annotation == annotationPrimary {
|
|
||||||
if data.ID == "" {
|
if data.ID == "" {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the JSON API Type
|
// Check the JSON API Type
|
||||||
if data.Type != args[1] {
|
if data.Type != args[1] {
|
||||||
er = fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Trying to Unmarshal an object of type %#v, but %#v does not match",
|
"Trying to Unmarshal an object of type %#v, but %#v does not match",
|
||||||
data.Type,
|
data.Type,
|
||||||
args[1],
|
args[1],
|
||||||
)
|
)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID will have to be transmitted as astring per the JSON API spec
|
|
||||||
v := reflect.ValueOf(data.ID)
|
|
||||||
|
|
||||||
// Deal with PTRS
|
// Deal with PTRS
|
||||||
var kind reflect.Kind
|
var kind reflect.Kind
|
||||||
if fieldValue.Kind() == reflect.Ptr {
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
@ -179,24 +271,23 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
kind = fieldType.Type.Kind()
|
kind = fieldType.Type.Kind()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var idValue reflect.Value
|
||||||
|
|
||||||
// Handle String case
|
// Handle String case
|
||||||
if kind == reflect.String {
|
if kind == reflect.String {
|
||||||
assign(fieldValue, v)
|
// ID will have to be transmitted as a string per the JSON API spec
|
||||||
continue
|
idValue = reflect.ValueOf(data.ID)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// Value was not a string... only other supported type was a numeric,
|
// Value was not a string... only other supported type was a numeric,
|
||||||
// which would have been sent as a float value.
|
// which would have been sent as a float value.
|
||||||
floatValue, err := strconv.ParseFloat(data.ID, 64)
|
floatValue, err := strconv.ParseFloat(data.ID, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Could not convert the value in the "id" attr to a float
|
// Could not convert the value in the "id" attr to a float
|
||||||
er = ErrBadJSONAPIID
|
return ErrBadJSONAPIID
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the numeric float to one of the supported ID numeric types
|
// Convert the numeric float to one of the supported ID numeric types
|
||||||
// (int[8,16,32,64] or uint[8,16,32,64])
|
// (int[8,16,32,64] or uint[8,16,32,64])
|
||||||
var idValue reflect.Value
|
|
||||||
switch kind {
|
switch kind {
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
n := int(floatValue)
|
n := int(floatValue)
|
||||||
|
@ -231,21 +322,112 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
default:
|
default:
|
||||||
// We had a JSON float (numeric), but our field was not one of the
|
// We had a JSON float (numeric), but our field was not one of the
|
||||||
// allowed numeric types
|
// allowed numeric types
|
||||||
er = ErrBadJSONAPIID
|
return ErrBadJSONAPIID
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set value and clear ID to denote it's already been processed
|
||||||
assign(fieldValue, idValue)
|
assign(fieldValue, idValue)
|
||||||
} else if annotation == annotationClientID {
|
data.ID = ""
|
||||||
if data.ClientID == "" {
|
|
||||||
continue
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRelationUnmarshal(data *Node, args []string, fieldValue reflect.Value, included *map[string]*Node) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return ErrBadJSONAPIStructTag
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
if data.Relationships == nil || data.Relationships[args[1]] == nil {
|
||||||
} else if annotation == annotationAttribute {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// to-one relationships
|
||||||
|
handler := handleToOneRelationUnmarshal
|
||||||
|
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||||
|
if isSlice {
|
||||||
|
// to-many relationship
|
||||||
|
handler = handleToManyRelationUnmarshal
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := handler(data.Relationships[args[1]], fieldValue.Type(), included)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// set only if there is a val since val can be null (e.g. to disassociate the relationship)
|
||||||
|
if v != nil {
|
||||||
|
fieldValue.Set(*v)
|
||||||
|
}
|
||||||
|
delete(data.Relationships, args[1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// to-one relationships
|
||||||
|
func handleToOneRelationUnmarshal(relationData interface{}, fieldType reflect.Type, included *map[string]*Node) (*reflect.Value, error) {
|
||||||
|
relationship := new(RelationshipOneNode)
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
json.NewEncoder(buf).Encode(relationData)
|
||||||
|
json.NewDecoder(buf).Decode(relationship)
|
||||||
|
|
||||||
|
m := reflect.New(fieldType.Elem())
|
||||||
|
/*
|
||||||
|
http://jsonapi.org/format/#document-resource-object-relationships
|
||||||
|
http://jsonapi.org/format/#document-resource-object-linkage
|
||||||
|
relationship can have a data node set to null (e.g. to disassociate the relationship)
|
||||||
|
so unmarshal and set fieldValue only if data obj is not null
|
||||||
|
*/
|
||||||
|
if relationship.Data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unmarshalNode(
|
||||||
|
fullNode(relationship.Data, included),
|
||||||
|
m,
|
||||||
|
included,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// to-many relationship
|
||||||
|
func handleToManyRelationUnmarshal(relationData interface{}, fieldType reflect.Type, included *map[string]*Node) (*reflect.Value, error) {
|
||||||
|
relationship := new(RelationshipManyNode)
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
json.NewEncoder(buf).Encode(relationData)
|
||||||
|
json.NewDecoder(buf).Decode(relationship)
|
||||||
|
|
||||||
|
models := reflect.New(fieldType).Elem()
|
||||||
|
|
||||||
|
rData := relationship.Data
|
||||||
|
for _, n := range rData {
|
||||||
|
m := reflect.New(fieldType.Elem().Elem())
|
||||||
|
|
||||||
|
if err := unmarshalNode(
|
||||||
|
fullNode(n, included),
|
||||||
|
m,
|
||||||
|
included,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
models = reflect.Append(models, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: break this out into smaller funcs
|
||||||
|
func handleAttributeUnmarshal(data *Node, args []string, fieldType reflect.StructField, fieldValue reflect.Value) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return ErrBadJSONAPIStructTag
|
||||||
|
}
|
||||||
attributes := data.Attributes
|
attributes := data.Attributes
|
||||||
if attributes == nil || len(data.Attributes) == 0 {
|
if attributes == nil || len(data.Attributes) == 0 {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var iso8601 bool
|
var iso8601 bool
|
||||||
|
@ -262,7 +444,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
|
|
||||||
// continue if the attribute was not included in the request
|
// continue if the attribute was not included in the request
|
||||||
if val == nil {
|
if val == nil {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(val)
|
v := reflect.ValueOf(val)
|
||||||
|
@ -274,19 +456,17 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
if v.Kind() == reflect.String {
|
if v.Kind() == reflect.String {
|
||||||
tm = v.Interface().(string)
|
tm = v.Interface().(string)
|
||||||
} else {
|
} else {
|
||||||
er = ErrInvalidISO8601
|
return ErrInvalidISO8601
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := time.Parse(iso8601TimeFormat, tm)
|
t, err := time.Parse(iso8601TimeFormat, tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
er = ErrInvalidISO8601
|
return ErrInvalidISO8601
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var at int64
|
var at int64
|
||||||
|
@ -302,8 +482,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
t := time.Unix(at, 0)
|
t := time.Unix(at, 0)
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
||||||
|
@ -313,8 +493,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(values))
|
fieldValue.Set(reflect.ValueOf(values))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||||
|
@ -323,21 +503,20 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
if v.Kind() == reflect.String {
|
if v.Kind() == reflect.String {
|
||||||
tm = v.Interface().(string)
|
tm = v.Interface().(string)
|
||||||
} else {
|
} else {
|
||||||
er = ErrInvalidISO8601
|
return ErrInvalidISO8601
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := time.Parse(iso8601TimeFormat, tm)
|
v, err := time.Parse(iso8601TimeFormat, tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
er = ErrInvalidISO8601
|
return ErrInvalidISO8601
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &v
|
t := &v
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var at int64
|
var at int64
|
||||||
|
@ -354,8 +533,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
t := &v
|
t := &v
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON value was a float (numeric)
|
// JSON value was a float (numeric)
|
||||||
|
@ -415,7 +594,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(fieldValue, numericValue)
|
assign(fieldValue, numericValue)
|
||||||
continue
|
delete(data.Attributes, args[1])
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field was a Pointer type
|
// Field was a Pointer type
|
||||||
|
@ -442,101 +622,30 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue.Set(concreteVal)
|
fieldValue.Set(concreteVal)
|
||||||
continue
|
delete(data.Attributes, args[1])
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
||||||
if fieldValue.Kind() != v.Kind() {
|
// Ignore interfaces since interfaces are poly
|
||||||
|
if fieldValue.Kind() != reflect.Interface && fieldValue.Kind() != v.Kind() {
|
||||||
return ErrInvalidType
|
return ErrInvalidType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set val and clear attribute key so its not processed again
|
||||||
fieldValue.Set(reflect.ValueOf(val))
|
fieldValue.Set(reflect.ValueOf(val))
|
||||||
|
delete(data.Attributes, args[1])
|
||||||
} else if annotation == annotationRelation {
|
return nil
|
||||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
|
||||||
|
|
||||||
if data.Relationships == nil || data.Relationships[args[1]] == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSlice {
|
|
||||||
// to-many relationship
|
|
||||||
relationship := new(RelationshipManyNode)
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
json.NewEncoder(buf).Encode(data.Relationships[args[1]])
|
|
||||||
json.NewDecoder(buf).Decode(relationship)
|
|
||||||
|
|
||||||
data := relationship.Data
|
|
||||||
models := reflect.New(fieldValue.Type()).Elem()
|
|
||||||
|
|
||||||
for _, n := range data {
|
|
||||||
m := reflect.New(fieldValue.Type().Elem().Elem())
|
|
||||||
|
|
||||||
if err := unmarshalNode(
|
|
||||||
fullNode(n, included),
|
|
||||||
m,
|
|
||||||
included,
|
|
||||||
); err != nil {
|
|
||||||
er = err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
models = reflect.Append(models, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue.Set(models)
|
|
||||||
} else {
|
|
||||||
// to-one relationships
|
|
||||||
relationship := new(RelationshipOneNode)
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
json.NewEncoder(buf).Encode(
|
|
||||||
data.Relationships[args[1]],
|
|
||||||
)
|
|
||||||
json.NewDecoder(buf).Decode(relationship)
|
|
||||||
|
|
||||||
/*
|
|
||||||
http://jsonapi.org/format/#document-resource-object-relationships
|
|
||||||
http://jsonapi.org/format/#document-resource-object-linkage
|
|
||||||
relationship can have a data node set to null (e.g. to disassociate the relationship)
|
|
||||||
so unmarshal and set fieldValue only if data obj is not null
|
|
||||||
*/
|
|
||||||
if relationship.Data == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
m := reflect.New(fieldValue.Type().Elem())
|
|
||||||
if err := unmarshalNode(
|
|
||||||
fullNode(relationship.Data, included),
|
|
||||||
m,
|
|
||||||
included,
|
|
||||||
); err != nil {
|
|
||||||
er = err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue.Set(m)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return er
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fullNode(n *Node, included *map[string]*Node) *Node {
|
func fullNode(n *Node, included *map[string]*Node) *Node {
|
||||||
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)
|
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)
|
||||||
|
|
||||||
if included != nil && (*included)[includedKey] != nil {
|
if included != nil && (*included)[includedKey] != nil {
|
||||||
return (*included)[includedKey]
|
return deepCopyNode((*included)[includedKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
return n
|
return deepCopyNode(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// assign will take the value specified and assign it to the field; if
|
// assign will take the value specified and assign it to the field; if
|
||||||
|
|
|
@ -703,6 +703,97 @@ func TestManyPayload_withLinks(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEmbeddedStructs_nonNilStructPtr(t *testing.T) {
|
||||||
|
originalVehicle := &Vehicle{
|
||||||
|
Make: "VW",
|
||||||
|
Model: "R32",
|
||||||
|
Year: 2008,
|
||||||
|
Engine: Engine{
|
||||||
|
NumberOfCylinders: 6,
|
||||||
|
HorsePower: 250,
|
||||||
|
},
|
||||||
|
BlockHeater: &BlockHeater{
|
||||||
|
Watts: 150,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize as JSON
|
||||||
|
jsonVehicle, err := json.Marshal(originalVehicle)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonUnmarshalledVehicle := &Vehicle{}
|
||||||
|
json.Unmarshal(jsonVehicle, jsonUnmarshalledVehicle)
|
||||||
|
|
||||||
|
// Proves that the JSON standard lib will allocate a BlockHeater
|
||||||
|
if jsonUnmarshalledVehicle.BlockHeater == nil {
|
||||||
|
t.Fatal("was expecting a non nil Block Heater ptr")
|
||||||
|
}
|
||||||
|
if e, a := originalVehicle.BlockHeater.Watts, jsonUnmarshalledVehicle.BlockHeater.Watts; e != a {
|
||||||
|
t.Fatalf("was expecting watts to be %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize as JSONAPI
|
||||||
|
jsonAPIVehicle := new(bytes.Buffer)
|
||||||
|
if err = MarshalPayload(jsonAPIVehicle, originalVehicle); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonAPIUnmarshalledVehicle := &Vehicle{}
|
||||||
|
if err = UnmarshalPayload(jsonAPIVehicle, jsonAPIUnmarshalledVehicle); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonAPIUnmarshalledVehicle.BlockHeater == nil {
|
||||||
|
t.Fatal("was expecting a non nil Block Heater ptr")
|
||||||
|
}
|
||||||
|
if e, a := originalVehicle.BlockHeater.Watts, jsonAPIUnmarshalledVehicle.BlockHeater.Watts; e != a {
|
||||||
|
t.Fatalf("was expecting watts to be %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbeddedStructs_nilStructPtr(t *testing.T) {
|
||||||
|
originalVehicle := &Vehicle{
|
||||||
|
Make: "VW",
|
||||||
|
Model: "R32",
|
||||||
|
Year: 2008,
|
||||||
|
Engine: Engine{
|
||||||
|
NumberOfCylinders: 6,
|
||||||
|
HorsePower: 250,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize as JSON
|
||||||
|
jsonVehicle, err := json.Marshal(originalVehicle)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonUnmarshalledVehicle := &Vehicle{}
|
||||||
|
json.Unmarshal(jsonVehicle, jsonUnmarshalledVehicle)
|
||||||
|
|
||||||
|
// Proves that the JSON standard lib will NOT allocate a BlockHeater
|
||||||
|
if e, a := originalVehicle.BlockHeater, jsonUnmarshalledVehicle.BlockHeater; e != a {
|
||||||
|
t.Fatalf("was expecting BlockHeater to be %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize as JSONAPI
|
||||||
|
jsonAPIVehicle := new(bytes.Buffer)
|
||||||
|
if err = MarshalPayload(jsonAPIVehicle, originalVehicle); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonAPIUnmarshalledVehicle := &Vehicle{}
|
||||||
|
if err = UnmarshalPayload(jsonAPIVehicle, jsonAPIUnmarshalledVehicle); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := originalVehicle.BlockHeater, jsonAPIUnmarshalledVehicle.BlockHeater; e != a {
|
||||||
|
t.Fatalf("was expecting BlockHeater to be %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func samplePayloadWithoutIncluded() map[string]interface{} {
|
func samplePayloadWithoutIncluded() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
|
|
66
response.go
66
response.go
|
@ -202,8 +202,10 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func visitModelNode(model interface{}, included *map[string]*Node,
|
// visitModelNode converts models to jsonapi payloads
|
||||||
sideload bool) (*Node, error) {
|
// it handles the deepest models first. (i.e.) embedded models
|
||||||
|
// this is so that upper-level attributes can overwrite lower-level attributes
|
||||||
|
func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) {
|
||||||
node := new(Node)
|
node := new(Node)
|
||||||
|
|
||||||
var er error
|
var er error
|
||||||
|
@ -211,16 +213,58 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
modelValue := reflect.ValueOf(model).Elem()
|
modelValue := reflect.ValueOf(model).Elem()
|
||||||
modelType := reflect.ValueOf(model).Type().Elem()
|
modelType := reflect.ValueOf(model).Type().Elem()
|
||||||
|
|
||||||
|
// handle just the embedded models first
|
||||||
for i := 0; i < modelValue.NumField(); i++ {
|
for i := 0; i < modelValue.NumField(); i++ {
|
||||||
structField := modelValue.Type().Field(i)
|
fieldValue := modelValue.Field(i)
|
||||||
tag := structField.Tag.Get(annotationJSONAPI)
|
fieldType := modelType.Field(i)
|
||||||
if tag == "" {
|
|
||||||
|
// skip if annotated w/ ignore
|
||||||
|
tag := fieldType.Tag.Get(annotationJSONAPI)
|
||||||
|
if shouldIgnoreField(tag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handles embedded structs and pointers to embedded structs
|
||||||
|
if isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) {
|
||||||
|
var embModel interface{}
|
||||||
|
if fieldType.Type.Kind() == reflect.Ptr {
|
||||||
|
if fieldValue.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
embModel = fieldValue.Interface()
|
||||||
|
} else {
|
||||||
|
embModel = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
embNode, err := visitModelNode(embModel, included, sideload)
|
||||||
|
if err != nil {
|
||||||
|
er = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
node.merge(embNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle everthing else
|
||||||
|
for i := 0; i < modelValue.NumField(); i++ {
|
||||||
fieldValue := modelValue.Field(i)
|
fieldValue := modelValue.Field(i)
|
||||||
fieldType := modelType.Field(i)
|
fieldType := modelType.Field(i)
|
||||||
|
|
||||||
|
tag := fieldType.Tag.Get(annotationJSONAPI)
|
||||||
|
|
||||||
|
if shouldIgnoreField(tag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip embedded because it was handled in a previous loop
|
||||||
|
if isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
args := strings.Split(tag, annotationSeperator)
|
args := strings.Split(tag, annotationSeperator)
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
|
@ -533,3 +577,15 @@ func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isEmbeddedStruct(sField reflect.StructField) bool {
|
||||||
|
return sField.Anonymous && sField.Type.Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmbeddedStructPtr(sField reflect.StructField) bool {
|
||||||
|
return sField.Anonymous && sField.Type.Kind() == reflect.Ptr && sField.Type.Elem().Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldIgnoreField(japiTag string) bool {
|
||||||
|
return strings.HasPrefix(japiTag, annotationIgnore)
|
||||||
|
}
|
||||||
|
|
|
@ -816,70 +816,3 @@ func TestMarshal_InvalidIntefaceArgument(t *testing.T) {
|
||||||
t.Fatal("Was expecting an error")
|
t.Fatal("Was expecting an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBlog() *Blog {
|
|
||||||
return &Blog{
|
|
||||||
ID: 5,
|
|
||||||
Title: "Title 1",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
Posts: []*Post{
|
|
||||||
&Post{
|
|
||||||
ID: 1,
|
|
||||||
Title: "Foo",
|
|
||||||
Body: "Bar",
|
|
||||||
Comments: []*Comment{
|
|
||||||
&Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
&Comment{
|
|
||||||
ID: 2,
|
|
||||||
Body: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LatestComment: &Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&Post{
|
|
||||||
ID: 2,
|
|
||||||
Title: "Fuubar",
|
|
||||||
Body: "Bas",
|
|
||||||
Comments: []*Comment{
|
|
||||||
&Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
&Comment{
|
|
||||||
ID: 3,
|
|
||||||
Body: "bas",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LatestComment: &Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurrentPost: &Post{
|
|
||||||
ID: 1,
|
|
||||||
Title: "Foo",
|
|
||||||
Body: "Bar",
|
|
||||||
Comments: []*Comment{
|
|
||||||
&Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
&Comment{
|
|
||||||
ID: 2,
|
|
||||||
Body: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LatestComment: &Comment{
|
|
||||||
ID: 1,
|
|
||||||
Body: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue