From 204695528dd568d002e2699257fb52cfdaee4bb0 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Fri, 10 Sep 2021 13:53:47 -0400 Subject: [PATCH] Added logic to handle multi error response with respective aide --- examples/example_simple_api.go | 23 ++++++ release-notes.md | 31 ++++++++ replier.go | 139 ++++++++++++++++++++++++++++----- replier_test.go | 8 +- 4 files changed, 179 insertions(+), 22 deletions(-) diff --git a/examples/example_simple_api.go b/examples/example_simple_api.go index e3e8c8b..82135ae 100644 --- a/examples/example_simple_api.go +++ b/examples/example_simple_api.go @@ -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) @@ -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{ @@ -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 @@ -225,6 +246,7 @@ 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) @@ -232,6 +254,7 @@ func handleRequest() { 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) diff --git a/release-notes.md b/release-notes.md index 9446773..0605ae4 100644 --- a/release-notes.md +++ b/release-notes.md @@ -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 diff --git a/replier.go b/replier.go index deac5ff..3261623 100644 --- a/replier.go +++ b/replier.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "net/http" + "strconv" ) // TransferObjectError outlines expected methods of a transfer object error @@ -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 @@ -70,6 +74,7 @@ type NewResponseRequest struct { StatusCode int Message string Error error + Errors []error AccessToken string RefreshToken string } @@ -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) @@ -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 @@ -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 { @@ -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. diff --git a/replier_test.go b/replier_test.go index 9c08a3b..21b1af0 100644 --- a/replier_test.go +++ b/replier_test.go @@ -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"), @@ -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"),