Support pointers as attrs (#40)

* add support to unmarshal attributes to struct field that are ptr types

* Spacing, addressed all the linter warnings, comments to describe errors, cleanup, left a TODO note to resolve the Error messaging.
This commit is contained in:
Aren Patel 2016-07-22 18:34:38 -07:00 committed by GitHub
parent dd46e31eb5
commit a70d58d3c8
3 changed files with 194 additions and 37 deletions

View File

@ -12,16 +12,27 @@ import (
"time" "time"
) )
const unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" const (
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
var ( clientIDAnnotation = "client-id"
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
) )
// Convert an io into a struct instance using jsonapi tags on struct fields. var (
// Method supports single request payloads only, at the moment. Bulk creates and updates // ErrInvalidTime is returned when a struct has a time.Time type field, but
// are not supported yet. // the JSON value was not a unix timestamp integer.
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
// ErrUnknownFieldNumberType is returned when the JSON value was a float
// (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")
)
// 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.
// //
// Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the // Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the
// object graph is complete. That is, in the "relationships" data there are type and id, // object graph is complete. That is, in the "relationships" data there are type and id,
@ -67,10 +78,8 @@ func UnmarshalPayload(in io.Reader, model interface{}) error {
} }
return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap) return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap)
} else {
return unmarshalNode(payload.Data, reflect.ValueOf(model), nil)
} }
return unmarshalNode(payload.Data, reflect.ValueOf(model), nil)
} }
func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
@ -98,7 +107,8 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
} }
return models, nil return models, nil
} else { }
var models []interface{} var models []interface{}
for _, data := range payload.Data { for _, data := range payload.Data {
@ -113,8 +123,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
return models, nil return models, nil
} }
}
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -145,7 +153,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
annotation := args[0] annotation := args[0]
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) < 2) { if (annotation == clientIDAnnotation && len(args) != 1) ||
(annotation != clientIDAnnotation && len(args) < 2) {
er = ErrBadJSONAPIStructTag er = ErrBadJSONAPIStructTag
break break
} }
@ -173,7 +182,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
er = ErrBadJSONAPIID er = ErrBadJSONAPIID
break break
} }
} else if annotation == "client-id" { } else if annotation == clientIDAnnotation {
if data.ClientID == "" { if data.ClientID == "" {
continue continue
} }
@ -194,6 +203,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
v := reflect.ValueOf(val) v := reflect.ValueOf(val)
// Handle field of type time.Time
if fieldValue.Type() == reflect.TypeOf(time.Time{}) { if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
var at int64 var at int64
@ -244,8 +254,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
continue continue
} }
// JSON value was a float (numeric)
if v.Kind() == reflect.Float64 { if v.Kind() == reflect.Float64 {
// Handle JSON numeric case
floatValue := v.Interface().(float64) floatValue := v.Interface().(float64)
// The field may or may not be a pointer to a numeric; the kind var // The field may or may not be a pointer to a numeric; the kind var
@ -297,6 +307,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
n := float64(floatValue) n := float64(floatValue)
numericValue = reflect.ValueOf(&n) numericValue = reflect.ValueOf(&n)
default: default:
// We had a JSON float (numeric), but our field was a non numeric
// type
er = ErrUnknownFieldNumberType er = ErrUnknownFieldNumberType
break break
} }
@ -309,7 +321,40 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
continue 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:
er = ErrUnsupportedPtrType
break
}
if fieldValue.Type() != concreteVal.Type() {
// TODO: use fmt.Errorf so that you can have a more informative
// message that reports the attempted type that was not supported.
er = ErrUnsupportedPtrType
break
}
fieldValue.Set(concreteVal)
continue
}
fieldValue.Set(reflect.ValueOf(val)) fieldValue.Set(reflect.ValueOf(val))
} else if annotation == "relation" { } else if annotation == "relation" {
isSlice := fieldValue.Type().Kind() == reflect.Slice isSlice := fieldValue.Type().Kind() == reflect.Slice
@ -331,7 +376,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
for _, n := range data { for _, n := range data {
m := reflect.New(fieldValue.Type().Elem().Elem()) m := reflect.New(fieldValue.Type().Elem().Elem())
if err := unmarshalNode(fullNode(n, included), m, included); err != nil { if err := unmarshalNode(
fullNode(n, included),
m,
included,
); err != nil {
er = err er = err
break break
} }
@ -345,12 +394,18 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
json.NewEncoder(buf).Encode(data.Relationships[args[1]]) json.NewEncoder(buf).Encode(
data.Relationships[args[1]],
)
json.NewDecoder(buf).Decode(relationship) json.NewDecoder(buf).Decode(relationship)
m := reflect.New(fieldValue.Type().Elem()) m := reflect.New(fieldValue.Type().Elem())
if err := unmarshalNode(fullNode(relationship.Data, included), m, included); err != nil { if err := unmarshalNode(
fullNode(relationship.Data, included),
m,
included,
); err != nil {
er = err er = err
break break
} }

View File

@ -13,6 +13,94 @@ type BadModel struct {
ID int `jsonapi:"primary"` ID int `jsonapi:"primary"`
} }
type WithPointer struct {
ID string `jsonapi:"primary,with-pointers"`
Name *string `jsonapi:"attr,name"`
IsActive *bool `jsonapi:"attr,is-active"`
IntVal *int `jsonapi:"attr,int-val"`
FloatVal *float32 `jsonapi:"attr,float-val"`
}
func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
out := new(WithPointer)
in := map[string]interface{}{
"name": "The name",
"is-active": true,
"int-val": 8,
"float-val": 1.1,
}
if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil {
t.Fatal(err)
}
if *out.Name != "The name" {
t.Fatalf("Error unmarshalling to string ptr")
}
if *out.IsActive != true {
t.Fatalf("Error unmarshalling to bool ptr")
}
if *out.IntVal != 8 {
t.Fatalf("Error unmarshalling to int ptr")
}
if *out.FloatVal != 1.1 {
t.Fatalf("Error unmarshalling to float ptr")
}
}
func TestUnmarshalToStructWithPointerAttr_AbsentVal(t *testing.T) {
out := new(WithPointer)
in := map[string]interface{}{
"name": "The name",
"is-active": true,
}
if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil {
t.Fatalf("Error unmarshalling to Foo")
}
// these were present in the payload -- expect val to be not nil
if out.Name == nil || out.IsActive == nil {
t.Fatalf("Error unmarshalling; expected ptr to be not nil")
}
// these were absent in the payload -- expect val to be nil
if out.IntVal != nil || out.FloatVal != nil {
t.Fatalf("Error unmarshalling; expected ptr to be nil")
}
}
func TestStringPointerField(t *testing.T) {
// Build Book payload
description := "Hello World!"
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "books",
"id": "5",
"attributes": map[string]interface{}{
"author": "aren55555",
"description": description,
"isbn": "",
},
},
}
payload, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
// Parse JSON API payload
book := new(Book)
if err := UnmarshalPayload(bytes.NewReader(payload), book); err != nil {
t.Fatal(err)
}
if book.Description == nil {
t.Fatal("Was not expecting a nil pointer for book.Description")
}
if expected, actual := description, *book.Description; expected != actual {
t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual)
}
}
func TestMalformedTag(t *testing.T) { func TestMalformedTag(t *testing.T) {
out := new(BadModel) out := new(BadModel)
err := UnmarshalPayload(samplePayload(), out) err := UnmarshalPayload(samplePayload(), out)
@ -264,6 +352,7 @@ func samplePayloadWithoutIncluded() (result []byte, err error) {
}, },
}, },
} }
result, err = json.Marshal(data) result, err = json.Marshal(data)
return return
} }
@ -333,7 +422,6 @@ func samplePayload() io.Reader {
} }
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
json.NewEncoder(out).Encode(payload) json.NewEncoder(out).Encode(payload)
return out return out
@ -352,7 +440,21 @@ func samplePayloadWithID() io.Reader {
} }
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
json.NewEncoder(out).Encode(payload)
return out
}
func sampleWithPointerPayload(m map[string]interface{}) io.Reader {
payload := &OnePayload{
Data: &Node{
ID: "2",
Type: "with-pointers",
Attributes: m,
},
}
out := bytes.NewBuffer(nil)
json.NewEncoder(out).Encode(payload) json.NewEncoder(out).Encode(payload)
return out return out
@ -440,7 +542,6 @@ func sampleSerializedEmbeddedTestModel() *Blog {
MarshalOnePayloadEmbedded(out, testModel()) MarshalOnePayloadEmbedded(out, testModel())
blog := new(Blog) blog := new(Blog)
UnmarshalPayload(out, blog) UnmarshalPayload(out, blog)
return blog return blog

View File

@ -41,6 +41,7 @@ type Book struct {
Author string `jsonapi:"attr,author"` Author string `jsonapi:"attr,author"`
ISBN string `jsonapi:"attr,isbn"` ISBN string `jsonapi:"attr,isbn"`
Title string `jsonapi:"attr,title,omitempty"` Title string `jsonapi:"attr,title,omitempty"`
Description *string `jsonapi:"attr,description"`
Pages *uint `jsonapi:"attr,pages,omitempty"` Pages *uint `jsonapi:"attr,pages,omitempty"`
PublishedAt time.Time PublishedAt time.Time
} }