Skip to content

Commit

Permalink
Adding support for an 'Ok' response type
Browse files Browse the repository at this point in the history
  • Loading branch information
Derek Dowling committed Dec 13, 2015
1 parent 19686b8 commit 7494127
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 34 deletions.
3 changes: 2 additions & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type SendableError interface {
// 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
// information contained in the ISE field. Really only useful when logging an
// outbound response
Internal() string
}

Expand Down
4 changes: 2 additions & 2 deletions list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func TestList(t *testing.T) {
req, reqErr := testRequest(writer.Body.Bytes())
So(reqErr, ShouldBeNil)

responseList, err := ParseList(req)
So(err, ShouldBeNil)
responseList, parseErr := ParseList(req)
So(parseErr, ShouldBeNil)
So(len(responseList), ShouldEqual, 1)
})
})
Expand Down
96 changes: 65 additions & 31 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,42 @@ type Sendable interface {
}

// Response represents the top level json format of incoming requests
// and outgoing responses
// and outgoing responses. Refer to the JSON API Specification for a full descriptor
// of each attribute: http://jsonapi.org/format/#document-structure
type Response struct {
HTTPStatus int `json:"-"`
Data interface{} `json:"data,omitempty"`
Errors interface{} `json:"errors,omitempty"`
Meta interface{} `json:"meta,omitempty"`
Links *Link `json:"links,omitempty"`
Included *List `json:"included,omitempty"`
JSONAPI struct {
Data interface{} `json:"data,omitempty"`
Errors interface{} `json:"errors,omitempty"`
Meta interface{} `json:"meta,omitempty"`
Links *Link `json:"links,omitempty"`
Included *List `json:"included,omitempty"`
JSONAPI struct {
Version string `json:"version"`
} `json:"jsonapi"`
// Custom HTTPStatus attribute to make it simpler to define expected responses
// for a given context
HTTPStatus int `json:"-"`
// empty is used to signify that the response shouldn't contain a json payload
// in the case that we only want to return an HTTP Status Code in order to bypass
// validation steps
empty bool
}

// Validate checks JSON Spec for the top level JSON document
func (r *Response) Validate() SendableError {

if r.Errors == nil && r.Data == nil {
return ISE("Both `errors` and `data` cannot be blank for a JSON response")
}
if r.Errors != nil && r.Data != nil {
return ISE("Both `errors` and `data` cannot be set for a JSON response")
}
if r.Data == nil && r.Included != nil {
return ISE("'included' should only be set for a response if 'data' is as well")
if !r.empty {
if r.Errors == nil && r.Data == nil {
return ISE("Both `errors` and `data` cannot be blank for a JSON response")
}
if r.Errors != nil && r.Data != nil {
return ISE("Both `errors` and `data` cannot be set for a JSON response")
}
if r.Data == nil && r.Included != nil {
return ISE("'included' should only be set for a response if 'data' is as well")
}
}
if r.HTTPStatus < 100 || r.HTTPStatus > 600 {
return ISE("Response HTTP Status must be of a valid range")
return ISE("Response HTTP Status is outside of valid range")
}

// probably not the best place for this, but...
Expand All @@ -57,52 +66,77 @@ func (r *Response) Validate() SendableError {

// Send will return a JSON payload to the requestor. If the payload response validation
// fails, it will send an appropriate error to the requestor and will return the error
func Send(w http.ResponseWriter, r *http.Request, payload Sendable) error {
func Send(w http.ResponseWriter, r *http.Request, payload Sendable) *Error {

response, err := payload.Prepare(r, true)
if err != nil {

// use the prepared error as the new response, unless something went horribly
// wrong
response, err = err.Prepare(r, true)
if err != nil {
http.Error(w, DefaultErrorTitle, http.StatusInternalServerError)
return fmt.Errorf("Error preparing JSH error: %s", err.Error())
return err.(*Error)
}

return fmt.Errorf("Error preparing JSON payload: %s", err.Error())
}

return SendResponse(w, r, response)
}

// SendResponse handles sending a fully packaged JSON Response allows API consumers
// to more manually build their Responses in case they want to send Meta, Links, etc
// The function will always, send but will return the last error it encountered
// to help with debugging
func SendResponse(w http.ResponseWriter, r *http.Request, response *Response) error {
// SendResponse handles sending a fully packaged JSON Response which is useful if you
// require manual, or custom, validation when building a Response:
//
// response := &jsh.Response{
// HTTPStatus: http.StatusAccepted,
// }
//
// The function will always send but will return the last error it encountered
// to help with debugging in the event of an ISE.
func SendResponse(w http.ResponseWriter, r *http.Request, response *Response) *Error {

err := response.Validate()
if err != nil {
response, err = err.Prepare(r, true)
errResp, prepErr := err.Prepare(r, true)

// If we ever hit this, something seriously wrong has happened
if err != nil {
if prepErr != nil {
http.Error(w, DefaultErrorTitle, http.StatusInternalServerError)
return fmt.Errorf("Error preparing JSH error: %s", err.Error())
return prepErr.(*Error)
}

return fmt.Errorf("Response validation error: %s", err.Error())
// if we didn't error out, make this the new response
response = errResp
}

content, jsonErr := json.MarshalIndent(response, "", " ")
if jsonErr != nil {
http.Error(w, DefaultErrorTitle, http.StatusInternalServerError)
return fmt.Errorf("Unable to marshal JSON payload: %s", jsonErr.Error())
return ISE(fmt.Sprintf("Unable to marshal JSON payload: %s", jsonErr.Error()))
}

w.Header().Add("Content-Type", ContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
w.WriteHeader(response.HTTPStatus)
w.Write(content)

if err != nil {
return err.(*Error)
}

return nil
}

// OkResponse fulfills the Sendable interface for a simple success response
type OkResponse struct{}

// Ok makes it simple to return a 200 OK response via jsh:
//
// jsh.Send(w, r, jsh.Ok())
func Ok() *OkResponse {
return &OkResponse{}
}

// Prepare turns OkResponse into the normalized Response type
func (o *OkResponse) Prepare(r *http.Request, response bool) (*Response, SendableError) {
return &Response{HTTPStatus: http.StatusOK, empty: true}, nil
}
7 changes: 7 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,12 @@ func TestSend(t *testing.T) {
})
})
})

Convey("->Ok()", func() {
ok := Ok()
err := Send(writer, request, ok)
So(err, ShouldBeNil)
So(writer.Code, ShouldEqual, http.StatusOK)
})
})
}

0 comments on commit 7494127

Please sign in to comment.