forked from Mirrors/jsonapi
Merge pull request #159 from shwoodard/shwoodard-defined-type-attributes
Add support for attributes of custom defined types
This commit is contained in:
commit
9246c912f5
32
README.md
32
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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
34
request.go
34
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue