Skip to content

Commit

Permalink
Added logic to handle multi error response with respective aide
Browse files Browse the repository at this point in the history
  • Loading branch information
Leon Silcott committed Sep 10, 2021
1 parent b990709 commit 2046955
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 22 deletions.
23 changes: 23 additions & 0 deletions examples/example_simple_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type user struct {

var baseManifest []reply.ErrorManifest = []reply.ErrorManifest{
{"example-404-error": reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound}},
{"example-name-validation-error": reply.ErrorManifestItem{Title: "Validation Error", Detail: "The name provided does not meet validation requirements", StatusCode: http.StatusBadRequest}},
{"example-dob-validation-error": reply.ErrorManifestItem{Title: "Validation Error", Detail: "Check your DoB, and try again.", Code: "100YT", StatusCode: http.StatusBadRequest}},
}

var replier *reply.Replier = reply.NewReplier(baseManifest)
Expand All @@ -99,6 +101,17 @@ func simpleUsersAPINotFoundHandler(w http.ResponseWriter, r *http.Request) {
})
}

func simpleUsersAPIMultiErrorHandler(w http.ResponseWriter, r *http.Request) {

// Do something with a server
serverErrs := []error{errors.New("example-dob-validation-error"), errors.New("example-name-validation-error")}

_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Errors: serverErrs,
})
}

