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
115
request.go
115
request.go
|
@ -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) {
|
||||||
|
@ -97,22 +106,21 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
||||||
models = append(models, model.Interface())
|
models = append(models, model.Interface())
|
||||||
}
|
}
|
||||||
|
|
||||||
return models, nil
|
|
||||||
} else {
|
|
||||||
var models []interface{}
|
|
||||||
|
|
||||||
for _, data := range payload.Data {
|
|
||||||
model := reflect.New(t.Elem())
|
|
||||||
err := unmarshalNode(data, model, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
models = append(models, model.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
return models, nil
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var models []interface{}
|
||||||
|
|
||||||
|
for _, data := range payload.Data {
|
||||||
|
model := reflect.New(t.Elem())
|
||||||
|
err := unmarshalNode(data, model, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
models = append(models, model.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
105
request_test.go
105
request_test.go
|
@ -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
|
||||||
|
|
|
@ -37,11 +37,12 @@ type Comment struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Book struct {
|
type Book struct {
|
||||||
ID int `jsonapi:"primary,books"`
|
ID int `jsonapi:"primary,books"`
|
||||||
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"`
|
||||||
Pages *uint `jsonapi:"attr,pages,omitempty"`
|
Description *string `jsonapi:"attr,description"`
|
||||||
|
Pages *uint `jsonapi:"attr,pages,omitempty"`
|
||||||
PublishedAt time.Time
|
PublishedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue