forked from Mirrors/jsonapi
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:
parent
dd46e31eb5
commit
a70d58d3c8
95
request.go
95
request.go
|
@ -12,16 +12,27 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
|
||||
var (
|
||||
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")
|
||||
const (
|
||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
clientIDAnnotation = "client-id"
|
||||
)
|
||||
|
||||
// Convert an io into a struct instance using jsonapi tags on struct fields.
|
||||
// Method supports single request payloads only, at the moment. Bulk creates and updates
|
||||
// are not supported yet.
|
||||
var (
|
||||
// ErrInvalidTime is returned when a struct has a time.Time type field, but
|
||||
// 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
|
||||
// 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)
|
||||
} 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) {
|
||||
|
@ -98,7 +107,8 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
|||
}
|
||||
|
||||
return models, nil
|
||||
} else {
|
||||
}
|
||||
|
||||
var models []interface{}
|
||||
|
||||
for _, data := range payload.Data {
|
||||
|
@ -111,8 +121,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
|||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
|
||||
|
@ -145,7 +153,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
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
|
||||
break
|
||||
}
|
||||
|
@ -173,7 +182,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
er = ErrBadJSONAPIID
|
||||
break
|
||||
}
|
||||
} else if annotation == "client-id" {
|
||||
} else if annotation == clientIDAnnotation {
|
||||
if data.ClientID == "" {
|
||||
continue
|
||||
}
|
||||
|
@ -194,6 +203,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
v := reflect.ValueOf(val)
|
||||
|
||||
// Handle field of type time.Time
|
||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
||||
var at int64
|
||||
|
||||
|
@ -244,8 +254,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
continue
|
||||
}
|
||||
|
||||
// JSON value was a float (numeric)
|
||||
if v.Kind() == reflect.Float64 {
|
||||
// Handle JSON numeric case
|
||||
floatValue := v.Interface().(float64)
|
||||
|
||||
// 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)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
default:
|
||||
// We had a JSON float (numeric), but our field was a non numeric
|
||||
// type
|
||||
er = ErrUnknownFieldNumberType
|
||||
break
|
||||
}
|
||||
|
@ -309,7 +321,40 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
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))
|
||||
|
||||
} else if annotation == "relation" {
|
||||
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 {
|
||||
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
|
||||
break
|
||||
}
|
||||
|
@ -345,12 +394,18 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
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)
|
||||
|
||||
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
|
||||
break
|
||||
}
|
||||
|
|
105
request_test.go
105
request_test.go
|
@ -13,6 +13,94 @@ type BadModel struct {
|
|||
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) {
|
||||
out := new(BadModel)
|
||||
err := UnmarshalPayload(samplePayload(), out)
|
||||
|
@ -264,6 +352,7 @@ func samplePayloadWithoutIncluded() (result []byte, err error) {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err = json.Marshal(data)
|
||||
return
|
||||
}
|
||||
|
@ -333,7 +422,6 @@ func samplePayload() io.Reader {
|
|||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
|
||||
json.NewEncoder(out).Encode(payload)
|
||||
|
||||
return out
|
||||
|
@ -352,7 +440,21 @@ func samplePayloadWithID() io.Reader {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
return out
|
||||
|
@ -440,7 +542,6 @@ func sampleSerializedEmbeddedTestModel() *Blog {
|
|||
MarshalOnePayloadEmbedded(out, testModel())
|
||||
|
||||
blog := new(Blog)
|
||||
|
||||
UnmarshalPayload(out, blog)
|
||||
|
||||
return blog
|
||||
|
|
|
@ -41,6 +41,7 @@ type Book struct {
|
|||
Author string `jsonapi:"attr,author"`
|
||||
ISBN string `jsonapi:"attr,isbn"`
|
||||
Title string `jsonapi:"attr,title,omitempty"`
|
||||
Description *string `jsonapi:"attr,description"`
|
||||
Pages *uint `jsonapi:"attr,pages,omitempty"`
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue