Merge pull request #99 from Slemgrim/master

Unmarshalling of nested structs
This commit is contained in:
Sam Woodard 2018-10-04 12:04:27 -07:00 committed by GitHub
commit bdc73a22a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 601 additions and 237 deletions

View File

@ -155,3 +155,24 @@ func (bc *BadComment) JSONAPILinks() *Links {
"self": []string{"invalid", "should error"},
}
}
type Company struct {
ID string `jsonapi:"primary,companies"`
Name string `jsonapi:"attr,name"`
Boss Employee `jsonapi:"attr,boss"`
Teams []Team `jsonapi:"attr,teams"`
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"`
}
type Team struct {
Name string `jsonapi:"attr,name"`
Leader *Employee `jsonapi:"attr,leader"`
Members []Employee `jsonapi:"attr,members"`
}
type Employee struct {
Firstname string `jsonapi:"attr,firstname"`
Surname string `jsonapi:"attr,surname"`
Age int `jsonapi:"attr,age"`
HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"`
}

View File

@ -13,7 +13,7 @@ import (
)
const (
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
)
var (
@ -27,13 +27,35 @@ var (
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
// float, etc)
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
// the JSON value was of a different type
ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported")
// ErrInvalidType is returned when the given type is incompatible with the expected type.
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
)
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
// the JSON value was of a different type
type ErrUnsupportedPtrType struct {
rf reflect.Value
t reflect.Type
structField reflect.StructField
}
func (eupt ErrUnsupportedPtrType) Error() string {
typeName := eupt.t.Elem().Name()
kind := eupt.t.Elem().Kind()
if kind.String() != "" && kind.String() != typeName {
typeName = fmt.Sprintf("%s (%s)", typeName, kind.String())
}
return fmt.Sprintf(
"jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`",
eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName,
)
}
func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error {
return ErrUnsupportedPtrType{rf, t, structField}
}
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
// struct fields. This method supports single request payloads only, at the
// moment. Bulk creates and updates are not supported yet.
@ -125,7 +147,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}()
modelValue := model.Elem()
modelType := model.Type().Elem()
modelType := modelValue.Type()
var er error
@ -139,7 +161,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
fieldValue := modelValue.Field(i)
args := strings.Split(tag, ",")
if len(args) < 1 {
er = ErrBadJSONAPIStructTag
break
@ -196,39 +217,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
// 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:
idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue)
if err != nil {
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
er = ErrBadJSONAPIID
@ -244,212 +234,27 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
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]]
attribute := attributes[args[1]]
// continue if the attribute was not included in the request
if val == nil {
if attribute == 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
structField := fieldType
value, err := unmarshalAttribute(attribute, args, structField, fieldValue)
if err != nil {
er = err
break
}
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))
assign(fieldValue, value)
continue
} else if annotation == annotationRelation {
isSlice := fieldValue.Type().Kind() == reflect.Slice
@ -522,7 +327,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}
} else {
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
er = fmt.Errorf(unsupportedStructTagMsg, annotation)
}
}
@ -548,3 +353,261 @@ func assign(field, value reflect.Value) {
field.Set(reflect.Indirect(value))
}
}
func unmarshalAttribute(
attribute interface{},
args []string,
structField reflect.StructField,
fieldValue reflect.Value) (value reflect.Value, err error) {
value = reflect.ValueOf(attribute)
fieldType := structField.Type
// Handle field of type []string
if fieldValue.Type() == reflect.TypeOf([]string{}) {
value, err = handleStringSlice(attribute)
return
}
// Handle field of type time.Time
if fieldValue.Type() == reflect.TypeOf(time.Time{}) ||
fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
value, err = handleTime(attribute, args, fieldValue)
return
}
// Handle field of type struct
if fieldValue.Type().Kind() == reflect.Struct {
value, err = handleStruct(attribute, fieldValue)
return
}
// Handle field containing slice of structs
if fieldValue.Type().Kind() == reflect.Slice &&
reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct {
value, err = handleStructSlice(attribute, fieldValue)
return
}
// JSON value was a float (numeric)
if value.Kind() == reflect.Float64 {
value, err = handleNumeric(attribute, fieldType, fieldValue)
return
}
// Field was a Pointer type
if fieldValue.Kind() == reflect.Ptr {
value, err = handlePointer(attribute, args, fieldType, fieldValue, structField)
return
}
// As a final catch-all, ensure types line up to avoid a runtime panic.
if fieldValue.Kind() != value.Kind() {
err = ErrInvalidType
return
}
return
}
func handleStringSlice(attribute interface{}) (reflect.Value, error) {
v := reflect.ValueOf(attribute)
values := make([]string, v.Len())
for i := 0; i < v.Len(); i++ {
values[i] = v.Index(i).Interface().(string)
}
return reflect.ValueOf(values), nil
}
func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
var isIso8601 bool
v := reflect.ValueOf(attribute)
if len(args) > 2 {
for _, arg := range args[2:] {
if arg == annotationISO8601 {
isIso8601 = true
}
}
}
if isIso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
}
t, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
}
if fieldValue.Kind() == reflect.Ptr {
return reflect.ValueOf(&t), nil
}
return reflect.ValueOf(t), 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 reflect.ValueOf(time.Now()), ErrInvalidTime
}
t := time.Unix(at, 0)
return reflect.ValueOf(t), nil
}
func handleNumeric(
attribute interface{},
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
v := reflect.ValueOf(attribute)
floatValue := v.Interface().(float64)
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Elem().Kind()
} else {
kind = fieldType.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 reflect.Value{}, ErrUnknownFieldNumberType
}
return numericValue, nil
}
func handlePointer(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value,
structField reflect.StructField) (reflect.Value, error) {
t := fieldValue.Type()
var concreteVal reflect.Value
switch cVal := attribute.(type) {
case string:
concreteVal = reflect.ValueOf(&cVal)
case bool:
concreteVal = reflect.ValueOf(&cVal)
case complex64, complex128, uintptr:
concreteVal = reflect.ValueOf(&cVal)
case map[string]interface{}:
var err error
concreteVal, err = handleStruct(attribute, fieldValue)
if err != nil {
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
return concreteVal, err
default:
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
if t != concreteVal.Type() {
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
return concreteVal, nil
}
func handleStruct(
attribute interface{},
fieldValue reflect.Value) (reflect.Value, error) {
data, err := json.Marshal(attribute)
if err != nil {
return reflect.Value{}, err
}
node := new(Node)
if err := json.Unmarshal(data, &node.Attributes); err != nil {
return reflect.Value{}, err
}
var model reflect.Value
if fieldValue.Kind() == reflect.Ptr {
model = reflect.New(fieldValue.Type().Elem())
} else {
model = reflect.New(fieldValue.Type())
}
if err := unmarshalNode(node, model, nil); err != nil {
return reflect.Value{}, err
}
return model, nil
}
func handleStructSlice(
attribute interface{},
fieldValue reflect.Value) (reflect.Value, error) {
models := reflect.New(fieldValue.Type()).Elem()
dataMap := reflect.ValueOf(attribute).Interface().([]interface{})
for _, data := range dataMap {
model := reflect.New(fieldValue.Type().Elem()).Elem()
value, err := handleStruct(data, model)
if err != nil {
continue
}
models = reflect.Append(models, reflect.Indirect(value))
}
return models, nil
}

View File

@ -121,12 +121,12 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) {
}
}
func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) {
out := new(WithPointer)
in := map[string]interface{}{
"name": true, // This is the wrong type.
}
expectedErrorMessage := ErrUnsupportedPtrType.Error()
expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`"
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
@ -136,6 +136,71 @@ func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
if err.Error() != expectedErrorMessage {
t.Fatalf("Unexpected error message: %s", err.Error())
}
if _, ok := err.(ErrUnsupportedPtrType); !ok {
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
}
}
func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) {
out := new(WithPointer)
in := map[string]interface{}{
"name": &map[string]interface{}{"a": 5}, // This is the wrong type.
}
expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`"
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
if err == nil {
t.Fatalf("Expected error due to invalid type.")
}
if err.Error() != expectedErrorMessage {
t.Fatalf("Unexpected error message: %s", err.Error())
}
if _, ok := err.(ErrUnsupportedPtrType); !ok {
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
}
}
func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) {
out := new(WithPointer)
type FooStruct struct{ A int }
in := map[string]interface{}{
"name": FooStruct{A: 5}, // This is the wrong type.
}
expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`"
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
if err == nil {
t.Fatalf("Expected error due to invalid type.")
}
if err.Error() != expectedErrorMessage {
t.Fatalf("Unexpected error message: %s", err.Error())
}
if _, ok := err.(ErrUnsupportedPtrType); !ok {
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
}
}
func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) {
out := new(WithPointer)
type FooStruct struct{ A, B int }
in := map[string]interface{}{
"name": []int{4, 5}, // This is the wrong type.
}
expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`"
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
if err == nil {
t.Fatalf("Expected error due to invalid type.")
}
if err.Error() != expectedErrorMessage {
t.Fatalf("Unexpected error message: %s", err.Error())
}
if _, ok := err.(ErrUnsupportedPtrType); !ok {
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
}
}
func TestStringPointerField(t *testing.T) {
@ -945,3 +1010,218 @@ func sampleSerializedEmbeddedTestModel() *Blog {
return blog
}
func TestUnmarshalNestedStructPtr(t *testing.T) {
type Director struct {
Firstname string `jsonapi:"attr,firstname"`
Surname string `jsonapi:"attr,surname"`
}
type Movie struct {
ID string `jsonapi:"primary,movies"`
Name string `jsonapi:"attr,name"`
Director *Director `jsonapi:"attr,director"`
}
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "movies",
"id": "123",
"attributes": map[string]interface{}{
"name": "The Shawshank Redemption",
"director": map[string]interface{}{
"firstname": "Frank",
"surname": "Darabont",
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)
out := new(Movie)
if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}
if out.Name != "The Shawshank Redemption" {
t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name)
}
if out.Director.Firstname != "Frank" {
t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname)
}
if out.Director.Surname != "Darabont" {
t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname)
}
}
func TestUnmarshalNestedStruct(t *testing.T) {
boss := map[string]interface{}{
"firstname": "Hubert",
"surname": "Farnsworth",
"age": 176,
"hired-at": "2016-08-17T08:27:12Z",
}
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "companies",
"id": "123",
"attributes": map[string]interface{}{
"name": "Planet Express",
"boss": boss,
"founded-at": "2016-08-17T08:27:12Z",
"teams": []map[string]interface{}{
map[string]interface{}{
"name": "Dev",
"members": []map[string]interface{}{
map[string]interface{}{"firstname": "Sean"},
map[string]interface{}{"firstname": "Iz"},
},
"leader": map[string]interface{}{"firstname": "Iz"},
},
map[string]interface{}{
"name": "DxE",
"members": []map[string]interface{}{
map[string]interface{}{"firstname": "Akshay"},
map[string]interface{}{"firstname": "Peri"},
},
"leader": map[string]interface{}{"firstname": "Peri"},
},
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)
out := new(Company)
if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}
if out.Boss.Firstname != "Hubert" {
t.Fatalf("expected `Hubert` at out.Boss.Firstname, but got `%s`", out.Boss.Firstname)
}
if out.Boss.Age != 176 {
t.Fatalf("expected `176` at out.Boss.Age, but got `%d`", out.Boss.Age)
}
if out.Boss.HiredAt.IsZero() {
t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero())
}
if len(out.Teams) != 2 {
t.Fatalf("expected len(out.Teams) to be 2, but got `%d`", len(out.Teams))
}
if out.Teams[0].Name != "Dev" {
t.Fatalf("expected out.Teams[0].Name to be `Dev`, but got `%s`", out.Teams[0].Name)
}
if out.Teams[1].Name != "DxE" {
t.Fatalf("expected out.Teams[1].Name to be `DxE`, but got `%s`", out.Teams[1].Name)
}
if len(out.Teams[0].Members) != 2 {
t.Fatalf("expected len(out.Teams[0].Members) to be 2, but got `%d`", len(out.Teams[0].Members))
}
if len(out.Teams[1].Members) != 2 {
t.Fatalf("expected len(out.Teams[1].Members) to be 2, but got `%d`", len(out.Teams[1].Members))
}
if out.Teams[0].Members[0].Firstname != "Sean" {
t.Fatalf("expected out.Teams[0].Members[0].Firstname to be `Sean`, but got `%s`", out.Teams[0].Members[0].Firstname)
}
if out.Teams[0].Members[1].Firstname != "Iz" {
t.Fatalf("expected out.Teams[0].Members[1].Firstname to be `Iz`, but got `%s`", out.Teams[0].Members[1].Firstname)
}
if out.Teams[1].Members[0].Firstname != "Akshay" {
t.Fatalf("expected out.Teams[1].Members[0].Firstname to be `Akshay`, but got `%s`", out.Teams[1].Members[0].Firstname)
}
if out.Teams[1].Members[1].Firstname != "Peri" {
t.Fatalf("expected out.Teams[1].Members[1].Firstname to be `Peri`, but got `%s`", out.Teams[1].Members[1].Firstname)
}
if out.Teams[0].Leader.Firstname != "Iz" {
t.Fatalf("expected out.Teams[0].Leader.Firstname to be `Iz`, but got `%s`", out.Teams[0].Leader.Firstname)
}
if out.Teams[1].Leader.Firstname != "Peri" {
t.Fatalf("expected out.Teams[1].Leader.Firstname to be `Peri`, but got `%s`", out.Teams[1].Leader.Firstname)
}
}
func TestUnmarshalNestedStructSlice(t *testing.T) {
fry := map[string]interface{}{
"firstname": "Philip J.",
"surname": "Fry",
"age": 25,
"hired-at": "2016-08-17T08:27:12Z",
}
bender := map[string]interface{}{
"firstname": "Bender Bending",
"surname": "Rodriguez",
"age": 19,
"hired-at": "2016-08-17T08:27:12Z",
}
deliveryCrew := map[string]interface{}{
"name": "Delivery Crew",
"members": []interface{}{
fry,
bender,
},
}
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "companies",
"id": "123",
"attributes": map[string]interface{}{
"name": "Planet Express",
"teams": []interface{}{
deliveryCrew,
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)
out := new(Company)
if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}
if out.Teams[0].Name != "Delivery Crew" {
t.Fatalf("Nested struct not unmarshalled: Expected `Delivery Crew` but got `%s`", out.Teams[0].Name)
}
if len(out.Teams[0].Members) != 2 {
t.Fatalf("Nested struct not unmarshalled: Expected to have `2` Members but got `%d`",
len(out.Teams[0].Members))
}
if out.Teams[0].Members[0].Firstname != "Philip J." {
t.Fatalf("Nested struct not unmarshalled: Expected `Philip J.` but got `%s`",
out.Teams[0].Members[0].Firstname)
}
}