diff --git a/README.md b/README.md index 1a2de5b..0d9801d 100644 --- a/README.md +++ b/README.md @@ -345,33 +345,19 @@ 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. +Custom types are supported for primitive types, only, as attributes. Examples, ```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." +type CustomIntType int +type CustomFloatType float64 +type CustomStringType string +``` -// MyTime is a custom type used to handle the custom time format -type MyTime struct { - time.Time -} +Types like following are not supported, but may be in the future: -// 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)) -} +```go +type CustomMapType map[string]interface{} +type CustomSliceMapType []map[string]interface{} ``` ### Errors 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..a7bb0b1 100644 --- a/request.go +++ b/request.go @@ -254,8 +254,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } assign(fieldValue, value) - continue - } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -347,10 +345,37 @@ 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 { + // initialize pointer so it's value + // can be set by assignValue + field.Set(reflect.New(field.Type().Elem())) + field = field.Elem() + + } + + 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) - } else { - field.Set(reflect.Indirect(value)) } } @@ -588,7 +613,6 @@ func handleStruct( return reflect.Value{}, err } - return model, nil } diff --git a/request_test.go b/request_test.go index 111b5fb..3326598 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) @@ -768,6 +774,86 @@ 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": 5, + "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) + 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 `%d`, got `%d`", expected, actual) + } + if expected, actual := customInt, *customAttributeTypes.IntPtr; 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 `%d`", customAttributeTypes.IntPtrNull) + } + + if expected, actual := customFloat, customAttributeTypes.Float; 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) + } +} + +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{}{ @@ -801,11 +887,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{ 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)