From ccac636b4bc988406af60cb0f7d60c668d714c40 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Wed, 28 Mar 2018 15:04:52 +0200 Subject: [PATCH 1/8] Add support for attributes of custom defined types --- models_test.go | 15 +++++++++++++++ request.go | 39 +++++++++++++++++++++++++++++++++------ request_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/models_test.go b/models_test.go index d443378..2d4aae4 100644 --- a/models_test.go +++ b/models_test.go @@ -176,3 +176,18 @@ type Employee struct { Age int `jsonapi:"attr,age"` HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"` } + +type CustomIntType int +type CustomFloatType float64 +type CustomStringType string + +type CustomAttributeTypes struct { + ID string `jsonapi:"primary,customtypes"` + + Int CustomIntType `jsonapi:"attr,int"` + IntPtr *CustomIntType `jsonapi:"attr,intptr"` + IntPtrNull *CustomIntType `jsonapi:"attr,intptrnull"` + + Float CustomFloatType `jsonapi:"attr,float"` + String CustomStringType `jsonapi:"attr,string"` +} diff --git a/request.go b/request.go index b9883f2..a830bb1 100644 --- a/request.go +++ b/request.go @@ -253,9 +253,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) break } - assign(fieldValue, value) - continue - + // As a final catch-all, ensure types line up to avoid a runtime panic. + if fieldValue.Kind() != v.Kind() { + return ErrInvalidType + } + assignValue(fieldValue, reflect.ValueOf(val)) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -347,10 +349,36 @@ func fullNode(n *Node, included *map[string]*Node) *Node { // assign will take the value specified and assign it to the field; if // field is expecting a ptr assign will assign a ptr. func assign(field, value reflect.Value) { + value = reflect.Indirect(value) + if field.Kind() == reflect.Ptr { - field.Set(value) + // initialize pointer so it's value + // can be set by assignValue + field.Set(reflect.New(field.Type().Elem())) + assignValue(field.Elem(), value) } else { - field.Set(reflect.Indirect(value)) + assignValue(field, value) + } +} + +// assign assigns the specified value to the field, +// expecting both values not to be pointer types. +func assignValue(field, value reflect.Value) { + switch field.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + field.SetInt(value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + field.SetUint(value.Uint()) + case reflect.Float32, reflect.Float64: + field.SetFloat(value.Float()) + case reflect.String: + field.SetString(value.String()) + case reflect.Bool: + field.SetBool(value.Bool()) + default: + field.Set(value) } } @@ -588,7 +616,6 @@ func handleStruct( return reflect.Value{}, err } - return model, nil } diff --git a/request_test.go b/request_test.go index 111b5fb..2a8e48b 100644 --- a/request_test.go +++ b/request_test.go @@ -768,6 +768,54 @@ func TestManyPayload_withLinks(t *testing.T) { } } +func TestUnmarshalCustomTypeAttributes(t *testing.T) { + customInt := CustomIntType(5) + customFloat := CustomFloatType(1.5) + customString := CustomStringType("Test") + + data := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "customtypes", + "id": "1", + "attributes": map[string]interface{}{ + "int": customInt, + "intptr": &customInt, + "intptrnull": nil, + + "float": customFloat, + "string": customString, + }, + }, + } + payload, err := payload(data) + if err != nil { + t.Fatal(err) + } + + // Parse JSON API payload + customAttributeTypes := new(CustomAttributeTypes) + if err := UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes); err != nil { + t.Fatal(err) + } + + if expected, actual := customInt, customAttributeTypes.Int; expected != actual { + t.Fatalf("Was expecting custom int to be `%s`, got `%s`", expected, actual) + } + if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual { + t.Fatalf("Was expecting custom int pointer to be `%s`, got `%s`", expected, actual) + } + if customAttributeTypes.IntPtrNull != nil { + t.Fatalf("Was expecting custom int pointer to be , got `%s`", customAttributeTypes.IntPtrNull) + } + + if expected, actual := customFloat, customAttributeTypes.Float; expected != actual { + t.Fatalf("Was expecting custom float to be `%s`, got `%s`", expected, actual) + } + if expected, actual := customString, customAttributeTypes.String; expected != actual { + t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual) + } +} + func samplePayloadWithoutIncluded() map[string]interface{} { return map[string]interface{}{ "data": map[string]interface{}{ From 87c6b8e5b5a83ae51b4de974b0262aa0e08b5c03 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Wed, 28 Mar 2018 15:14:32 +0200 Subject: [PATCH 2/8] Fixed format types --- request.go | 4 ++-- request_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/request.go b/request.go index a830bb1..93275a7 100644 --- a/request.go +++ b/request.go @@ -254,10 +254,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != v.Kind() { + if fieldValue.Kind() != value.Kind() { return ErrInvalidType } - assignValue(fieldValue, reflect.ValueOf(val)) + assignValue(fieldValue, value) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice diff --git a/request_test.go b/request_test.go index 2a8e48b..a3e52f9 100644 --- a/request_test.go +++ b/request_test.go @@ -799,17 +799,17 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { } if expected, actual := customInt, customAttributeTypes.Int; expected != actual { - t.Fatalf("Was expecting custom int to be `%s`, got `%s`", expected, actual) + t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual) } if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual { - t.Fatalf("Was expecting custom int pointer to be `%s`, got `%s`", expected, actual) + t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual) } if customAttributeTypes.IntPtrNull != nil { - t.Fatalf("Was expecting custom int pointer to be , got `%s`", customAttributeTypes.IntPtrNull) + t.Fatalf("Was expecting custom int pointer to be , got `%d`", customAttributeTypes.IntPtrNull) } if expected, actual := customFloat, customAttributeTypes.Float; expected != actual { - t.Fatalf("Was expecting custom float to be `%s`, got `%s`", expected, actual) + t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual) } if expected, actual := customString, customAttributeTypes.String; expected != actual { t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual) From ab2491314841ff2f2d2633cbc136aa029317b21a Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Fri, 5 Oct 2018 07:27:18 -0700 Subject: [PATCH 3/8] adjust test so values are like from a json payload --- request_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/request_test.go b/request_test.go index a3e52f9..6fb8a7e 100644 --- a/request_test.go +++ b/request_test.go @@ -778,12 +778,12 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { "type": "customtypes", "id": "1", "attributes": map[string]interface{}{ - "int": customInt, - "intptr": &customInt, + "int": 5, + "intptr": 5, "intptrnull": nil, - "float": customFloat, - "string": customString, + "float": 1.5, + "string": "Test", }, }, } From d05fcd97df612b980b0bcecbfd8939dc22ecca32 Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Fri, 5 Oct 2018 08:56:32 -0700 Subject: [PATCH 4/8] one line method removed --- request_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/request_test.go b/request_test.go index 6fb8a7e..6e7870b 100644 --- a/request_test.go +++ b/request_test.go @@ -301,7 +301,10 @@ func TestUnmarshalSetsID(t *testing.T) { func TestUnmarshal_nonNumericID(t *testing.T) { data := samplePayloadWithoutIncluded() data["data"].(map[string]interface{})["id"] = "non-numeric-id" - payload, _ := payload(data) + payload, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } in := bytes.NewReader(payload) out := new(Post) @@ -402,7 +405,10 @@ func TestUnmarshalInvalidISO8601(t *testing.T) { } func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { - data, _ := payload(samplePayloadWithoutIncluded()) + data, err := json.Marshal(samplePayloadWithoutIncluded()) + if err != nil { + t.Fatal(err) + } in := bytes.NewReader(data) out := new(Post) @@ -787,7 +793,7 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { }, }, } - payload, err := payload(data) + payload, err := json.Marshal(data) if err != nil { t.Fatal(err) } @@ -849,11 +855,6 @@ func samplePayloadWithoutIncluded() map[string]interface{} { } } -func payload(data map[string]interface{}) (result []byte, err error) { - result, err = json.Marshal(data) - return -} - func samplePayload() io.Reader { payload := &OnePayload{ Data: &Node{ From ed08d4f02a4dd48caa526df5702bdc3bc837b578 Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Fri, 5 Oct 2018 08:57:33 -0700 Subject: [PATCH 5/8] refactor, consistency, add test to ensure we don't need additional type check --- request.go | 13 +++++-------- request_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/request.go b/request.go index 93275a7..a7bb0b1 100644 --- a/request.go +++ b/request.go @@ -253,11 +253,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) break } - // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != value.Kind() { - return ErrInvalidType - } - assignValue(fieldValue, value) + assign(fieldValue, value) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -355,10 +351,11 @@ func assign(field, value reflect.Value) { // initialize pointer so it's value // can be set by assignValue field.Set(reflect.New(field.Type().Elem())) - assignValue(field.Elem(), value) - } else { - assignValue(field, value) + field = field.Elem() + } + + assignValue(field, value) } // assign assigns the specified value to the field, diff --git a/request_test.go b/request_test.go index 6e7870b..3326598 100644 --- a/request_test.go +++ b/request_test.go @@ -822,6 +822,38 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { } } +func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) { + data := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "customtypes", + "id": "1", + "attributes": map[string]interface{}{ + "int": "bad", + "intptr": 5, + "intptrnull": nil, + + "float": 1.5, + "string": "Test", + }, + }, + } + payload, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + // Parse JSON API payload + customAttributeTypes := new(CustomAttributeTypes) + err = UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes) + if err == nil { + t.Fatal("Expected an error unmarshalling the payload due to type mismatch, got none") + } + + if err != ErrInvalidType { + t.Fatalf("Expected error to be %v, was %v", ErrInvalidType, err) + } +} + func samplePayloadWithoutIncluded() map[string]interface{} { return map[string]interface{}{ "data": map[string]interface{}{ From 906357051e782d7c89b49ab2d417933c7ff2a4f1 Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Thu, 11 Oct 2018 04:47:50 -0700 Subject: [PATCH 6/8] run gofmt on package --- response_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/response_test.go b/response_test.go index dc89c48..5b42595 100644 --- a/response_test.go +++ b/response_test.go @@ -39,10 +39,9 @@ func TestMarshalPayload(t *testing.T) { func TestMarshalPayloadWithNulls(t *testing.T) { - books := []*Book{nil, {ID:101}, nil} + books := []*Book{nil, {ID: 101}, nil} var jsonData map[string]interface{} - out := bytes.NewBuffer(nil) if err := MarshalPayload(out, books); err != nil { t.Fatal(err) From 1947fea11fff75b7d6977b488f6a09c166a3c055 Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Thu, 11 Oct 2018 09:04:55 -0700 Subject: [PATCH 7/8] wrote failing test for example so removing from docs --- README.md | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/README.md b/README.md index c599dd3..44b0541 100644 --- a/README.md +++ b/README.md @@ -343,37 +343,6 @@ func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { } ``` -### Custom types - -If you need to support custom types (e.g. for custom time formats), you'll need to implement the json.Marshaler and json.Unmarshaler interfaces on the type. - -```go -// MyTimeFormat is a custom format I invented for fun -const MyTimeFormat = "The time is 15:04:05. The year is 2006, and it is day 2 of January." - -// MyTime is a custom type used to handle the custom time format -type MyTime struct { - time.Time -} - -// UnmarshalJSON to implement the json.Unmarshaler interface -func (m *MyTime) UnmarshalJSON(b []byte) error { - t, err := time.Parse(MyTimeFormat, string(b)) - if err != nil { - return err - } - - m.Time = t - - return nil -} - -// MarshalJSON to implement the json.Marshaler interface -func (m *MyTime) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Time.Format(MyTimeFormat)) -} -``` - ### Errors This package also implements support for JSON API compatible `errors` payloads using the following types. From e22856db883897c7a5b1cb4fb606e50682498970 Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Thu, 11 Oct 2018 09:08:03 -0700 Subject: [PATCH 8/8] add documentation for primative custom types only --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 44b0541..2c636f3 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,23 @@ func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { } ``` +### Custom types + +Custom types are supported for primitive types, only, as attributes. Examples, + +```go +type CustomIntType int +type CustomFloatType float64 +type CustomStringType string +``` + +Types like following are not supported, but may be in the future: + +```go +type CustomMapType map[string]interface{} +type CustomSliceMapType []map[string]interface{} +``` + ### Errors This package also implements support for JSON API compatible `errors` payloads using the following types.