Compare commits

...

13 Commits

Author SHA1 Message Date
Aren Patel 7822e6f331
Merge branch 'master' into feature/embeded-structs 2017-09-05 11:11:42 -04:00
Aren Patel 0ec2f4e6b9
Fixed embedded spelling mistakes. 2017-08-01 18:03:13 -07:00
Aren Patel 8e4b2619cf
Added two additional tests (currently passing) that test the cases where an Outer struct had an attr field and an embedded field provided a field with the same attr name. 2017-08-01 18:01:13 -07:00
Aren Patel cab68dab0e
Misc cleanup. 2017-08-01 17:50:13 -07:00
Aren Patel a92a3b75d9
Fixing more warnings in tests. 2017-08-01 17:45:33 -07:00
Aren Patel d94776e6cc
Addressing warnings. 2017-08-01 17:44:50 -07:00
Aren Patel cf9619af15
Moved all embeded struct tests into its own test file; added 3 more failing tests that are related to the handling of duplicate embedded struct fields. 2017-08-01 17:42:38 -07:00
Aren Patel 20c2b2286b
Merge branch 'master' into feature/embeded-structs 2017-08-01 11:56:56 -07:00
skimata 662431e860 Feature/embeded structs fix tests (#105)
* fix TestEmbededStructs_nonNilStructPtr; bug on loop w/ (multiple) embedded structs; should just continue on success

* fix TestMarshal_duplicatePrimaryAnnotationFromEmbeddedStructs; fix order of processing of visitModelNode
2017-08-01 11:48:27 -07:00
Aren Patel 5be05083d8 Test against 1.9 2017-07-27 17:49:25 -07:00
Aren Patel 003d45f589 Added another failing test; this time for the marshalling of duplicate primary annotation fields. 2017-07-27 17:42:37 -07:00
Aren Patel 3e612cc977 Added a failing test for the allocation of struct pointers on serialization/deserilization. 2017-07-27 11:30:09 -07:00
skimata a6ac768a27 Embedded structs v3 (#100)
* working version

* fix text

* combine test files

* move private funcs to bottom

* ErrInvalidType should ignore interfaces

* replace MarshalOnePayload w/ MarshalPayload; fix bug w/ node merge()

* minor tweaks; address a couple comments

* decompose unmarshalNode() to smaller funcs; unmarshal should go from top-level to embedded

* deep copy the node when passing relation/sideloaded notes to unmarshal()

* add some comments and do some additional cleanup

* add test uses annotationIgnore

* implement support for struct fields that implement json.Marshaler/Unmarshaler

* add additional test that compares marshal/unmarshal behavior w/ standard json library

* add support for pointer embedded structs

* Revert "implement support for struct fields that implement json.Marshaler/Unmarshaler"

This reverts commit deeffb78df.
2017-07-27 11:16:00 -07:00
9 changed files with 1818 additions and 457 deletions

View File

@ -10,6 +10,7 @@ const (
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationSeperator = ","
annotationIgnore = "-"
iso8601TimeFormat = "2006-01-02T15:04:05Z"

1000
embeded_structs_test.go Normal file

File diff suppressed because it is too large Load Diff

88
helpers_test.go Normal file
View File

@ -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",
},
},
}
}

View File

