diff --git a/models_test.go b/models_test.go index a53dd61..d443378 100644 --- a/models_test.go +++ b/models_test.go @@ -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"` +} diff --git a/request.go b/request.go index fe29706..b9883f2 100644 --- a/request.go +++ b/request.go @@ -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 +} diff --git a/request_test.go b/request_test.go index 2206449..111b5fb 100644 --- a/request_test.go +++ b/request_test.go @@ -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) + } +}