diff --git a/error.go b/error.go index dccb8dc..02ff898 100644 --- a/error.go +++ b/error.go @@ -14,26 +14,9 @@ var DefaultErrorDetail = "Request failed, something went wrong." // DefaultTitle can be customized to provide a more customized ISE Title var DefaultErrorTitle = "Internal Server Error" -// SendableError conforms to a standard error format for logging, but can also -// be sent as a JSON response -type SendableError interface { - Sendable - // Error returns a safe for user error message - Error() string - // Internal returns a fully formatted error including any sensitive debugging - // information contained in the ISE field. Really only useful when logging an - // outbound response - Internal() string -} - -// Error represents a JSON Specification Error. Error.Source.Pointer is used in 422 -// status responses to indicate validation errors on a JSON Object attribute. -// -// ISE (internal server error) captures the server error internally to help with -// logging/troubleshooting, but is never returned in a response. -// -// Once a jsh.Error is returned, and you have logged/handled it accordingly, you -// can simply return it using jsh.Send(): +// ErrorObject consists of a number of contextual attributes to make conveying +// certain error type simpler as per the JSON API specification: +// http://jsonapi.org/format/#error-objects // // error := &jsh.Error{ // Title: "Authentication Failure", @@ -43,7 +26,7 @@ type SendableError interface { // // jsh.Send(w, r, error) // -type Error struct { +type ErrorObject struct { Title string `json:"title"` Detail string `json:"detail"` Status int `json:"status"` @@ -54,7 +37,7 @@ type Error struct { } // Error is a safe for public consumption error message -func (e *Error) Error() string { +func (e *ErrorObject) Error() string { msg := fmt.Sprintf("%s: %s", e.Title, e.Detail) if e.Source.Pointer != "" { msg += fmt.Sprintf("Source.Pointer: %s", e.Source.Pointer) @@ -65,62 +48,62 @@ func (e *Error) Error() string { // Internal is a convenience function that prints out the full error including the // ISE which is useful when debugging, NOT to be used for returning errors to user, // use e.Error() for that -func (e *Error) Internal() string { +func (e *ErrorObject) Internal() string { return fmt.Sprintf("%s ISE: %s", e.Error(), e.ISE) } -// Prepare returns a response containing a prepared error list since the JSON -// API specification requires that errors are returned as a list -func (e *Error) Prepare(req *http.Request, response bool) (*Response, SendableError) { - list := &ErrorList{Errors: []*Error{e}} - return list.Prepare(req, response) -} - -// ErrorList is just a wrapped error array that implements Sendable -type ErrorList struct { - Errors []*Error +// Error is a Sendable type consistenting of one or more error messages. Error +// implements Sendable and as such, when encountered, can simply be sent via +// jsh: +// +// object, err := ParseObject(request) +// if err != nil { +// err := jsh.Send(err, w, request) +// } +type Error struct { + Objects []*ErrorObject } // Error allows ErrorList to conform to the default Go error interface -func (e *ErrorList) Error() string { +func (e *Error) Error() string { err := "Errors: " - for _, e := range e.Errors { - err = strings.Join([]string{err, fmt.Sprintf("%s;", e.Error())}, "\n") + for _, m := range e.Objects { + err = strings.Join([]string{err, fmt.Sprintf("%s;", m.Error())}, "\n") } return err } // Internal prints a formatted error list including ISE's, useful for debugging -func (e *ErrorList) Internal() string { +func (e *Error) Internal() string { err := "Errors:" - for _, e := range e.Errors { - err = strings.Join([]string{err, fmt.Sprintf("%s;", e.Internal())}, "\n") + for _, m := range e.Objects { + err = strings.Join([]string{err, fmt.Sprintf("%s;", m.Internal())}, "\n") } return err } // Add first validates the error, and then appends it to the ErrorList -func (e *ErrorList) Add(newError *Error) *Error { - err := validateError(newError) +func (e *Error) Add(object *ErrorObject) *Error { + err := validateError(object) if err != nil { return err } - e.Errors = append(e.Errors, newError) + e.Objects = append(e.Objects, object) return nil } // Prepare first validates the errors, and then returns an appropriate response -func (e *ErrorList) Prepare(req *http.Request, response bool) (*Response, SendableError) { - if len(e.Errors) == 0 { +func (e *Error) Prepare(req *http.Request, response bool) (*Response, *Error) { + if len(e.Objects) == 0 { return nil, ISE("No errors provided for attempted error response.") } - return &Response{Errors: e.Errors, HTTPStatus: e.Errors[0].Status}, nil + return &Response{Errors: e.Objects, HTTPStatus: e.Objects[0].Status}, nil } // validateError ensures that the error is ready for a response in it's current state -func validateError(err *Error) *Error { +func validateError(err *ErrorObject) *Error { if err.Status < 400 || err.Status > 600 { return ISE(fmt.Sprintf("Invalid HTTP Status for error %+v\n", err)) @@ -131,38 +114,64 @@ func validateError(err *Error) *Error { return nil } +// NewError is a convenience function that makes creating a Sendable Error from a +// Error Object simple. Because ErrorObjects are validated agains the JSON API +// Specification before being added, there is a chance that a ISE error might be +// returned in your new error's place. +func NewError(object *ErrorObject) *Error { + newError := &Error{} + + err := newError.Add(object) + if err != nil { + return err + } + + return newError +} + // ISE is a convenience function for creating a ready-to-go Internal Service Error -// response. As previously mentioned, the Error.ISE field is for logging only, and -// won't be returned to the end user. -func ISE(err string) *Error { - return &Error{ +// response. The message you pass in is set to the ErrorObject.ISE attribute so you +// can gracefully log ISE's internally before sending them +func ISE(internalMessage string) *Error { + return NewError(&ErrorObject{ Title: DefaultErrorTitle, Detail: DefaultErrorDetail, Status: http.StatusInternalServerError, - ISE: err, - } + ISE: internalMessage, + }) } // InputError creates a properly formatted Status 422 error with an appropriate // user facing message, and a Status Pointer to the first attribute that func InputError(attribute string, detail string) *Error { - err := &Error{ + message := &ErrorObject{ Title: "Invalid Attribute", Detail: detail, Status: 422, } // Assign this after the fact, easier to do - err.Source.Pointer = fmt.Sprintf("/data/attributes/%s", strings.ToLower(attribute)) + message.Source.Pointer = fmt.Sprintf("/data/attributes/%s", strings.ToLower(attribute)) + err := &Error{} + err.Add(message) return err } // SpecificationError is used whenever the Client violates the JSON API Spec func SpecificationError(detail string) *Error { - return &Error{ - Title: "API Specification Error", + return NewError(&ErrorObject{ + Title: "JSON API Specification Error", Detail: detail, Status: http.StatusNotAcceptable, - } + }) +} + +// NotFound returns a 404 formatted error +func NotFound(resourceType string, id string) *Error { + return NewError(&ErrorObject{ + Title: "Not Found", + Detail: fmt.Sprintf("No resource of type '%s' exists for ID: %s", resourceType, id), + Status: http.StatusNotFound, + }) } diff --git a/error_test.go b/error_test.go index 4391aa5..187449e 100644 --- a/error_test.go +++ b/error_test.go @@ -16,7 +16,7 @@ func TestError(t *testing.T) { writer := httptest.NewRecorder() request := &http.Request{} - testError := &Error{ + testErrorObject := &ErrorObject{ Status: http.StatusBadRequest, Title: "Fail", Detail: "So badly", @@ -25,73 +25,67 @@ func TestError(t *testing.T) { Convey("->validateError()", func() { Convey("should not fail for a valid Error", func() { - validErr := ISE("Valid Error") - err := validateError(validErr) + err := validateError(testErrorObject) So(err, ShouldBeNil) }) Convey("422 Status Formatting", func() { - testError.Status = 422 + testErrorObject.Status = 422 Convey("should accept a properly formatted 422 error", func() { - testError.Source.Pointer = "/data/attributes/test" - err := validateError(testError) + testErrorObject.Source.Pointer = "/data/attributes/test" + err := validateError(testErrorObject) So(err, ShouldBeNil) }) Convey("should error if Source.Pointer isn't set", func() { - err := validateError(testError) + err := validateError(testErrorObject) So(err, ShouldNotBeNil) }) }) Convey("should fail for an out of range HTTP error status", func() { - testError.Status = http.StatusOK - err := validateError(testError) + testErrorObject.Status = http.StatusOK + err := validateError(testErrorObject) So(err, ShouldNotBeNil) }) }) - Convey("->Send()", func() { - Send(writer, request, testError) - So(writer.Code, ShouldEqual, http.StatusBadRequest) - }) - Convey("Error List Tests", func() { Convey("->Add()", func() { - list := &ErrorList{} + testError := &Error{} Convey("should successfully add a valid error", func() { - err := list.Add(testError) + err := testError.Add(testErrorObject) So(err, ShouldBeNil) - So(len(list.Errors), ShouldEqual, 1) + So(len(testError.Objects), ShouldEqual, 1) }) Convey("should error if validation fails while adding an error", func() { - badError := &Error{ + badError := &ErrorObject{ Title: "Invalid", Detail: "So badly", } - err := list.Add(badError) - So(err.Status, ShouldEqual, 500) - So(list.Errors, ShouldBeEmpty) + err := testError.Add(badError) + So(err.Objects[0].Status, ShouldEqual, 500) + So(testError.Objects, ShouldBeEmpty) }) }) - Convey("->Send(ErrorList)", func() { + Convey("->Send()", func() { - testErrors := &ErrorList{Errors: []*Error{&Error{ + testError := NewError(&ErrorObject{ Status: http.StatusForbidden, Title: "Forbidden", Detail: "Can't Go Here", - }, testError}} + }) Convey("should send a properly formatted JSON error list", func() { - err := Send(writer, request, testErrors) + err := Send(writer, request, testError) So(err, ShouldBeNil) So(writer.Code, ShouldEqual, http.StatusForbidden) diff --git a/list.go b/list.go index 2851dd3..b62fbdd 100644 --- a/list.go +++ b/list.go @@ -6,6 +6,6 @@ import "net/http" type List []*Object // Prepare returns a success status response -func (list List) Prepare(r *http.Request, response bool) (*Response, SendableError) { +func (list List) Prepare(r *http.Request, response bool) (*Response, *Error) { return &Response{Data: list, HTTPStatus: http.StatusOK}, nil } diff --git a/object.go b/object.go index 267b924..d470ea5 100644 --- a/object.go +++ b/object.go @@ -19,10 +19,10 @@ type Object struct { // NewObject prepares a new JSON Object for an API response. Whatever is provided // as attributes will be marshalled to JSON. -func NewObject(id string, objType string, attributes interface{}) (*Object, SendableError) { +func NewObject(id string, resourceType string, attributes interface{}) (*Object, *Error) { object := &Object{ ID: id, - Type: objType, + Type: resourceType, Links: map[string]*Link{}, Relationships: map[string]*Object{}, } @@ -36,8 +36,8 @@ func NewObject(id string, objType string, attributes interface{}) (*Object, Send return object, nil } -// Unmarshal puts an Object's Attributes into a more useful target type defined -// by the user. A correct object type specified must also be provided otherwise +// Unmarshal puts an Object's Attributes into a more useful target resourceType defined +// by the user. A correct object resourceType specified must also be provided otherwise // an error is returned to prevent hard to track down situations. // // Optionally, used https://github.com/go-validator/validator for request input validation. @@ -58,12 +58,12 @@ func NewObject(id string, objType string, attributes interface{}) (*Object, Send // // log errors via error.ISE // jsh.Send(w, r, errors) // } -func (o *Object) Unmarshal(objType string, target interface{}) SendableError { +func (o *Object) Unmarshal(resourceType string, target interface{}) *Error { - if objType != o.Type { + if resourceType != o.Type { return ISE(fmt.Sprintf( "Expected type %s, when converting actual type: %s", - objType, + resourceType, o.Type, )) } @@ -72,7 +72,7 @@ func (o *Object) Unmarshal(objType string, target interface{}) SendableError { if jsonErr != nil { return ISE(fmt.Sprintf( "For type '%s' unable to marshal: %s\nError:%s", - objType, + resourceType, string(o.Attributes), jsonErr.Error(), )) @@ -83,7 +83,7 @@ func (o *Object) Unmarshal(objType string, target interface{}) SendableError { // Marshal allows you to load a modified payload back into an object to preserve // all of the data it has -func (o *Object) Marshal(attributes interface{}) SendableError { +func (o *Object) Marshal(attributes interface{}) *Error { raw, err := json.MarshalIndent(attributes, "", " ") if err != nil { return ISE(fmt.Sprintf("Error marshaling attrs while creating a new JSON Object: %s", err)) @@ -95,7 +95,7 @@ func (o *Object) Marshal(attributes interface{}) SendableError { // Prepare creates a new JSON single object response with an appropriate HTTP status // to match the request method type. -func (o *Object) Prepare(r *http.Request, response bool) (*Response, SendableError) { +func (o *Object) Prepare(r *http.Request, response bool) (*Response, *Error) { if o.ID == "" { @@ -132,26 +132,27 @@ func (o *Object) Prepare(r *http.Request, response bool) (*Response, SendableErr // validateInput runs go-validator on each attribute on the struct and returns all // errors that it picks up -func validateInput(target interface{}) SendableError { +func validateInput(target interface{}) *Error { _, validationError := govalidator.ValidateStruct(target) if validationError != nil { - manyErrors, isType := validationError.(govalidator.Errors) + errorList, isType := validationError.(govalidator.Errors) if isType { - list := &ErrorList{} - for _, err := range manyErrors.Errors() { - singleErr, _ := err.(govalidator.Error) - list.Add(InputError(singleErr.Name, singleErr.Err.Error())) - } - // Don't send back a list if it's just a single error, govalidator - // seems to always return an error Array even for a single error - if len(list.Errors) == 1 { - return list.Errors[0] + err := &Error{} + for _, singleErr := range errorList.Errors() { + + // parse out validation error + goValidErr, _ := singleErr.(govalidator.Error) + inputErr := InputError(goValidErr.Name, goValidErr.Err.Error()) + + // gross way to do this, but will require a refactor probably + // to achieve something more elegant + err.Add(inputErr.Objects[0]) } - return list + return err } } diff --git a/object_test.go b/object_test.go index 6099afd..2bc5ba6 100644 --- a/object_test.go +++ b/object_test.go @@ -73,12 +73,9 @@ func TestObject(t *testing.T) { Foo string `valid:"ipv4,required" json:"foo"` }{} - unmarshalErr := testObject.Unmarshal("testObject", &testValidation) - So(unmarshalErr, ShouldNotBeNil) - - e, ok := unmarshalErr.(*Error) - So(ok, ShouldBeTrue) - So(e.Source.Pointer, ShouldEqual, "/data/attributes/foo") + err := testObject.Unmarshal("testObject", &testValidation) + So(err, ShouldNotBeNil) + So(err.Objects[0].Source.Pointer, ShouldEqual, "/data/attributes/foo") }) Convey("should return a 422 Error correctly for multiple validation failures", func() { @@ -94,15 +91,12 @@ func TestObject(t *testing.T) { Baz string `valid:"alpha,required" json:"baz"` }{} - unmarshalErr := testManyObject.Unmarshal("testObject", &testManyValidations) - So(unmarshalErr, ShouldNotBeNil) + err := testManyObject.Unmarshal("testObject", &testManyValidations) + So(err, ShouldNotBeNil) - errorList, ok := unmarshalErr.(*ErrorList) - So(ok, ShouldBeTrue) - So(errorList.Errors[0].Source.Pointer, ShouldEqual, "/data/attributes/foo") - So(errorList.Errors[1].Source.Pointer, ShouldEqual, "/data/attributes/baz") + So(err.Objects[0].Source.Pointer, ShouldEqual, "/data/attributes/foo") + So(err.Objects[1].Source.Pointer, ShouldEqual, "/data/attributes/baz") }) - }) }) diff --git a/parser.go b/parser.go index 39833e5..69cef14 100644 --- a/parser.go +++ b/parser.go @@ -46,7 +46,7 @@ const ( // // err := jsh.Send(w, r, object) // } -func ParseObject(r *http.Request) (*Object, SendableError) { +func ParseObject(r *http.Request) (*Object, *Error) { object, err := buildParser(r).GetObject() if err != nil { @@ -62,7 +62,7 @@ func ParseObject(r *http.Request) (*Object, SendableError) { // ParseList validates the HTTP request and returns a resulting list of objects // parsed from the request Body. Use just like ParseObject. -func ParseList(r *http.Request) (List, SendableError) { +func ParseList(r *http.Request) (List, *Error) { return buildParser(r).GetList() } @@ -84,7 +84,7 @@ func buildParser(request *http.Request) *Parser { } // GetObject returns a single JSON data object from the parser -func (p *Parser) GetObject() (*Object, SendableError) { +func (p *Parser) GetObject() (*Object, *Error) { byteData, loadErr := prepareJSON(p.Headers, p.Payload) if loadErr != nil { return nil, loadErr @@ -113,7 +113,7 @@ func (p *Parser) GetObject() (*Object, SendableError) { } // GetList returns a JSON data list from the parser -func (p *Parser) GetList() (List, SendableError) { +func (p *Parser) GetList() (List, *Error) { byteData, loadErr := prepareJSON(p.Headers, p.Payload) if loadErr != nil { return nil, loadErr @@ -147,7 +147,7 @@ func (p *Parser) GetList() (List, SendableError) { // prepareJSON ensures that the provide headers are JSON API compatible and then // reads and closes the closer -func prepareJSON(headers http.Header, closer io.ReadCloser) ([]byte, SendableError) { +func prepareJSON(headers http.Header, closer io.ReadCloser) ([]byte, *Error) { defer closeReader(closer) validationErr := validateHeaders(headers) @@ -170,7 +170,7 @@ func closeReader(reader io.ReadCloser) { } } -func validateHeaders(headers http.Header) SendableError { +func validateHeaders(headers http.Header) *Error { reqContentType := headers.Get("Content-Type") if reqContentType != ContentType { diff --git a/parser_test.go b/parser_test.go index 7c67803..118a716 100644 --- a/parser_test.go +++ b/parser_test.go @@ -13,15 +13,13 @@ func TestParsing(t *testing.T) { Convey("Parse Tests", t, func() { Convey("->validateHeaders()", func() { - req, err := http.NewRequest("GET", "", nil) - So(err, ShouldBeNil) + req, reqErr := http.NewRequest("GET", "", nil) + So(reqErr, ShouldBeNil) req.Header.Set("Content-Type", "jpeg") - err = validateHeaders(req.Header) + err := validateHeaders(req.Header) So(err, ShouldNotBeNil) - - singleErr := err.(*Error) - So(singleErr.Status, ShouldEqual, http.StatusNotAcceptable) + So(err.Objects[0].Status, ShouldEqual, http.StatusNotAcceptable) }) Convey("->prepareJSON()", func() { @@ -59,11 +57,8 @@ func TestParsing(t *testing.T) { _, err := ParseObject(req) So(err, ShouldNotBeNil) - - vErr, ok := err.(*Error) - So(ok, ShouldBeTrue) - So(vErr.Status, ShouldEqual, 422) - So(vErr.Source.Pointer, ShouldEqual, "/data/attributes/type") + So(err.Objects[0].Status, ShouldEqual, 422) + So(err.Objects[0].Source.Pointer, ShouldEqual, "/data/attributes/type") }) Convey("should accept empty ID only for POST", func() { @@ -119,11 +114,8 @@ func TestParsing(t *testing.T) { _, err := ParseList(req) So(err, ShouldNotBeNil) - - vErr, ok := err.(*Error) - So(ok, ShouldBeTrue) - So(vErr.Status, ShouldEqual, 422) - So(vErr.Source.Pointer, ShouldEqual, "/data/attributes/id") + So(err.Objects[0].Status, ShouldEqual, 422) + So(err.Objects[0].Source.Pointer, ShouldEqual, "/data/attributes/id") }) }) }) diff --git a/response.go b/response.go index 4d665a3..b7f7969 100644 --- a/response.go +++ b/response.go @@ -16,7 +16,7 @@ const JSONAPIVersion = "1.1" type Sendable interface { // Prepare allows a "raw" response type to perform specification assertions, // and format any data before it is actually send - Prepare(r *http.Request, response bool) (*Response, SendableError) + Prepare(r *http.Request, response bool) (*Response, *Error) } // Response represents the top level json format of incoming requests @@ -41,7 +41,7 @@ type Response struct { } // Validate checks JSON Spec for the top level JSON document -func (r *Response) Validate() SendableError { +func (r *Response) Validate() *Error { if !r.empty { if r.Errors == nil && r.Data == nil { @@ -76,7 +76,7 @@ func Send(w http.ResponseWriter, r *http.Request, payload Sendable) *Error { response, err = err.Prepare(r, true) if err != nil { http.Error(w, DefaultErrorTitle, http.StatusInternalServerError) - return err.(*Error) + return err } } @@ -101,7 +101,7 @@ func SendResponse(w http.ResponseWriter, r *http.Request, response *Response) *E // If we ever hit this, something seriously wrong has happened if prepErr != nil { http.Error(w, DefaultErrorTitle, http.StatusInternalServerError) - return prepErr.(*Error) + return prepErr } // if we didn't error out, make this the new response @@ -120,7 +120,7 @@ func SendResponse(w http.ResponseWriter, r *http.Request, response *Response) *E w.Write(content) if err != nil { - return err.(*Error) + return err } return nil @@ -137,6 +137,6 @@ func Ok() *OkResponse { } // Prepare turns OkResponse into the normalized Response type -func (o *OkResponse) Prepare(r *http.Request, response bool) (*Response, SendableError) { +func (o *OkResponse) Prepare(r *http.Request, response bool) (*Response, *Error) { return &Response{HTTPStatus: http.StatusOK, empty: true}, nil }