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
|
||||
|
||||
### `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
|
||||
// the JSON value was of a different type
|
||||
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
|
||||
|
@ -294,8 +296,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
} else if v.Kind() == reflect.Int {
|
||||
at = v.Int()
|
||||
} else {
|
||||
er = ErrInvalidTime
|
||||
break
|
||||
return ErrInvalidTime
|
||||
}
|
||||
|
||||
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 {
|
||||
at = v.Int()
|
||||
} else {
|
||||
er = ErrInvalidTime
|
||||
break
|
||||
return ErrInvalidTime
|
||||
}
|
||||
|
||||
v := time.Unix(at, 0)
|
||||
|
@ -408,13 +408,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
n := float32(floatValue)
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
case reflect.Float64:
|
||||
n := float64(floatValue)
|
||||
n := floatValue
|
||||
numericValue = reflect.ValueOf(&n)
|
||||
default:
|
||||
// We had a JSON float (numeric), but our field was a non numeric
|
||||
// type
|
||||
er = ErrUnknownFieldNumberType
|
||||
break
|
||||
return ErrUnknownFieldNumberType
|
||||
}
|
||||
|
||||
assign(fieldValue, numericValue)
|
||||
|
@ -437,21 +434,21 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
case uintptr:
|
||||
concreteVal = reflect.ValueOf(&cVal)
|
||||
default:
|
||||
er = ErrUnsupportedPtrType
|
||||
break
|
||||
return ErrUnsupportedPtrType
|
||||
}
|
||||
|
||||
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
|
||||
return ErrUnsupportedPtrType
|
||||
}
|
||||
|
||||
fieldValue.Set(concreteVal)
|
||||
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))
|
||||
|
||||
} 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 nil
|
||||
return er
|
||||
}
|
||||
|
||||
func fullNode(n *Node, included *map[string]*Node) *Node {
|
||||
|
|
|
@ -22,6 +22,14 @@ type WithPointer struct {
|
|||
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) {
|
||||
out := new(WithPointer)
|
||||
in := map[string]interface{}{
|
||||
|
@ -36,7 +44,7 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
|
|||
if *out.Name != "The name" {
|
||||
t.Fatalf("Error unmarshalling to string ptr")
|
||||
}
|
||||
if *out.IsActive != true {
|
||||
if !*out.IsActive {
|
||||
t.Fatalf("Error unmarshalling to bool ptr")
|
||||
}
|
||||
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) {
|
||||
// Build Book payload
|
||||
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) {
|
||||
in := samplePayloadWithID()
|
||||
out := new(Blog)
|
||||
|
@ -759,6 +813,21 @@ func samplePayloadWithID() io.Reader {
|
|||
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 {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
|
|
Loading…
Reference in New Issue