func simpleUsersAPIHandler(w http.ResponseWriter, r *http.Request) {

mockedQueriedUsers := []user{
Expand Down Expand Up @@ -164,6 +177,14 @@ func simpleUsersAPINotFoundCustomReplierHandler(w http.ResponseWriter, r *http.R
//////////////////////////////
//// Handlers Using Aides ////

func simpleUsersAPIMultiErrorUsingAideHandler(w http.ResponseWriter, r *http.Request) {

// Do something with a server
serverErrs := []error{errors.New("example-dob-validation-error"), errors.New("example-name-validation-error")}

_ = replier.NewHTTPMultiErrorResponse(w, serverErrs)
}

func simpleUsersAPINotFoundUsingAideHandler(w http.ResponseWriter, r *http.Request) {

// Do something with a server
Expand Down Expand Up @@ -225,13 +246,15 @@ func simpleUsersAPINotFoundCustomReplierUsingAideHandler(w http.ResponseWriter,
func handleRequest() {
var port string = ":8081"

http.HandleFunc("/errors", simpleUsersAPIMultiErrorHandler)
http.HandleFunc("/users", simpleUsersAPIHandler)
http.HandleFunc("/users/3", simpleUsersAPINotFoundHandler)
http.HandleFunc("/users/4", simpleUsersAPINoManifestEntryHandler)
http.HandleFunc("/tokens/refresh", simpleTokensAPIHandler)
http.HandleFunc("/defaults/1", simpleAPIDefaultResponseHandler)
http.HandleFunc("/custom/users/3", simpleUsersAPINotFoundCustomReplierHandler)

http.HandleFunc("/aides/errors", simpleUsersAPIMultiErrorUsingAideHandler)
http.HandleFunc("/aides/users", simpleUsersAPIUsingAideHandler)
http.HandleFunc("/aides/users/3", simpleUsersAPINotFoundUsingAideHandler)
http.HandleFunc("/aides/users/4", simpleUsersAPINoManifestEntryUsingAideHandler)
Expand Down
31 changes: 31 additions & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Reply Release Notes

## [v1.0.0-alpha.1](https://github.com/ooaklee/reply/releases/tag/v1.0.0-alpha.1)
2021-09-10

* Added new aide `NewHTTPMultiErrorResponse` to support multiple error response
* Updated `example simple api` to use the new aide `NewHTTPMultiErrorResponse`
* Added logic to handle/ create multiple error response
* Refactor code to make it more readable with new logic

## [v1.0.0-alpha](https://github.com/ooaklee/reply/releases/tag/v1.0.0-alpha)
2021-09-09

* Update top-level response members from `meta`, `status` and `data` **->** `meta`, `errors` and `data`
* Updated underlying logic to how an error is handled
* Updated `Manifest Error Item` attributes

## [v0.2.0](https://github.com/ooaklee/reply/releases/tag/v0.2.0)
2021-09-04

* Fixed bug in logic for merging error manifests
* Added helper functions (aides) to help users more efficiently use the library

## [v0.2.0-alpha.1](https://github.com/ooaklee/reply/releases/tag/v0.2.0-alpha.1)
2021-09-04

* Fixed bug in logic for merging error manifests

## [v0.2.0-alpha](https://github.com/ooaklee/reply/releases/tag/v0.2.0-alpha)
2021-09-03

* Initial logic for helper functions (`aides`) to help users more efficiently use the library

## [v0.1.1](https://github.com/ooaklee/reply/releases/tag/v0.1.1)
2021-08-31

Expand Down
139 changes: 121 additions & 18 deletions replier.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"log"
"net/http"
"strconv"
)

// TransferObjectError outlines expected methods of a transfer object error
Expand Down Expand Up @@ -49,6 +50,9 @@ const (

// defaultStatusCode returns default response status code
defaultStatusCode = http.StatusOK

// defaultErrorsStatusCode returns default status code for errors
defaultErrorsStatusCode = http.StatusBadRequest
)

// Option used to build on top of default features
Expand All @@ -70,6 +74,7 @@ type NewResponseRequest struct {
StatusCode int
Message string
Error error
Errors []error
AccessToken string
RefreshToken string
}
Expand Down Expand Up @@ -124,6 +129,11 @@ func (r *Replier) NewHTTPResponse(response *NewResponseRequest) error {

r.setUniversalAttributes(response.Writer, response.Headers, response.Meta, response.StatusCode)

// Manage response for multi errors
if len(response.Errors) > 0 {
return r.generateMultiErrorResponse(response.Errors)
}

// Manage response for error
if response.Error != nil {
return r.generateErrorResponse(response.Error)
Expand Down Expand Up @@ -164,36 +174,61 @@ func (r *Replier) generateTokenResponse(accessToken, refreshToken string) error
return sendHTTPResponse(r.transferObject.GetWriter(), r.transferObject)
}

// generateMultiErrorResponse generates error response for multiple
// errors
//
// NOTE - If at anytime one of the errors return a 5XX error manifest item,
// only the 5XX error will be returned
func (r *Replier) generateMultiErrorResponse(errs []error) error {

transferObjectErrors := []TransferObjectError{}

for _, err := range errs {
manifestItem := r.getErrorManifestItem(err)

if is5xx(manifestItem.StatusCode) {
return r.sendHTTPErrorsResponse(manifestItem.StatusCode, append(
[]TransferObjectError{},
ConvertErrorItemToTransferObjectError(manifestItem)))
}

transferObjectErrors = append(transferObjectErrors, ConvertErrorItemToTransferObjectError(manifestItem))
}

statusCode := getAppropiateStatusCodeOrDefault(transferObjectErrors)

return r.sendHTTPErrorsResponse(statusCode, transferObjectErrors)
}

// generateErrorResponse generates correct error response based on passed
// error
func (r *Replier) generateErrorResponse(err error) error {
manifestItem, ok := r.errorManifest[err.Error()]
if !ok {
manifestItem = getInternalServertErrorManifestItem()
log.Printf("reply/error-response: failed to find error manifest item for %v", err)
}
manifestItem := r.getErrorManifestItem(err)

transferObjectErrors := append([]TransferObjectError{}, ConvertErrorItemToTransferObjectError(manifestItem))

// Overwrite status code
r.transferObject.SetStatusCode(manifestItem.StatusCode)
return r.sendHTTPErrorsResponse(manifestItem.StatusCode, transferObjectErrors)
}

// sendHTTPErrorsResponse handles setting status code and transfer object errors before
// attempting to send response
func (r *Replier) sendHTTPErrorsResponse(statusCode int, transferObjectErrors []TransferObjectError) error {
r.transferObject.SetStatusCode(statusCode)
r.transferObject.SetErrors(transferObjectErrors)

return sendHTTPResponse(r.transferObject.GetWriter(), r.transferObject)
}

// ConvertErrorItemToTransferObjectError converts manifest item to valid
// transfer object error
func ConvertErrorItemToTransferObjectError(errorItem ErrorManifestItem) TransferObjectError {
convertedError := Error{}
convertedError.SetTitle(errorItem.Title)
convertedError.SetDetail(errorItem.Detail)
convertedError.SetAbout(errorItem.About)
convertedError.SetCode(errorItem.Code)
convertedError.SetStatusCode(errorItem.StatusCode)
convertedError.SetMeta(errorItem.Meta)
// getErrorManifestItem returns the corresponding manifest Item if found,
// otherwise the internal server error is returned
func (r *Replier) getErrorManifestItem(err error) ErrorManifestItem {
manifestItem, ok := r.errorManifest[err.Error()]
if !ok {
manifestItem = getInternalServertErrorManifestItem()
log.Printf("reply/error-response: failed to find error manifest item for %v", err)
}

return &convertedError
return manifestItem
}

// setUniversalAttributes sets the attributes that are common across all
Expand Down Expand Up @@ -234,6 +269,49 @@ func (r *Replier) setHeaders(h map[string]string) {
}
}

// ConvertErrorItemToTransferObjectError converts manifest item to valid
// transfer object error
func ConvertErrorItemToTransferObjectError(errorItem ErrorManifestItem) TransferObjectError {
convertedError := Error{}
convertedError.SetTitle(errorItem.Title)
convertedError.SetDetail(errorItem.Detail)
convertedError.SetAbout(errorItem.About)
convertedError.SetCode(errorItem.Code)
convertedError.SetStatusCode(errorItem.StatusCode)
convertedError.SetMeta(errorItem.Meta)

return &convertedError
}

// getAppropiateStatusCodeOrDefault loops through collection of transfer object errors (first to last), and
// attempts to pull and convert status code (string).
//
// NOTE: If error occurs the next element will be attempted. In the event no elements are left, the default
// error status code (400) will be returned
func getAppropiateStatusCodeOrDefault(transferObjectErrors []TransferObjectError) int {

for _, transferObjectError := range transferObjectErrors {

statusCode, err := strconv.Atoi(transferObjectError.GetStatusCode())
if err != nil {
continue
}

return statusCode
}

return defaultErrorsStatusCode
}

// is5xx returns whether status code is a 5xx
func is5xx(statusCode int) bool {
if statusCode >= 500 && statusCode <= 599 {
return true
}

return false
}

// sendHTTPResponse handles sending response based on the transfer object
func sendHTTPResponse(writer http.ResponseWriter, transferObject TransferObject) error {

Expand Down Expand Up @@ -296,6 +374,31 @@ func WithMeta(meta map[string]interface{}) ResponseAttributes {
}
}

// NewHTTPMultiErrorResponse this response aide is used to create
// a multi error response. It will utilise the manifest
// declared when creating its base replier to pull all corresponding
// error manifest items.
//
// With this aide, if desired, you can add additional attributes by using the
// WithHeaders and/ or WithMeta optional response attributes.
//
// Note: If ANY of the passed errors do not have a manifest entry, a single
// 500 error will be returned.
func (r *Replier) NewHTTPMultiErrorResponse(w http.ResponseWriter, errs []error, attributes ...ResponseAttributes) error {

request := NewResponseRequest{
Writer: w,
Errors: errs,
}

// Add attributes to response request
for _, attribute := range attributes {
attribute(&request)
}

return r.NewHTTPResponse(&request)
}

// NewHTTPErrorResponse this response aide is used to create
// response explicitly for errors. It will utilise the manifest
// declared when creating its base replier.
Expand Down
8 changes: 4 additions & 4 deletions replier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,10 @@ func TestReplier_NewHTTPResponseForError(t *testing.T) {
{
name: "Success - Resource not found",
manifests: append([]reply.ErrorManifest{
{"test-404-error": reply.ErrorManifestItem{Message: "resource not found", StatusCode: http.StatusNotFound}},
{"test-404-error": reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound}},
},
reply.ErrorManifest{
"test-401-error": reply.ErrorManifestItem{Message: "unauthorized", StatusCode: http.StatusUnauthorized},
"test-401-error": reply.ErrorManifestItem{Title: "unauthorized", StatusCode: http.StatusUnauthorized},
},
),
err: errors.New("test-404-error"),
Expand Down Expand Up @@ -260,10 +260,10 @@ func TestReplier_AideNewHTTPErrorResponse(t *testing.T) {
{
name: "Success - Resource not found",
manifests: append([]reply.ErrorManifest{
{"test-404-error": reply.ErrorManifestItem{Message: "resource not found", StatusCode: http.StatusNotFound}},
{"test-404-error": reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound}},
},
reply.ErrorManifest{
"test-401-error": reply.ErrorManifestItem{Message: "unauthorized", StatusCode: http.StatusUnauthorized},
"test-401-error": reply.ErrorManifestItem{Title: "unauthorized", StatusCode: http.StatusUnauthorized},
},
),
err: errors.New("test-404-error"),
Expand Down

0 comments on commit 2046955

Please sign in to comment.