Merge pull request #159 from shwoodard/shwoodard-defined-type-attributes

Add support for attributes of custom defined types
This commit is contained in:
Sam Woodard 2018-10-15 16:24:20 -07:00 committed by GitHub
commit 9246c912f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 142 additions and 37 deletions

View File

@ -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

View File

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

View File

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

View File

@ -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 <nil>, 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{

View File

@ -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)