forked from Mirrors/jsonapi
Merge pull request #38 from thedodd/handle-type-errors
Handle type errors during unmarshalNode routine.
This commit is contained in:
commit
6e86cbdd0d
48
README.md
48
README.md
|
@ -359,6 +359,54 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *map[string]interface
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
This package also implements support for JSON API compatible `errors` payloads using the following types.
|
||||||
|
|
||||||
|
#### `MarshalErrors`
|
||||||
|
```go
|
||||||
|
MarshalErrors(w io.Writer, errs []*ErrorObject) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes a JSON API response using the given `[]error`.
|
||||||
|
|
||||||
|
#### `ErrorsPayload`
|
||||||
|
```go
|
||||||
|
type ErrorsPayload struct {
|
||||||
|
Errors []*ErrorObject `json:"errors"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||||
|
|
||||||
|
#### `ErrorObject`
|
||||||
|
```go
|
||||||
|
type ErrorObject struct { ... }
|
||||||
|
|
||||||
|
// Error implements the `Error` interface.
|
||||||
|
func (e *ErrorObject) Error() string {
|
||||||
|
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
|
||||||
|
|
||||||
|
The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
|
||||||
|
|
||||||
|
##### Errors Example Code
|
||||||
|
```go
|
||||||
|
// An error has come up in your code, so set an appropriate status, and serialize the error.
|
||||||
|
if err := validate(&myStructToValidate); err != nil {
|
||||||
|
context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status.
|
||||||
|
jsonapi.MarshalErrors(w, []*ErrorObject{{
|
||||||
|
Title: "Validation Error",
|
||||||
|
Detail: "Given request body was invalid.",
|
||||||
|
Status: "400",
|
||||||
|
Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"},
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### `MarshalOnePayloadEmbedded`
|
### `MarshalOnePayloadEmbedded`
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package jsonapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalErrors writes a JSON API response using the given `[]error`.
|
||||||
|
//
|
||||||
|
// For more information on JSON API error payloads, see the spec here:
|
||||||
|
// http://jsonapi.org/format/#document-top-level
|
||||||
|
// and here: http://jsonapi.org/format/#error-objects.
|
||||||
|
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
|
||||||
|
if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||||
|
type ErrorsPayload struct {
|
||||||
|
Errors []*ErrorObject `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
|
||||||
|
//
|
||||||
|
// The main idea behind this struct is that you can use it directly in your code as an error type
|
||||||
|
// and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
|
||||||
|
// For more information on Golang errors, see: https://golang.org/pkg/errors/
|
||||||
|
// For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects
|
||||||
|
type ErrorObject struct {
|
||||||
|
// ID is a unique identifier for this particular occurrence of a problem.
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
|
// Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
|
||||||
|
// Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
|
||||||
|
// Status is the HTTP status code applicable to this problem, expressed as a string value.
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
|
// Code is an application-specific error code, expressed as a string value.
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
|
||||||
|
// Meta is an object containing non-standard meta-information about the error.
|
||||||
|
Meta *map[string]interface{} `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the `Error` interface.
|
||||||
|
func (e *ErrorObject) Error() string {
|
||||||
|
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package jsonapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) {
|
||||||
|
err := &ErrorObject{Title: "Title test.", Detail: "Detail test."}
|
||||||
|
var input error = err
|
||||||
|
|
||||||
|
output := input.Error()
|
||||||
|
|
||||||
|
if output != fmt.Sprintf("Error: %s %s\n", err.Title, err.Detail) {
|
||||||
|
t.Fatal("Unexpected output.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) {
|
||||||
|
var marshalErrorsTableTasts = []struct {
|
||||||
|
In []*ErrorObject
|
||||||
|
Out map[string]interface{}
|
||||||
|
}{
|
||||||
|
{ // This tests that given fields are turned into the appropriate JSON representation.
|
||||||
|
In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}},
|
||||||
|
Out: map[string]interface{}{"errors": []interface{}{
|
||||||
|
map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{ // This tests that the `Meta` field is serialized properly.
|
||||||
|
In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}},
|
||||||
|
Out: map[string]interface{}{"errors": []interface{}{
|
||||||
|
map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testRow := range marshalErrorsTableTasts {
|
||||||
|
buffer, output := bytes.NewBuffer(nil), map[string]interface{}{}
|
||||||
|
var writer io.Writer = buffer
|
||||||
|
|
||||||
|
_ = MarshalErrors(writer, testRow.In)
|
||||||
|
json.Unmarshal(buffer.Bytes(), &output)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(output, testRow.Out) {
|
||||||
|
t.Fatalf("Expected: \n%#v \nto equal: \n%#v", output, testRow.Out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
request.go
33
request.go
|
@ -30,6 +30,8 @@ var (
|
||||||
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
|
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
|
||||||
// the JSON value was of a different type
|
// the JSON value was of a different type
|
||||||
ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported")
|
ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported")
|
||||||
|
// ErrInvalidType is returned when the given type is incompatible with the expected type.
|
||||||
|
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
|
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
|
||||||
|
@ -294,8 +296,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
} else if v.Kind() == reflect.Int {
|
} else if v.Kind() == reflect.Int {
|
||||||
at = v.Int()
|
at = v.Int()
|
||||||
} else {
|
} else {
|
||||||
er = ErrInvalidTime
|
return ErrInvalidTime
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Unix(at, 0)
|
t := time.Unix(at, 0)
|
||||||
|
@ -346,8 +347,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
} else if v.Kind() == reflect.Int {
|
} else if v.Kind() == reflect.Int {
|
||||||
at = v.Int()
|
at = v.Int()
|
||||||
} else {
|
} else {
|
||||||
er = ErrInvalidTime
|
return ErrInvalidTime
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v := time.Unix(at, 0)
|
v := time.Unix(at, 0)
|
||||||
|
@ -408,13 +408,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
n := float32(floatValue)
|
n := float32(floatValue)
|
||||||
numericValue = reflect.ValueOf(&n)
|
numericValue = reflect.ValueOf(&n)
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
n := float64(floatValue)
|
n := floatValue
|
||||||
numericValue = reflect.ValueOf(&n)
|
numericValue = reflect.ValueOf(&n)
|
||||||
default:
|
default:
|
||||||
// We had a JSON float (numeric), but our field was a non numeric
|
return ErrUnknownFieldNumberType
|
||||||
// type
|
|
||||||
er = ErrUnknownFieldNumberType
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(fieldValue, numericValue)
|
assign(fieldValue, numericValue)
|
||||||
|
@ -437,21 +434,21 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
case uintptr:
|
case uintptr:
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
concreteVal = reflect.ValueOf(&cVal)
|
||||||
default:
|
default:
|
||||||
er = ErrUnsupportedPtrType
|
return ErrUnsupportedPtrType
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fieldValue.Type() != concreteVal.Type() {
|
if fieldValue.Type() != concreteVal.Type() {
|
||||||
// TODO: use fmt.Errorf so that you can have a more informative
|
return ErrUnsupportedPtrType
|
||||||
// message that reports the attempted type that was not supported.
|
|
||||||
er = ErrUnsupportedPtrType
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValue.Set(concreteVal)
|
fieldValue.Set(concreteVal)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
||||||
|
if fieldValue.Kind() != v.Kind() {
|
||||||
|
return ErrInvalidType
|
||||||
|
}
|
||||||
fieldValue.Set(reflect.ValueOf(val))
|
fieldValue.Set(reflect.ValueOf(val))
|
||||||
|
|
||||||
} else if annotation == annotationRelation {
|
} else if annotation == annotationRelation {
|
||||||
|
@ -529,11 +526,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if er != nil {
|
return er
|
||||||
return er
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fullNode(n *Node, included *map[string]*Node) *Node {
|
func fullNode(n *Node, included *map[string]*Node) *Node {
|
||||||
|
|
|
@ -22,6 +22,14 @@ type WithPointer struct {
|
||||||
FloatVal *float32 `jsonapi:"attr,float-val"`
|
FloatVal *float32 `jsonapi:"attr,float-val"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModelBadTypes struct {
|
||||||
|
ID string `jsonapi:"primary,badtypes"`
|
||||||
|
StringField string `jsonapi:"attr,string_field"`
|
||||||
|
FloatField float64 `jsonapi:"attr,float_field"`
|
||||||
|
TimeField time.Time `jsonapi:"attr,time_field"`
|
||||||
|
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
|
func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
|
||||||
out := new(WithPointer)
|
out := new(WithPointer)
|
||||||
in := map[string]interface{}{
|
in := map[string]interface{}{
|
||||||
|
@ -36,7 +44,7 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
|
||||||
if *out.Name != "The name" {
|
if *out.Name != "The name" {
|
||||||
t.Fatalf("Error unmarshalling to string ptr")
|
t.Fatalf("Error unmarshalling to string ptr")
|
||||||
}
|
}
|
||||||
if *out.IsActive != true {
|
if !*out.IsActive {
|
||||||
t.Fatalf("Error unmarshalling to bool ptr")
|
t.Fatalf("Error unmarshalling to bool ptr")
|
||||||
}
|
}
|
||||||
if *out.IntVal != 8 {
|
if *out.IntVal != 8 {
|
||||||
|
@ -98,6 +106,23 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
|
||||||
|
out := new(WithPointer)
|
||||||
|
in := map[string]interface{}{
|
||||||
|
"name": true, // This is the wrong type.
|
||||||
|
}
|
||||||
|
expectedErrorMessage := ErrUnsupportedPtrType.Error()
|
||||||
|
|
||||||
|
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error due to invalid type.")
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStringPointerField(t *testing.T) {
|
func TestStringPointerField(t *testing.T) {
|
||||||
// Build Book payload
|
// Build Book payload
|
||||||
description := "Hello World!"
|
description := "Hello World!"
|
||||||
|
@ -150,6 +175,35 @@ func TestUnmarshalInvalidJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalInvalidJSON_BadType(t *testing.T) {
|
||||||
|
var badTypeTests = []struct {
|
||||||
|
Field string
|
||||||
|
BadValue interface{}
|
||||||
|
Error error
|
||||||
|
}{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields.
|
||||||
|
{Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType}, // Expected string.
|
||||||
|
{Field: "float_field", BadValue: "A string.", Error: ErrInvalidType}, // Expected float64.
|
||||||
|
{Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected int64.
|
||||||
|
{Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64.
|
||||||
|
}
|
||||||
|
for idx, test := range badTypeTests {
|
||||||
|
println("index:", idx)
|
||||||
|
out := new(ModelBadTypes)
|
||||||
|
in := map[string]interface{}{}
|
||||||
|
in[test.Field] = test.BadValue
|
||||||
|
expectedErrorMessage := test.Error.Error()
|
||||||
|
|
||||||
|
err := UnmarshalPayload(samplePayloadWithBadTypes(in), out)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error due to invalid type.")
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshalSetsID(t *testing.T) {
|
func TestUnmarshalSetsID(t *testing.T) {
|
||||||
in := samplePayloadWithID()
|
in := samplePayloadWithID()
|
||||||
out := new(Blog)
|
out := new(Blog)
|
||||||
|
@ -759,6 +813,21 @@ func samplePayloadWithID() io.Reader {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader {
|
||||||
|
payload := &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
ID: "2",
|
||||||
|
Type: "badtypes",
|
||||||
|
Attributes: m,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
json.NewEncoder(out).Encode(payload)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func sampleWithPointerPayload(m map[string]interface{}) io.Reader {
|
func sampleWithPointerPayload(m map[string]interface{}) io.Reader {
|
||||||
payload := &OnePayload{
|
payload := &OnePayload{
|
||||||
Data: &Node{
|
Data: &Node{
|
||||||
|
|
Loading…
Reference in New Issue