Skip to content

Commit 3cea029

Browse files
author
Derek Dowling
committed
Removing SendableError, separating ErrorObject from error which is now a list
1 parent 391d173 commit 3cea029

File tree

8 files changed

+136
-146
lines changed

8 files changed

+136
-146
lines changed

error.go

+66-57
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,9 @@ var DefaultErrorDetail = "Request failed, something went wrong."
1414
// DefaultTitle can be customized to provide a more customized ISE Title
1515
var DefaultErrorTitle = "Internal Server Error"
1616

17-
// SendableError conforms to a standard error format for logging, but can also
18-
// be sent as a JSON response
19-
type SendableError interface {
20-
Sendable
21-
// Error returns a safe for user error message
22-
Error() string
23-
// Internal returns a fully formatted error including any sensitive debugging
24-
// information contained in the ISE field. Really only useful when logging an
25-
// outbound response
26-
Internal() string
27-
}
28-
29-
// Error represents a JSON Specification Error. Error.Source.Pointer is used in 422
30-
// status responses to indicate validation errors on a JSON Object attribute.
31-
//
32-
// ISE (internal server error) captures the server error internally to help with
33-
// logging/troubleshooting, but is never returned in a response.
34-
//
35-
// Once a jsh.Error is returned, and you have logged/handled it accordingly, you
36-
// can simply return it using jsh.Send():
17+
// ErrorObject consists of a number of contextual attributes to make conveying
18+
// certain error type simpler as per the JSON API specification:
19+
// http://jsonapi.org/format/#error-objects
3720
//
3821
// error := &jsh.Error{
3922
// Title: "Authentication Failure",
@@ -43,7 +26,7 @@ type SendableError interface {
4326
//
4427
// jsh.Send(w, r, error)
4528
//
46-
type Error struct {
29+
type ErrorObject struct {
4730
Title string `json:"title"`
4831
Detail string `json:"detail"`
4932
Status int `json:"status"`
@@ -54,7 +37,7 @@ type Error struct {
5437
}
5538

5639
// Error is a safe for public consumption error message
57-
func (e *Error) Error() string {
40+
func (e *ErrorObject) Error() string {
5841
msg := fmt.Sprintf("%s: %s", e.Title, e.Detail)
5942
if e.Source.Pointer != "" {
6043
msg += fmt.Sprintf("Source.Pointer: %s", e.Source.Pointer)
@@ -65,62 +48,62 @@ func (e *Error) Error() string {
6548
// Internal is a convenience function that prints out the full error including the
6649
// ISE which is useful when debugging, NOT to be used for returning errors to user,
6750
// use e.Error() for that
68-
func (e *Error) Internal() string {
51+
func (e *ErrorObject) Internal() string {
6952
return fmt.Sprintf("%s ISE: %s", e.Error(), e.ISE)
7053
}
7154

72-
// Prepare returns a response containing a prepared error list since the JSON
73-
// API specification requires that errors are returned as a list
74-
func (e *Error) Prepare(req *http.Request, response bool) (*Response, SendableError) {
75-
list := &ErrorList{Errors: []*Error{e}}
76-
return list.Prepare(req, response)
77-
}
78-
79-
// ErrorList is just a wrapped error array that implements Sendable
80-
type ErrorList struct {
81-
Errors []*Error
55+
// Error is a Sendable type consistenting of one or more error messages. Error
56+
// implements Sendable and as such, when encountered, can simply be sent via
57+
// jsh:
58+
//
59+
// object, err := ParseObject(request)
60+
// if err != nil {
61+
// err := jsh.Send(err, w, request)
62+
// }
63+
type Error struct {
64+
Objects []*ErrorObject
8265
}
8366

8467
// Error allows ErrorList to conform to the default Go error interface
85-
func (e *ErrorList) Error() string {
68+
func (e *Error) Error() string {
8669
err := "Errors: "
87-
for _, e := range e.Errors {
88-
err = strings.Join([]string{err, fmt.Sprintf("%s;", e.Error())}, "\n")
70+
for _, m := range e.Objects {
71+
err = strings.Join([]string{err, fmt.Sprintf("%s;", m.Error())}, "\n")
8972
}
9073
return err
9174
}
9275

9376
// Internal prints a formatted error list including ISE's, useful for debugging
94-
func (e *ErrorList) Internal() string {
77+
func (e *Error) Internal() string {
9578
err := "Errors:"
96-
for _, e := range e.Errors {
97-
err = strings.Join([]string{err, fmt.Sprintf("%s;", e.Internal())}, "\n")
79+
for _, m := range e.Objects {
80+
err = strings.Join([]string{err, fmt.Sprintf("%s;", m.Internal())}, "\n")
9881
}
9982
return err
10083
}
10184

10285
// Add first validates the error, and then appends it to the ErrorList
103-
func (e *ErrorList) Add(newError *Error) *Error {
104-
err := validateError(newError)
86+
func (e *Error) Add(object *ErrorObject) *Error {
87+
err := validateError(object)
10588
if err != nil {
10689
return err
10790
}
10891

109-
e.Errors = append(e.Errors, newError)
92+
e.Objects = append(e.Objects, object)
11093
return nil
11194
}
11295

11396
// Prepare first validates the errors, and then returns an appropriate response
114-
func (e *ErrorList) Prepare(req *http.Request, response bool) (*Response, SendableError) {
115-
if len(e.Errors) == 0 {
97+
func (e *Error) Prepare(req *http.Request, response bool) (*Response, *Error) {
98+
if len(e.Objects) == 0 {
11699
return nil, ISE("No errors provided for attempted error response.")
117100
}
118101

119-
return &Response{Errors: e.Errors, HTTPStatus: e.Errors[0].Status}, nil
102+
return &Response{Errors: e.Objects, HTTPStatus: e.Objects[0].Status}, nil
120103
}
121104

122105
// validateError ensures that the error is ready for a response in it's current state
123-
func validateError(err *Error) *Error {
106+
func validateError(err *ErrorObject) *Error {
124107

125108
if err.Status < 400 || err.Status > 600 {
126109
return ISE(fmt.Sprintf("Invalid HTTP Status for error %+v\n", err))
@@ -131,38 +114,64 @@ func validateError(err *Error) *Error {
131114
return nil
132115
}
133116

117+
// NewError is a convenience function that makes creating a Sendable Error from a
118+
// Error Object simple. Because ErrorObjects are validated agains the JSON API
119+
// Specification before being added, there is a chance that a ISE error might be
120+
// returned in your new error's place.
121+
func NewError(object *ErrorObject) *Error {
122+
newError := &Error{}
123+
124+
err := newError.Add(object)
125+
if err != nil {
126+
return err
127+
}
128+
129+
return newError
130+
}
131+
134132
// ISE is a convenience function for creating a ready-to-go Internal Service Error
135-
// response. As previously mentioned, the Error.ISE field is for logging only, and
136-
// won't be returned to the end user.
137-
func ISE(err string) *Error {
138-
return &Error{
133+
// response. The message you pass in is set to the ErrorObject.ISE attribute so you
134+
// can gracefully log ISE's internally before sending them
135+
func ISE(internalMessage string) *Error {
136+
return NewError(&ErrorObject{
139137
Title: DefaultErrorTitle,
140138
Detail: DefaultErrorDetail,
141139
Status: http.StatusInternalServerError,
142-
ISE: err,
143-
}
140+
ISE: internalMessage,
141+
})
144142
}
145143

146144
// InputError creates a properly formatted Status 422 error with an appropriate
147145
// user facing message, and a Status Pointer to the first attribute that
148146
func InputError(attribute string, detail string) *Error {
149-
err := &Error{
147+
message := &ErrorObject{
150148
Title: "Invalid Attribute",
151149
Detail: detail,
152150
Status: 422,
153151
}
154152

155153
// Assign this after the fact, easier to do
156-
err.Source.Pointer = fmt.Sprintf("/data/attributes/%s", strings.ToLower(attribute))
154+
message.Source.Pointer = fmt.Sprintf("/data/attributes/%s", strings.ToLower(attribute))
157155

156+
err := &Error{}
157+
err.Add(message)
158158
return err
159159
}
160160

161161
// SpecificationError is used whenever the Client violates the JSON API Spec
162162
func SpecificationError(detail string) *Error {
163-
return &Error{
164-
Title: "API Specification Error",
163+
return NewError(&ErrorObject{
164+
Title: "JSON API Specification Error",
165165
Detail: detail,
166166
Status: http.StatusNotAcceptable,
167-
}
167+
})
168+
}
169+
170+
// NotFound returns a 404 formatted error
171+
func NotFound(resourceType string, id string) *Error {
172+
return NewError(&ErrorObject{
173+
Title: "Not Found",
174+
Detail: fmt.Sprintf("No resource of type '%s' exists for ID: %s", resourceType, id),
175+
Status: http.StatusNotFound,
176+
})
168177
}

error_test.go

+19-25
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestError(t *testing.T) {
1616
writer := httptest.NewRecorder()
1717
request := &http.Request{}
1818

19-
testError := &Error{
19+
testErrorObject := &ErrorObject{
2020
Status: http.StatusBadRequest,
2121
Title: "Fail",
2222
Detail: "So badly",
@@ -25,73 +25,67 @@ func TestError(t *testing.T) {
2525
Convey("->validateError()", func() {
2626

2727
Convey("should not fail for a valid Error", func() {
28-
validErr := ISE("Valid Error")
29-
err := validateError(validErr)
28+
err := validateError(testErrorObject)
3029
So(err, ShouldBeNil)
3130
})
3231

3332
Convey("422 Status Formatting", func() {
3433

35-
testError.Status = 422
34+
testErrorObject.Status = 422
3635

3736
Convey("should accept a properly formatted 422 error", func() {
38-
testError.Source.Pointer = "/data/attributes/test"
39-
err := validateError(testError)
37+
testErrorObject.Source.Pointer = "/data/attributes/test"
38+
err := validateError(testErrorObject)
4039
So(err, ShouldBeNil)
4140
})
4241

4342
Convey("should error if Source.Pointer isn't set", func() {
44-
err := validateError(testError)
43+
err := validateError(testErrorObject)
4544
So(err, ShouldNotBeNil)
4645
})
4746
})
4847

4948
Convey("should fail for an out of range HTTP error status", func() {
50-
testError.Status = http.StatusOK
51-
err := validateError(testError)
49+
testErrorObject.Status = http.StatusOK
50+
err := validateError(testErrorObject)
5251
So(err, ShouldNotBeNil)
5352
})
5453
})
5554

56-
Convey("->Send()", func() {
57-
Send(writer, request, testError)
58-
So(writer.Code, ShouldEqual, http.StatusBadRequest)
59-
})
60-
6155
Convey("Error List Tests", func() {
6256

6357
Convey("->Add()", func() {
6458

65-
list := &ErrorList{}
59+
testError := &Error{}
6660

6761
Convey("should successfully add a valid error", func() {
68-
err := list.Add(testError)
62+
err := testError.Add(testErrorObject)
6963
So(err, ShouldBeNil)
70-
So(len(list.Errors), ShouldEqual, 1)
64+
So(len(testError.Objects), ShouldEqual, 1)
7165
})
7266

7367
Convey("should error if validation fails while adding an error", func() {
74-
badError := &Error{
68+
badError := &ErrorObject{
7569
Title: "Invalid",
7670
Detail: "So badly",
7771
}
7872

79-
err := list.Add(badError)
80-
So(err.Status, ShouldEqual, 500)
81-
So(list.Errors, ShouldBeEmpty)
73+
err := testError.Add(badError)
74+
So(err.Objects[0].Status, ShouldEqual, 500)
75+
So(testError.Objects, ShouldBeEmpty)
8276
})
8377
})
8478

85-
Convey("->Send(ErrorList)", func() {
79+
Convey("->Send()", func() {
8680

87-
testErrors := &ErrorList{Errors: []*Error{&Error{
81+
testError := NewError(&ErrorObject{
8882
Status: http.StatusForbidden,
8983
Title: "Forbidden",
9084
Detail: "Can't Go Here",
91-
}, testError}}
85+
})
9286

9387
Convey("should send a properly formatted JSON error list", func() {
94-
err := Send(writer, request, testErrors)
88+
err := Send(writer, request, testError)
9589
So(err, ShouldBeNil)
9690
So(writer.Code, ShouldEqual, http.StatusForbidden)
9791

list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import "net/http"
66
type List []*Object
77

88
// Prepare returns a success status response
9-
func (list List) Prepare(r *http.Request, response bool) (*Response, SendableError) {
9+
func (list List) Prepare(r *http.Request, response bool) (*Response, *Error) {
1010
return &Response{Data: list, HTTPStatus: http.StatusOK}, nil
1111
}

0 commit comments

Comments
 (0)