From 2fe9e66ddac7fea67bd5ffec68512834b92f5723 Mon Sep 17 00:00:00 2001 From: survivorbat Date: Mon, 25 Nov 2024 14:47:59 +0100 Subject: [PATCH] Initial version of v3 --- .golangci.yaml | 110 ++++++++++++++ Makefile | 5 +- README.md | 76 +++------- errors.go | 184 +++++++++-------------- errors_test.go | 384 ++++++++++++----------------------------------- examples_test.go | 96 ++++++------ go.mod | 4 +- 7 files changed, 357 insertions(+), 502 deletions(-) create mode 100644 .golangci.yaml diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..8da17e0 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,110 @@ +issues: + exclude-rules: + - path: (.+)_test.go + linters: + - goconst # Test data doesn't need to be in constants + - err113 # Necessary for tests + +linters-settings: + nlreturn: + block-size: 3 + + gocritic: + disabled-checks: + - "paramTypeCombine" + - "unnamedResult" + enabled-tags: + - "performance" + - "style" + - "diagnostic" + + govet: + enable-all: true + disable: + - fieldalignment + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - containedctx + - contextcheck + - copyloopvar + - cyclop + - decorder + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - copyloopvar + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives + - gochecknoinits + - gocognit + - goconst + - gocritic + - err113 + - gofmt + - gofumpt + - goheader + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - gosmopolitan + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - intrange + - ireturn + - loggercheck + - maintidx + - makezero + - mirror + - misspell + - mnd + - musttag + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - nolintlint + - nonamedreturns + - nosprintfhostport + - paralleltest + - perfsprint + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - spancheck + - sqlclosecheck + - staticcheck + - tagalign + - tagliatelle + - tenv + - testifylint + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - wastedassign + - whitespace + - zerologlint diff --git a/Makefile b/Makefile index caf5866..21935af 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,9 @@ test: fmt ## Run unit tests, alias: t go test ./... -timeout=30s -parallel=8 fmt: ## Format go code - @go mod tidy - @gofumpt -l -w . + go mod tidy + gofumpt -l -w . + golangci-lint run --fix ./... tools: ## Install extra tools for development go install mvdan.cc/gofumpt@latest diff --git a/README.md b/README.md index af48369..59193b5 100644 --- a/README.md +++ b/README.md @@ -8,75 +8,35 @@ Sending any error back to the user can pose a [big security risk](https://owasp. For this reason we developed an error registry that allows you to register specific error handlers for your application. This way you can control what information is sent back to the user. -You can register errors in 3 ways: -- By error type -- By value of string errors -- By defining the error name yourself - -## 👷 V2 migration guide - -V2 of this library changes the interface of all the methods to allow contexts to be passed to handlers. This -allows you to add additional data to the final response. - -The interface changes are as follows. - -- `RegisterErrorHandler` and all its variants take a context as a first parameter in the handler, allowing you to pass more data to the response -- `RegisterErrorHandler` and all its variants require the callback function to return `(int, any)` instead of `(int, R)`, removing the unnecessary generic -- Both `NewErrorResponse` and `NewErrorResponseFrom` take a context as a first parameter, this could be the request context but that's up to you +## 👷 V3 migration guide + +V3 completely revamps the `ErrorRegistry` and now utilises the `errors` package to match errors. +The following changes have been made: + +- `RegisterErrorHandler` now requires a concrete instance of the error as its first argument +- `RegisterErrorHandlerOn` now requires a concrete instance of the error as its second argument +- `RegisterStringErrorHandler` has been removed, use static `errors.New` in `RegisterErrorHandler` to get this to work +- `RegisterStringErrorHandlerOn` has been removed, use static `errors.New` in `RegisterErrorHandlerOn` to get this to work +- `RegisterCustomErrorTypeHandler` has been removed, wrap unexported errors from libraries to create handlers for these +- `RegisterCustomErrorTypeHandlerOn` has been removed, wrap unexported errors from libraries to create handlers for these +- `ErrorRegistry` changes: + - `DefaultCode` has been removed, use `RegisterDefaultHandler` instead + - `DefaultResponse` has been removed, use `RegisterDefaultHandler` instead + - `SetDefaultResponse` has been removed, use `RegisterDefaultHandler` instead ## ⬇️ Installation -`go get github.com/ing-bank/ginerr/v2` +`go get github.com/ing-bank/ginerr/v3` ## 📋 Usage -```go -package main - -import ( - "github.com/gin-gonic/gin" - "github.com/ing-bank/ginerr/v2" - "net/http" -) - -type MyError struct { -} - -func (m *MyError) Error() string { - return "Something went wrong!" -} - -// Response is an example response object, you can return anything you like -type Response struct { - Errors map[string]any `json:"errors,omitempty"` -} - -func main() { - handler := func(ctx context.Context, myError *MyError) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError.Error(), - }, - } - } - - ginerr.RegisterErrorHandler(handler) - - // [...] -} - -func handleGet(c *gin.Context) { - err := &MyError{} - c.JSON(ginerr.NewErrorResponse(c.Request.Context(), err)) -} -``` +Check out [the examples here](./examples_test.go). ## 🚀 Development 1. Clone the repository 2. Run `make tools` to install necessary tools -3. Run `make t` to run unit tests -4. Run `make fmt` to format code +3. Run `make fmt` to format code 4. Run `make lint` to lint your code You can run `make` to see a list of useful commands. diff --git a/errors.go b/errors.go index c3eca1f..33612e2 100644 --- a/errors.go +++ b/errors.go @@ -2,158 +2,118 @@ package ginerr import ( "context" + "errors" + "fmt" "net/http" - "reflect" ) -const defaultCode = http.StatusInternalServerError - +// DefaultErrorRegistry is a global singleton empty ErrorRegistry for convenience. var DefaultErrorRegistry = NewErrorRegistry() -type ( - internalHandler func(ctx context.Context, err error) (int, any) - internalStringHandler func(ctx context.Context, err string) (int, any) -) +// errorHandler encompasses the methods necessary to validate an error and calculate a response. +type errorHandler struct { + // isStringError is used to catch cases where errors.New() is being used as error types, + // as we need to use errors.Is for those cases, errors.As is not enough + isStringError bool -func NewErrorRegistry() *ErrorRegistry { - registry := &ErrorRegistry{ - handlers: make(map[string]internalHandler), - stringHandlers: make(map[string]internalStringHandler), - DefaultCode: defaultCode, - } + // isType is a wrapped around an `errors.As` from RegisterERrorHandler with the type information + // of the target error still intact. + isType func(err error) bool - // Make sure the stringHandlers are available in the handlers - registry.handlers["errors.errorString"] = func(ctx context.Context, err error) (int, any) { - // Check if the error string exists - if handler, ok := registry.stringHandlers[err.Error()]; ok { - return handler(ctx, err.Error()) - } + // handle will calculate the response. It's a wrapper around the user-provided handler + // which ensures that the type of the error is properly asserted using `errors.As`. + handle func(ctx context.Context, err error) (int, any) +} - return registry.defaultResponse(ctx, err) +// NewErrorRegistry instantiates a new ErrorRegistry. If you're looking for the 'default' error +// registry, check out DefaultErrorRegistry. +func NewErrorRegistry() *ErrorRegistry { + registry := &ErrorRegistry{ + handlers: make(map[error]*errorHandler), + defaultHandler: func(context.Context, error) (int, any) { + return http.StatusInternalServerError, nil + }, } return registry } +// ErrorRegistry is the place where errors and callbacks are stored. type ErrorRegistry struct { - // handlers are used when we know the type of the error - handlers map[string]internalHandler + // handlers maps error types with their handlers + handlers map[error]*errorHandler - // stringHandlers are used when the error is only a string - stringHandlers map[string]internalStringHandler - - // DefaultHandler takes precedent over DefaultCode and DefaultResponse - DefaultHandler func(ctx context.Context, err error) (int, any) - - // DefaultCode to return when no handler is found. Deprecated: Prefer DefaultHandler - DefaultCode int - - // DefaultResponse to return when no handler is found. Deprecated: Prefer DefaultHandler - DefaultResponse any -} - -// SetDefaultResponse is deprecated, prefer RegisterDefaultHandler -func (e *ErrorRegistry) SetDefaultResponse(code int, response any) { - e.DefaultCode = code - e.DefaultResponse = response + // defaultHandler + defaultHandler func(ctx context.Context, err error) (int, any) } func (e *ErrorRegistry) RegisterDefaultHandler(callback func(ctx context.Context, err error) (int, any)) { - e.DefaultHandler = callback -} - -func (e *ErrorRegistry) defaultResponse(ctx context.Context, err error) (int, any) { - // In production, we should return a generic error message. If you want to know why, read this: - // https://owasp.org/www-community/Improper_Error_Handling - if e.DefaultHandler != nil { - return e.DefaultHandler(ctx, err) - } - - return e.DefaultCode, e.DefaultResponse + e.defaultHandler = callback } // NewErrorResponse Returns an error response using the DefaultErrorRegistry. If no specific handler could be found, // it will return the defaults. func NewErrorResponse(ctx context.Context, err error) (int, any) { - return NewErrorResponseFrom(DefaultErrorRegistry, ctx, err) + return NewErrorResponseFrom(ctx, DefaultErrorRegistry, err) } // NewErrorResponseFrom Returns an error response using the given registry. If no specific handler could be found, // it will return the defaults. -func NewErrorResponseFrom[E error](registry *ErrorRegistry, ctx context.Context, err E) (int, any) { - errorType := getErrorType[E](err) +func NewErrorResponseFrom[E error](ctx context.Context, registry *ErrorRegistry, err E) (int, any) { + for errConcrete, handler := range registry.handlers { + // We can't use `errors.As` here directly, as we don't have a concrete version of the type here + if !handler.isType(err) { + continue + } + + // If it's a string error, it must match the given error exactly, otherwise it might mix up if we only + // check on type + if handler.isStringError { + if errors.Is(err, errConcrete) { + // It might be wrapped, so we pass the concrete type + return handler.handle(ctx, errConcrete) + } + + continue + } - // If a handler is registered for the error type, use it. - if entry, ok := registry.handlers[errorType]; ok { - return entry(ctx, err) + return handler.handle(ctx, err) } - return registry.defaultResponse(ctx, err) + return registry.defaultHandler(ctx, err) } -// RegisterErrorHandler registers an error handler in DefaultErrorRegistry. The R type is the type of the response body. -func RegisterErrorHandler[E error](handler func(context.Context, E) (int, any)) { - RegisterErrorHandlerOn(DefaultErrorRegistry, handler) +// RegisterErrorHandler registers an error handler in DefaultErrorRegistry. +func RegisterErrorHandler[E error](instance E, handler func(context.Context, E) (int, any)) { + RegisterErrorHandlerOn(DefaultErrorRegistry, instance, handler) } -// RegisterErrorHandlerOn registers an error handler in the given registry. The R type is the type of the response body. -func RegisterErrorHandlerOn[E error](registry *ErrorRegistry, handler func(context.Context, E) (int, any)) { - // Name of the type - errorType := getErrorType[E](new(E)) +// errorStringType is used to check if an error was created by errors.New or fmt.Errorf +// +//nolint:err113 // We need it here for the type name +var errorStringType = fmt.Sprintf("%T", errors.New("")) +// RegisterErrorHandlerOn registers an error handler in the given registry. +func RegisterErrorHandlerOn[E error](registry *ErrorRegistry, instance E, handler func(context.Context, E) (int, any)) { // Wrap it in a closure, we can't save it directly because err E is not available in NewErrorResponseFrom. It will // be available in the closure when it is called. Check out TestErrorResponseFrom_ReturnsErrorBInInterface for an example. - registry.handlers[errorType] = func(ctx context.Context, err error) (int, any) { - return handler(ctx, err.(E)) - } -} - -// RegisterCustomErrorTypeHandler registers an error handler in DefaultErrorRegistry. Same as RegisterErrorHandler, -// but you can set the fmt.Sprint("%T", err) error yourself. Allows you to register error types that aren't exported -// from their respective packages such as the uuid error or *errors.errorString. The R type is the type of the response body. -func RegisterCustomErrorTypeHandler(errorType string, handler func(ctx context.Context, err error) (int, any)) { - RegisterCustomErrorTypeHandlerOn(DefaultErrorRegistry, errorType, handler) -} - -// RegisterCustomErrorTypeHandlerOn registers an error handler in the given registry. Same as RegisterErrorHandlerOn, -// but you can set the fmt.Sprint("%T", err) error yourself. Allows you to register error types that aren't exported -// from their respective packages such as the uuid error or *errors.errorString. The R type is the type of the response body. -func RegisterCustomErrorTypeHandlerOn(registry *ErrorRegistry, errorType string, handler func(ctx context.Context, err error) (int, any)) { - // Wrap it in a closure, we can't save it directly - registry.handlers[errorType] = handler -} + registry.handlers[instance] = &errorHandler{ + // Necessary to make sure we match error strings using `errors.Is` + isStringError: fmt.Sprintf("%T", instance) == errorStringType, -// RegisterStringErrorHandler allows you to register an error handler for a simple errorString created with -// errors.New() or fmt.Errorf(). Can be used in case you are dealing with libraries that don't have exported -// error objects. Uses the DefaultErrorRegistry. The R type is the type of the response body. -func RegisterStringErrorHandler(errorString string, handler func(ctx context.Context, err string) (int, any)) { - RegisterStringErrorHandlerOn(DefaultErrorRegistry, errorString, handler) -} + // Handler that uses errors.As to cast to an error + handle: func(ctx context.Context, err error) (int, any) { + var errorOfType E -// RegisterStringErrorHandlerOn allows you to register an error handler for a simple errorString created with -// errors.New() or fmt.Errorf(). Can be used in case you are dealing with libraries that don't have exported -// error objects. The R type is the type of the response body. -func RegisterStringErrorHandlerOn(registry *ErrorRegistry, errorString string, handler func(ctx context.Context, err string) (int, any)) { - registry.stringHandlers[errorString] = handler -} + // This function should only be called if errors.Is succeeded, so this should never fail + _ = errors.As(err, &errorOfType) -// getErrorType returns the errorType from the generic type. If the generic type returns the typealias "error", -// e.g. due to `type SomeError error`, retry with the concrete `err` value. -func getErrorType[E error](err any) string { - typeOf := reflect.ValueOf(new(E)).Type() - for typeOf.Kind() == reflect.Pointer { - typeOf = typeOf.Elem() - } - errorType := typeOf.String() + return handler(ctx, errorOfType) + }, - if errorType == "error" { - // try once more but with err instead of new(E) - typeOf = reflect.ValueOf(err).Type() - for typeOf.Kind() == reflect.Pointer { - typeOf = typeOf.Elem() - } - errorType = typeOf.String() + // Type check, as we need `instance` from this function + isType: func(err error) bool { + return errors.As(err, &instance) + }, } - - return errorType } diff --git a/errors_test.go b/errors_test.go index 2dd62ca..ceaaef0 100644 --- a/errors_test.go +++ b/errors_test.go @@ -3,6 +3,7 @@ package ginerr import ( "context" "errors" + "fmt" "net/http" "testing" @@ -11,43 +12,40 @@ import ( // Register functions are tested through NewErrorResponse[E error] -type ErrorA struct { +type AError struct { message string } -func (e ErrorA) Error() string { +func (e *AError) Error() string { return e.message } -type ErrorB struct { +type BError struct { message string } -func (e ErrorB) Error() string { +func (e *BError) Error() string { return e.message } -type ErrorC error +type dummyContextKey string // The top ones are not parallel because it uses the DefaultErrorRegistry, which is a global +//nolint:paralleltest // Can't be used, we use global variables func TestErrorResponse_UsesDefaultErrorRegistry(t *testing.T) { // Arrange - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } + expectedResponse := "user-friendly error" - var calledWithError *ErrorA - callback := func(ctx context.Context, err *ErrorA) (int, any) { + var calledWithError *AError + callback := func(_ context.Context, err *AError) (int, any) { calledWithError = err - return 634, Response{ - Errors: map[string]any{"error": err.Error()}, - } + return 634, expectedResponse } - err := &ErrorA{message: "It was the man with one hand!"} + err := &AError{message: "It was the man with one hand!"} - RegisterErrorHandler(callback) + RegisterErrorHandler(&AError{}, callback) // Act code, response := NewErrorResponse(context.Background(), err) @@ -58,77 +56,19 @@ func TestErrorResponse_UsesDefaultErrorRegistry(t *testing.T) { assert.Equal(t, expectedResponse, response) } -func TestErrorResponse_UsesDefaultErrorRegistryForStrings(t *testing.T) { - // Arrange - expectedResponse := Response{ - Errors: map[string]any{"error": "my error"}, - } - - var calledWithError string - var calledWithContext context.Context - callback := func(ctx context.Context, err string) (int, any) { - calledWithError = err - calledWithContext = ctx - return 123, Response{ - Errors: map[string]any{"error": err}, - } - } - - err := errors.New("my error") - - RegisterStringErrorHandler("my error", callback) - - ctx := context.WithValue(context.Background(), ErrorA{}, "anything") - - // Act - code, response := NewErrorResponse(ctx, err) - - // Assert - assert.Equal(t, ctx, calledWithContext) - assert.Equal(t, err.Error(), calledWithError) - assert.Equal(t, 123, code) - assert.Equal(t, expectedResponse, response) -} - -func TestErrorResponse_UsesDefaultErrorRegistryForCustomTypes(t *testing.T) { - // Arrange - expectedResponse := Response{ - Errors: map[string]any{"error": "assert.AnError general error for testing"}, - } - - var calledWithError error - callback := func(ctx context.Context, err error) (int, any) { - calledWithError = err - return 123, Response{ - Errors: map[string]any{"error": err.Error()}, - } - } - - RegisterCustomErrorTypeHandler("errors.errorString", callback) - - // Act - code, response := NewErrorResponse(context.Background(), assert.AnError) - - // Assert - assert.Equal(t, assert.AnError, calledWithError) - assert.Equal(t, 123, code) - assert.Equal(t, expectedResponse, response) -} - // These are parallel because it uses the 'from' variant -func TestErrorResponseFrom_ReturnsGenericErrorOnNotFound(t *testing.T) { +func TestErrorResponseFrom_ReturnsNullOnNoDefaultErrorDefined(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() - registry.SetDefaultResponse(123, "test") // Act - code, response := NewErrorResponseFrom(registry, context.Background(), assert.AnError) + code, response := NewErrorResponseFrom(context.Background(), registry, assert.AnError) // Assert - assert.Equal(t, 123, code) - assert.Equal(t, "test", response) + assert.Equal(t, http.StatusInternalServerError, code) + assert.Nil(t, response) } func TestErrorResponseFrom_UsesDefaultCallbackOnNotFound(t *testing.T) { @@ -136,147 +76,164 @@ func TestErrorResponseFrom_UsesDefaultCallbackOnNotFound(t *testing.T) { // Arrange registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "internal server error"}, - } - var calledWithErr error var calledWithCtx context.Context callback := func(ctx context.Context, err error) (int, any) { calledWithErr = err calledWithCtx = ctx - return http.StatusInternalServerError, expectedResponse + return http.StatusPaymentRequired, "abc" } registry.RegisterDefaultHandler(callback) - ctx := context.WithValue(context.Background(), ErrorA{}, "good") + // Dummy value to check whether it was passed correctly + ctx := context.WithValue(context.Background(), dummyContextKey("abc"), "good") // Act - code, response := NewErrorResponseFrom(registry, ctx, assert.AnError) + code, response := NewErrorResponseFrom(ctx, registry, assert.AnError) // Assert - assert.Equal(t, expectedResponse, response) - assert.Equal(t, code, http.StatusInternalServerError) + assert.Equal(t, "abc", response) + assert.Equal(t, http.StatusPaymentRequired, code) assert.Equal(t, ctx, calledWithCtx) assert.Equal(t, assert.AnError, calledWithErr) } -func TestErrorResponseFrom_ReturnsErrorAWithContext(t *testing.T) { +func TestErrorResponseFrom_ReturnsExpectedResponsesOnErrorTypes(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, + + var calledWithErrA *AError + var calledWithCtxA context.Context + callbackA := func(ctx context.Context, err *AError) (int, any) { + calledWithErrA = err + calledWithCtxA = ctx + return 123, "abc" } - var calledWithErr *ErrorA - var calledWithCtx context.Context - callback := func(ctx context.Context, err *ErrorA) (int, any) { - calledWithErr = err - calledWithCtx = ctx - return http.StatusInternalServerError, expectedResponse + var calledWithErrB *BError + var calledWithCtxB context.Context + callbackB := func(ctx context.Context, err *BError) (int, any) { + calledWithErrB = err + calledWithCtxB = ctx + return 456, "def" } - err := &ErrorA{message: "It was the man with one hand!"} + errA := &AError{message: "It was the man with one hand!"} + errB := &BError{message: "It was the man with one hand!"} - RegisterErrorHandlerOn(registry, callback) + RegisterErrorHandlerOn(registry, &AError{}, callbackA) + RegisterErrorHandlerOn(registry, &BError{}, callbackB) - ctx := context.WithValue(context.Background(), ErrorA{}, "cool") + ctx := context.WithValue(context.Background(), dummyContextKey("abc"), "cool") // Act - code, response := NewErrorResponseFrom(registry, ctx, err) + codeA, responseA := NewErrorResponseFrom(ctx, registry, errA) + codeB, responseB := NewErrorResponseFrom(ctx, registry, errB) // Assert - assert.Equal(t, http.StatusInternalServerError, code) - assert.Equal(t, expectedResponse, response) + assert.Equal(t, 123, codeA) + assert.Equal(t, "abc", responseA) + assert.Equal(t, errA, calledWithErrA) + assert.Equal(t, ctx, calledWithCtxA) - assert.Equal(t, err, calledWithErr) - assert.Equal(t, ctx, calledWithCtx) + assert.Equal(t, 456, codeB) + assert.Equal(t, "def", responseB) + assert.Equal(t, errB, calledWithErrB) + assert.Equal(t, ctx, calledWithCtxB) } -func TestErrorResponseFrom_ReturnsErrorB(t *testing.T) { +func TestErrorResponseFrom_ReturnsExpectedResponsesOnErrorStrings(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, + + var calledWithErrA error + var calledWithCtxA context.Context + callbackA := func(ctx context.Context, err error) (int, any) { + calledWithErrA = err + calledWithCtxA = ctx + return 123, "abc" } - var calledWithErr *ErrorB - callback := func(ctx context.Context, err *ErrorB) (int, any) { - calledWithErr = err - return http.StatusInternalServerError, expectedResponse + var calledWithErrB error + var calledWithCtxB context.Context + callbackB := func(ctx context.Context, err error) (int, any) { + calledWithErrB = err + calledWithCtxB = ctx + return 456, "def" } - err := &ErrorB{message: "It was the man with one hand!"} + errA := errors.New("error A") + errB := errors.New("error B") - RegisterErrorHandlerOn(registry, callback) + RegisterErrorHandlerOn(registry, errA, callbackA) + RegisterErrorHandlerOn(registry, errB, callbackB) + + ctx := context.WithValue(context.Background(), dummyContextKey("abc"), "cool") // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) + codeA, responseA := NewErrorResponseFrom(ctx, registry, fmt.Errorf("error A: %w", errA)) + codeB, responseB := NewErrorResponseFrom(ctx, registry, fmt.Errorf("error B: %w", errB)) // Assert - assert.Equal(t, calledWithErr, err) + assert.Equal(t, 123, codeA) + assert.Equal(t, "abc", responseA) + assert.Equal(t, errA, calledWithErrA) + assert.Equal(t, ctx, calledWithCtxA) - assert.Equal(t, http.StatusInternalServerError, code) - assert.Equal(t, expectedResponse, response) + assert.Equal(t, 456, codeB) + assert.Equal(t, "def", responseB) + assert.Equal(t, errB, calledWithErrB) + assert.Equal(t, ctx, calledWithCtxB) } -func TestErrorResponseFrom_ReturnsErrorC(t *testing.T) { +func TestErrorResponseFrom_HandlesWrappedErrorsProperly(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } + expectedResponse := "user-friendly error" - var calledWithErr []ErrorC - callback := func(ctx context.Context, err ErrorC) (int, any) { - calledWithErr = append(calledWithErr, err) + var calledWithErr *AError + callback := func(_ context.Context, err *AError) (int, any) { + calledWithErr = err return http.StatusInternalServerError, expectedResponse } - err := ErrorC(ErrorB{message: "It was the man with one hand!"}) - err2 := ErrorC(errors.New("It was the man with one hand!")) + err := fmt.Errorf("this error happened: %w", &AError{message: "abc"}) - RegisterErrorHandlerOn(registry, callback) + RegisterErrorHandlerOn(registry, &AError{}, callback) // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) - code2, response2 := NewErrorResponseFrom(registry, context.Background(), err2) + code, response := NewErrorResponseFrom(context.Background(), registry, err) // Assert - assert.Equal(t, calledWithErr[0], err) - assert.Equal(t, calledWithErr[1], err2) - assert.Equal(t, http.StatusInternalServerError, code) - assert.Equal(t, http.StatusInternalServerError, code2) assert.Equal(t, expectedResponse, response) - assert.Equal(t, expectedResponse, response2) + + assert.Equal(t, &AError{message: "abc"}, calledWithErr) } func TestErrorResponseFrom_ReturnsErrorBInInterface(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } + expectedResponse := "user-friendly error" var calledWithErr error - callback := func(ctx context.Context, err *ErrorB) (int, any) { + callback := func(_ context.Context, err *BError) (int, any) { calledWithErr = err return http.StatusInternalServerError, expectedResponse } - var err error = &ErrorB{message: "It was the man with one hand!"} + var err error = &BError{message: "It was the man with one hand!"} - RegisterErrorHandlerOn(registry, callback) + RegisterErrorHandlerOn(registry, &BError{}, callback) // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) + code, response := NewErrorResponseFrom(context.Background(), registry, err) // Assert assert.Equal(t, calledWithErr, err) @@ -284,160 +241,15 @@ func TestErrorResponseFrom_ReturnsErrorBInInterface(t *testing.T) { assert.Equal(t, expectedResponse, response) } -func TestErrorResponseFrom_ReturnsErrorStrings(t *testing.T) { - tests := []string{ - "Something went completely wrong!", - "Record not found", - } - - for _, errorString := range tests { - errorString := errorString - t.Run(errorString, func(t *testing.T) { - // Arrange - registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": errorString}, - } - - var calledWithContext context.Context - var calledWithErr string - callback := func(ctx context.Context, err string) (int, any) { - calledWithErr = err - calledWithContext = ctx - return 234, Response{ - Errors: map[string]any{"error": err}, - } - } - - err := errors.New(errorString) - - RegisterStringErrorHandlerOn(registry, errorString, callback) - - ctx := context.WithValue(context.Background(), ErrorA{}, "good") - - // Act - code, response := NewErrorResponseFrom(registry, ctx, err) - - // Assert - assert.Equal(t, ctx, calledWithContext) - assert.Equal(t, err.Error(), calledWithErr) - assert.Equal(t, 234, code) - assert.Equal(t, expectedResponse, response) - }) - } -} - -func TestErrorResponseFrom_CanConfigureMultipleErrorStrings(t *testing.T) { - // Arrange - registry := NewErrorRegistry() - - var callback1CalledWithString string - var callback1CalledWithContext context.Context - callback1 := func(ctx context.Context, err string) (int, any) { - callback1CalledWithContext = ctx - callback1CalledWithString = err - return 456, Response{} - } - - var callback2CalledWithString string - var callback2CalledWithContext context.Context - callback2 := func(ctx context.Context, err string) (int, any) { - callback2CalledWithContext = ctx - callback2CalledWithString = err - return 123, Response{} - } - - RegisterStringErrorHandlerOn(registry, "callback1", callback1) - RegisterStringErrorHandlerOn(registry, "callback2", callback2) - - err1 := errors.New("callback1") - err2 := errors.New("callback2") - - ctx := context.WithValue(context.Background(), ErrorA{}, "anything") - - // Act - code1, _ := NewErrorResponseFrom(registry, ctx, err1) - code2, _ := NewErrorResponseFrom(registry, ctx, err2) - - // Assert - assert.Equal(t, err1.Error(), callback1CalledWithString) - assert.Equal(t, err2.Error(), callback2CalledWithString) - - assert.Equal(t, ctx, callback1CalledWithContext) - assert.Equal(t, ctx, callback2CalledWithContext) - - assert.Equal(t, 456, code1) - assert.Equal(t, 123, code2) -} - -func TestErrorResponseFrom_ReturnsCustomErrorHandlers(t *testing.T) { - tests := []string{ - "Something went completely wrong!", - "Record not found", - } - - for _, errorString := range tests { - errorString := errorString - t.Run(errorString, func(t *testing.T) { - // Arrange - registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": errorString}, - } - - var calledWithContext context.Context - var calledWithErr error - callback := func(ctx context.Context, err error) (int, any) { - calledWithErr = err - calledWithContext = ctx - return 234, Response{ - Errors: map[string]any{"error": err.Error()}, - } - } - - err := errors.New(errorString) - - RegisterCustomErrorTypeHandlerOn(registry, "errors.errorString", callback) - - ctx := context.WithValue(context.Background(), ErrorA{}, "good") - - // Act - code, response := NewErrorResponseFrom(registry, ctx, err) - - // Assert - assert.Equal(t, ctx, calledWithContext) - assert.Equal(t, err, calledWithErr) - assert.Equal(t, 234, code) - assert.Equal(t, expectedResponse, response) - }) - } -} - func TestErrorResponseFrom_ReturnsGenericErrorOnTypeNotFound(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() // Act - code, response := NewErrorResponseFrom(registry, context.Background(), &ErrorB{}) + code, response := NewErrorResponseFrom(context.Background(), registry, &BError{}) // Assert assert.Equal(t, http.StatusInternalServerError, code) assert.Nil(t, response) } - -func TestErrorRegistry_SetDefaultResponse_SetsProperties(t *testing.T) { - t.Parallel() - // Arrange - registry := NewErrorRegistry() - - code := 123 - response := Response{Errors: map[string]any{"error": "Something went wrong"}} - - // Act - registry.SetDefaultResponse(123, response) - - // Assert - assert.Equal(t, code, registry.DefaultCode) - assert.Equal(t, response, registry.DefaultResponse) -} diff --git a/examples_test.go b/examples_test.go index 6b51d83..9372dc8 100644 --- a/examples_test.go +++ b/examples_test.go @@ -2,67 +2,79 @@ package ginerr import ( "context" + "errors" + "fmt" "net/http" ) -type Response struct { - Errors map[string]any `json:"errors,omitempty"` -} +var ErrDatabaseOverloaded = errors.New("database overloaded") -type MyError struct{} +type InputValidationError struct { + // ... +} -func (m MyError) Error() string { - return "Something went wrong!" +func (m *InputValidationError) Error() string { + return "..." } func ExampleRegisterErrorHandler() { - handler := func(ctx context.Context, myError *MyError) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError.Error(), - }, - } + // Write your error handlers + validationHandler := func(_ context.Context, err *InputValidationError) (int, any) { + return http.StatusBadRequest, "Your input was invalid: " + err.Error() } + databaseOverloadedHandler := func(context.Context, error) (int, any) { + return http.StatusBadGateway, "Please try again later" + } + + // Register the error handlers and supply an empty version of the type for type reference + RegisterErrorHandler(&InputValidationError{}, validationHandler) + RegisterErrorHandler(ErrDatabaseOverloaded, databaseOverloadedHandler) + + // Return errors somewhere deep in your code + errA := fmt.Errorf("validation error: %w", &InputValidationError{}) + errB := fmt.Errorf("could not connect to database: %w", ErrDatabaseOverloaded) + + // In your HTTP handlers, instantiate responses and return those to the users + codeA, responseA := NewErrorResponse(context.Background(), errA) + codeB, responseB := NewErrorResponse(context.Background(), errB) + + // Check the output + fmt.Printf("%d: %s\n", codeA, responseA) + fmt.Printf("%d: %s\n", codeB, responseB) - RegisterErrorHandler(handler) + // Output + // 400: Your input was invalid: ... + // 502: Please try again later } func ExampleRegisterErrorHandlerOn() { registry := NewErrorRegistry() - handler := func(ctx context.Context, myError *MyError) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError.Error(), - }, - } + // Write your error handlers + validationHandler := func(_ context.Context, err *InputValidationError) (int, any) { + return http.StatusBadRequest, "Your input was invalid: " + err.Error() } - - RegisterErrorHandlerOn(registry, handler) -} - -func ExampleRegisterStringErrorHandler() { - handler := func(ctx context.Context, myError string) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError, - }, - } + databaseOverloadedHandler := func(context.Context, error) (int, any) { + return http.StatusBadGateway, "please try again later" } - RegisterStringErrorHandler("some string error", handler) -} + // Register the error handlers and supply an empty version of the type for type reference + RegisterErrorHandlerOn(registry, &InputValidationError{}, validationHandler) + RegisterErrorHandlerOn(registry, ErrDatabaseOverloaded, databaseOverloadedHandler) -func ExampleRegisterStringErrorHandlerOn() { - registry := NewErrorRegistry() + // Return errors somewhere deep in your code + errA := fmt.Errorf("validation error: %w", &InputValidationError{}) + errB := fmt.Errorf("could not connect to database: %w", ErrDatabaseOverloaded) - handler := func(ctx context.Context, myError string) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError, - }, - } - } + // In your HTTP handlers, instantiate responses and return those to the users + codeA, responseA := NewErrorResponseFrom(context.Background(), registry, errA) + codeB, responseB := NewErrorResponseFrom(context.Background(), registry, errB) + + // Check the output + fmt.Printf("%d: %s\n", codeA, responseA) + fmt.Printf("%d: %s\n", codeB, responseB) - RegisterStringErrorHandlerOn(registry, "some string error", handler) + // Output + // 400: Your input was invalid: ... + // 502: Please try again later } diff --git a/go.mod b/go.mod index e5fbc7a..c2fbaf4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/ing-bank/ginerr/v2 +module github.com/ing-bank/ginerr/v3 -go 1.20 +go 1.23.0 require github.com/stretchr/testify v1.8.1