Merge pull request #38 from thedodd/handle-type-errors

Handle type errors during unmarshalNode routine.
This commit is contained in:
Sam Woodard 2017-01-31 12:35:06 -08:00 committed by GitHub
commit 6e86cbdd0d
5 changed files with 238 additions and 21 deletions

View File

@ -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`

55
errors.go Normal file
View File

@ -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 fields 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)
}

52
errors_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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{