@ -155,3 +155,22 @@ func (bc *BadComment) JSONAPILinks() *Links {
"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
View File

@ -44,6 +44,38 @@ type Node struct {
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
type RelationshipOneNode struct {
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`)
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 &copy
}

View File

@ -117,6 +117,11 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
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) {
defer func() {
if r := recover(); r != nil {
@ -127,416 +132,520 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
modelValue := model.Elem()
modelType := model.Type().Elem()
var er error
type embedded struct {
structField, model reflect.Value
}
embeddeds := []*embedded{}
for i := 0; i < modelValue.NumField(); 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 == "" {
continue
}
fieldValue := modelValue.Field(i)
args := strings.Split(tag, ",")
args := strings.Split(tag, annotationSeperator)
// require atleast 1
if len(args) < 1 {
er = ErrBadJSONAPIStructTag
break
return ErrBadJSONAPIStructTag
}
annotation := args[0]
if (annotation == annotationClientID && len(args) != 1) ||
(annotation != annotationClientID && len(args) < 2) {
er = ErrBadJSONAPIStructTag
break
}
if annotation == annotationPrimary {
if data.ID == "" {
continue
// args[0] == annotation
switch args[0] {
case annotationClientID:
if err := handleClientIDUnmarshal(data, args, fieldValue); err != nil {
return err
}
// Check the JSON API Type
if data.Type != args[1] {
er = fmt.Errorf(
"Trying to Unmarshal an object of type %#v, but %#v does not match",
data.Type,
args[1],
)
break
case annotationPrimary:
if err := handlePrimaryUnmarshal(data, args, fieldType, fieldValue); err != nil {
return err
}
// ID will have to be transmitted as astring per the JSON API spec
v := reflect.ValueOf(data.ID)
// Deal with PTRS
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
} else {
kind = fieldType.Type.Kind()
case annotationAttribute:
if err := handleAttributeUnmarshal(data, args, fieldType, fieldValue); err != nil {
return err
}
// Handle String case
if kind == reflect.String {
assign(fieldValue, v)
continue
case annotationRelation:
if err := handleRelationUnmarshal(data, args, fieldValue, included); err != nil {
return err
}
// Value was not a string... only other supported type was a numeric,
// which would have been sent as a float value.
floatValue, err := strconv.ParseFloat(data.ID, 64)
if err != nil {
// Could not convert the value in the "id" attr to a float
er = ErrBadJSONAPIID
break
}
// Convert the numeric float to one of the supported ID numeric types
// (int[8,16,32,64] or uint[8,16,32,64])
var idValue reflect.Value
switch kind {
case reflect.Int:
n := int(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
idValue = reflect.ValueOf(&n)
default:
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
er = ErrBadJSONAPIID
break
}
assign(fieldValue, idValue)
} else if annotation == annotationClientID {
if data.ClientID == "" {
continue
}
fieldValue.Set(reflect.ValueOf(data.ClientID))
} else if annotation == annotationAttribute {
attributes := data.Attributes
if attributes == nil || len(data.Attributes) == 0 {
continue
}
var iso8601 bool
if len(args) > 2 {
for _, arg := range args[2:] {
if arg == annotationISO8601 {
iso8601 = true
}
}
}
val := attributes[args[1]]
// continue if the attribute was not included in the request
if val == nil {
continue
}
v := reflect.ValueOf(val)
// Handle field of type time.Time
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
if iso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
er = ErrInvalidISO8601
break
}
t, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
er = ErrInvalidISO8601
break
}
fieldValue.Set(reflect.ValueOf(t))
continue
}
var at int64
if v.Kind() == reflect.Float64 {
at = int64(v.Interface().(float64))
} else if v.Kind() == reflect.Int {
at = v.Int()
} else {
return ErrInvalidTime
}
t := time.Unix(at, 0)
fieldValue.Set(reflect.ValueOf(t))
continue
}
if fieldValue.Type() == reflect.TypeOf([]string{}) {
values := make([]string, v.Len())
for i := 0; i < v.Len(); i++ {
values[i] = v.Index(i).Interface().(string)
}
fieldValue.Set(reflect.ValueOf(values))
continue
}
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
if iso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
er = ErrInvalidISO8601
break
}
v, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
er = ErrInvalidISO8601
break
}
t := &v
fieldValue.Set(reflect.ValueOf(t))
continue
}
var at int64
if v.Kind() == reflect.Float64 {
at = int64(v.Interface().(float64))
} else if v.Kind() == reflect.Int {
at = v.Int()
} else {
return ErrInvalidTime
}
v := time.Unix(at, 0)
t := &v
fieldValue.Set(reflect.ValueOf(t))
continue
}
// JSON value was a float (numeric)
if v.Kind() == reflect.Float64 {
floatValue := v.Interface().(float64)
// The field may or may not be a pointer to a numeric; the kind var
// will not contain a pointer type
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
} else {
kind = fieldType.Type.Kind()
}
var numericValue reflect.Value
switch kind {
case reflect.Int:
n := int(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float32:
n := float32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float64:
n := floatValue
numericValue = reflect.ValueOf(&n)
default:
return ErrUnknownFieldNumberType
}
assign(fieldValue, numericValue)
continue
}
// Field was a Pointer type
if fieldValue.Kind() == reflect.Ptr {
var concreteVal reflect.Value
switch cVal := val.(type) {
case string:
concreteVal = reflect.ValueOf(&cVal)
case bool:
concreteVal = reflect.ValueOf(&cVal)
case complex64:
concreteVal = reflect.ValueOf(&cVal)
case complex128:
concreteVal = reflect.ValueOf(&cVal)
case uintptr:
concreteVal = reflect.ValueOf(&cVal)
default:
return ErrUnsupportedPtrType
}
if fieldValue.Type() != concreteVal.Type() {
return ErrUnsupportedPtrType
}
fieldValue.Set(concreteVal)
continue
}
// As a final catch-all, ensure types line up to avoid a runtime panic.
if fieldValue.Kind() != v.Kind() {
return ErrInvalidType
}
fieldValue.Set(reflect.ValueOf(val))
} else if annotation == annotationRelation {
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)
default:
return fmt.Errorf(unsuportedStructTagMsg, args[0])
}
}
return er
// 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 data.ID == "" {
return nil
}
// Check the JSON API Type
if data.Type != args[1] {
return fmt.Errorf(
"Trying to Unmarshal an object of type %#v, but %#v does not match",
data.Type,
args[1],
)
}
// Deal with PTRS
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
} else {
kind = fieldType.Type.Kind()
}
var idValue reflect.Value
// Handle String case
if kind == reflect.String {
// ID will have to be transmitted as a string per the JSON API spec
idValue = reflect.ValueOf(data.ID)
} else {
// Value was not a string... only other supported type was a numeric,
// which would have been sent as a float value.
floatValue, err := strconv.ParseFloat(data.ID, 64)
if err != nil {
// Could not convert the value in the "id" attr to a float
return ErrBadJSONAPIID
}
// Convert the numeric float to one of the supported ID numeric types
// (int[8,16,32,64] or uint[8,16,32,64])
switch kind {
case reflect.Int:
n := int(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
idValue = reflect.ValueOf(&n)
default:
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
return ErrBadJSONAPIID
}
}
// set value and clear ID to denote it's already been processed
assign(fieldValue, idValue)
data.ID = ""
return nil
}
func handleRelationUnmarshal(data *Node, args []string, fieldValue reflect.Value, included *map[string]*Node) error {
if len(args) < 2 {
return ErrBadJSONAPIStructTag
}
if data.Relationships == nil || data.Relationships[args[1]] == nil {
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
if attributes == nil || len(data.Attributes) == 0 {
return nil
}
var iso8601 bool
if len(args) > 2 {
for _, arg := range args[2:] {
if arg == annotationISO8601 {
iso8601 = true
}
}
}
val := attributes[args[1]]
// continue if the attribute was not included in the request
if val == nil {
return nil
}
v := reflect.ValueOf(val)
// Handle field of type time.Time
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
if iso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
return ErrInvalidISO8601
}
t, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
return ErrInvalidISO8601
}
fieldValue.Set(reflect.ValueOf(t))
delete(data.Attributes, args[1])
return nil
}
var at int64
if v.Kind() == reflect.Float64 {
at = int64(v.Interface().(float64))
} else if v.Kind() == reflect.Int {
at = v.Int()
} else {
return ErrInvalidTime
}
t := time.Unix(at, 0)
fieldValue.Set(reflect.ValueOf(t))
delete(data.Attributes, args[1])
return nil
}
if fieldValue.Type() == reflect.TypeOf([]string{}) {
values := make([]string, v.Len())
for i := 0; i < v.Len(); i++ {
values[i] = v.Index(i).Interface().(string)
}
fieldValue.Set(reflect.ValueOf(values))
delete(data.Attributes, args[1])
return nil
}
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
if iso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
return ErrInvalidISO8601
}
v, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
return ErrInvalidISO8601
}
t := &v
fieldValue.Set(reflect.ValueOf(t))
delete(data.Attributes, args[1])
return nil
}
var at int64
if v.Kind() == reflect.Float64 {
at = int64(v.Interface().(float64))
} else if v.Kind() == reflect.Int {
at = v.Int()
} else {
return ErrInvalidTime
}
v := time.Unix(at, 0)
t := &v
fieldValue.Set(reflect.ValueOf(t))
delete(data.Attributes, args[1])
return nil
}
// JSON value was a float (numeric)
if v.Kind() == reflect.Float64 {
floatValue := v.Interface().(float64)
// The field may or may not be a pointer to a numeric; the kind var
// will not contain a pointer type
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
} else {
kind = fieldType.Type.Kind()
}
var numericValue reflect.Value
switch kind {
case reflect.Int:
n := int(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float32:
n := float32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float64:
n := floatValue
numericValue = reflect.ValueOf(&n)
default:
return ErrUnknownFieldNumberType
}
assign(fieldValue, numericValue)
delete(data.Attributes, args[1])
return nil
}
// Field was a Pointer type
if fieldValue.Kind() == reflect.Ptr {
var concreteVal reflect.Value
switch cVal := val.(type) {
case string:
concreteVal = reflect.ValueOf(&cVal)
case bool:
concreteVal = reflect.ValueOf(&cVal)
case complex64:
concreteVal = reflect.ValueOf(&cVal)
case complex128:
concreteVal = reflect.ValueOf(&cVal)
case uintptr:
concreteVal = reflect.ValueOf(&cVal)
default:
return ErrUnsupportedPtrType
}
if fieldValue.Type() != concreteVal.Type() {
return ErrUnsupportedPtrType
}
fieldValue.Set(concreteVal)
delete(data.Attributes, args[1])
return nil
}
// As a final catch-all, ensure types line up to avoid a runtime panic.
// Ignore interfaces since interfaces are poly
if fieldValue.Kind() != reflect.Interface && fieldValue.Kind() != v.Kind() {
return ErrInvalidType
}
// set val and clear attribute key so its not processed again
fieldValue.Set(reflect.ValueOf(val))
delete(data.Attributes, args[1])
return nil
}
func fullNode(n *Node, included *map[string]*Node) *Node {
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)
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

View File

@ -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{} {
return map[string]interface{}{
"data": map[string]interface{}{

View File

@ -202,8 +202,10 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
return nil
}
func visitModelNode(model interface{}, included *map[string]*Node,
sideload bool) (*Node, error) {
// visitModelNode converts models to jsonapi payloads
// 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)
var er error
@ -211,16 +213,58 @@ func visitModelNode(model interface{}, included *map[string]*Node,
modelValue := reflect.ValueOf(model).Elem()
modelType := reflect.ValueOf(model).Type().Elem()
// handle just the embedded models first
for i := 0; i < modelValue.NumField(); i++ {
structField := modelValue.Type().Field(i)
tag := structField.Tag.Get(annotationJSONAPI)
if tag == "" {
fieldValue := modelValue.Field(i)
fieldType := modelType.Field(i)
// skip if annotated w/ ignore
tag := fieldType.Tag.Get(annotationJSONAPI)
if shouldIgnoreField(tag) {
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)
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)
if len(args) < 1 {
@ -533,3 +577,15 @@ func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
}
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)
}

View File

@ -816,70 +816,3 @@ func TestMarshal_InvalidIntefaceArgument(t *testing.T) {
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",
},
},
}
}