diff --git a/.golangci.yaml b/.golangci.yaml deleted file mode 100644 index ce6d8b7..0000000 --- a/.golangci.yaml +++ /dev/null @@ -1,96 +0,0 @@ -linters-settings: - gocritic: - disabled-checks: - - "paramTypeCombine" - enabled-tags: - - "performance" - - "style" - - "diagnostic" - -issues: - exclude-rules: - - path: (.+)_test.go - linters: - - funlen - - goconst - - nilnil - - goerr113 - -linters: - enable: - - asasalint - - asciicheck - - bidichk - - bodyclose - - containedctx - - contextcheck - - cyclop - - decorder - - dogsled - - dupword - - durationcheck - - errcheck - - errname - - errorlint - - execinquery - - exhaustive - - exportloopref - - forbidigo - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoinits - - gocognit - - goconst - - gocritic - - gocyclo - - goerr113 - - gofmt - - gofumpt - - goheader - - goimports - - gomoddirectives - - gomodguard - - goprintffuncname - - gosec - - gosimple - - gosmopolitan - - govet - - grouper - - ifshort - - importas - - ineffassign - - interfacebloat - - interfacer - - ireturn - - loggercheck - - maintidx - - makezero - - mirror - - misspell - - musttag - - nakedret - - nestif - - nilerr - - nilnil - - nlreturn - - nonamedreturns - - nosprintfhostport - - paralleltest - - prealloc - - predeclared - - promlinter - - reassign - - rowserrcheck - - sqlclosecheck - - staticcheck - - tenv - - thelper - - tparallel - - typecheck - - unconvert - - unparam - - unused - - usestdlibvars - - wastedassign - - whitespace - - zerologlint diff --git a/README.md b/README.md index 705ce47..af48369 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ ![GitHub](https://img.shields.io/github/license/ing-bank/ginerr) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/ing-bank/ginerr) -**[❗ 🚨 Click here for version 2 🚨 ❗](./v2)** - Sending any error back to the user can pose a [big security risk](https://owasp.org/www-community/Improper_Error_Handling). 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. @@ -15,9 +13,20 @@ You can register errors in 3 ways: - 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 + ## ⬇️ Installation -`go get github.com/ing-bank/ginerr` +`go get github.com/ing-bank/ginerr/v2` ## 📋 Usage @@ -26,7 +35,7 @@ package main import ( "github.com/gin-gonic/gin" - "github.com/ing-bank/ginerr" + "github.com/ing-bank/ginerr/v2" "net/http" ) @@ -43,7 +52,7 @@ type Response struct { } func main() { - handler := func(myError *MyError) (int, Response) { + handler := func(ctx context.Context, myError *MyError) (int, any) { return http.StatusInternalServerError, Response{ Errors: map[string]any{ "error": myError.Error(), @@ -58,7 +67,7 @@ func main() { func handleGet(c *gin.Context) { err := &MyError{} - c.JSON(ginerr.NewErrorResponse(err)) + c.JSON(ginerr.NewErrorResponse(c.Request.Context(), err)) } ``` diff --git a/errors.go b/errors.go index 95fc04b..c3eca1f 100644 --- a/errors.go +++ b/errors.go @@ -1,36 +1,20 @@ package ginerr import ( - "fmt" + "context" "net/http" + "reflect" ) const defaultCode = http.StatusInternalServerError -// Deprecated: Please use v2 of this library var DefaultErrorRegistry = NewErrorRegistry() type ( - internalHandler func(err error) (int, any) - internalStringHandler func(err string) (int, any) + internalHandler func(ctx context.Context, err error) (int, any) + internalStringHandler func(ctx context.Context, err string) (int, any) ) -// CustomErrorHandler is the template for unexported errors. For example binding.SliceValidationError -// or uuid.invalidLengthError -// Deprecated: Please use v2 of this library -type CustomErrorHandler[R any] func(err error) (int, R) - -// ErrorStringHandler is the template for string errors that don't have their own object available. For example -// "record not found" or "invalid input" -// Deprecated: Please use v2 of this library -type ErrorStringHandler[R any] func(err string) (int, R) - -// ErrorHandler is the template of an error handler in the ErrorRegistry. The E type is the error type that -// the handler is registered for. The R type is the type of the response body. -// Deprecated: Please use v2 of this library -type ErrorHandler[E error, R any] func(E) (int, R) - -// Deprecated: Please use v2 of this library func NewErrorRegistry() *ErrorRegistry { registry := &ErrorRegistry{ handlers: make(map[string]internalHandler), @@ -39,19 +23,18 @@ func NewErrorRegistry() *ErrorRegistry { } // Make sure the stringHandlers are available in the handlers - registry.handlers["*errors.errorString"] = func(err error) (int, any) { + 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(err.Error()) + return handler(ctx, err.Error()) } - return registry.DefaultCode, registry.DefaultResponse + return registry.defaultResponse(ctx, err) } return registry } -// Deprecated: Please use v2 of this library type ErrorRegistry struct { // handlers are used when we know the type of the error handlers map[string]internalHandler @@ -59,102 +42,118 @@ type ErrorRegistry struct { // stringHandlers are used when the error is only a string stringHandlers map[string]internalStringHandler - // DefaultCode to return when no handler is found + // 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 + // DefaultResponse to return when no handler is found. Deprecated: Prefer DefaultHandler DefaultResponse any } -// Deprecated: Please use v2 of this library +// SetDefaultResponse is deprecated, prefer RegisterDefaultHandler func (e *ErrorRegistry) SetDefaultResponse(code int, response any) { e.DefaultCode = code e.DefaultResponse = response } +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 +} + // NewErrorResponse Returns an error response using the DefaultErrorRegistry. If no specific handler could be found, -// it will return the defaults. It returns an HTTP status code and a response object. -// -// Deprecated: Please use v2 of this library -// -//nolint:gocritic // Unnamed return arguments are described -func NewErrorResponse(err error) (int, any) { - return NewErrorResponseFrom(DefaultErrorRegistry, err) +// it will return the defaults. +func NewErrorResponse(ctx context.Context, err error) (int, any) { + return NewErrorResponseFrom(DefaultErrorRegistry, ctx, err) } // NewErrorResponseFrom Returns an error response using the given registry. If no specific handler could be found, -// it will return the defaults. It returns an HTTP status code and a response object. -// -// Deprecated: Please use v2 of this library -// -//nolint:gocritic // Unnamed return arguments are described -func NewErrorResponseFrom(registry *ErrorRegistry, err error) (int, any) { - errorType := fmt.Sprintf("%T", err) +// it will return the defaults. +func NewErrorResponseFrom[E error](registry *ErrorRegistry, ctx context.Context, err E) (int, any) { + errorType := getErrorType[E](err) // If a handler is registered for the error type, use it. if entry, ok := registry.handlers[errorType]; ok { - return entry(err) + return entry(ctx, err) } - // 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 - return registry.DefaultCode, registry.DefaultResponse + return registry.defaultResponse(ctx, err) } // RegisterErrorHandler registers an error handler in DefaultErrorRegistry. The R type is the type of the response body. -// Deprecated: Please use v2 of this library -func RegisterErrorHandler[E error, R any](handler ErrorHandler[E, R]) { +func RegisterErrorHandler[E error](handler func(context.Context, E) (int, any)) { RegisterErrorHandlerOn(DefaultErrorRegistry, handler) } // RegisterErrorHandlerOn registers an error handler in the given registry. The R type is the type of the response body. -// Deprecated: Please use v2 of this library -func RegisterErrorHandlerOn[E error, R any](registry *ErrorRegistry, handler ErrorHandler[E, R]) { +func RegisterErrorHandlerOn[E error](registry *ErrorRegistry, handler func(context.Context, E) (int, any)) { // Name of the type - errorType := fmt.Sprintf("%T", *new(E)) + errorType := getErrorType[E](new(E)) // 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(err error) (int, any) { - // We can safely cast it here, because we know it's the right type. - //nolint:errorlint // Not relevant, we're casting anyway - return handler(err.(E)) + 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. -// Deprecated: Please use v2 of this library -func RegisterCustomErrorTypeHandler[R any](errorType string, handler CustomErrorHandler[R]) { +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. -// Deprecated: Please use v2 of this library -func RegisterCustomErrorTypeHandlerOn[R any](registry *ErrorRegistry, errorType string, handler CustomErrorHandler[R]) { +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] = func(err error) (int, any) { - return handler(err) - } + registry.handlers[errorType] = handler } // 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. -// Deprecated: Please use v2 of this library -func RegisterStringErrorHandler[R any](errorString string, handler ErrorStringHandler[R]) { +func RegisterStringErrorHandler(errorString string, handler func(ctx context.Context, err string) (int, any)) { RegisterStringErrorHandlerOn(DefaultErrorRegistry, errorString, handler) } // 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. -// Deprecated: Please use v2 of this library -func RegisterStringErrorHandlerOn[R any](registry *ErrorRegistry, errorString string, handler ErrorStringHandler[R]) { - registry.stringHandlers[errorString] = func(err string) (int, any) { - return handler(err) +func RegisterStringErrorHandlerOn(registry *ErrorRegistry, errorString string, handler func(ctx context.Context, err string) (int, any)) { + registry.stringHandlers[errorString] = handler +} + +// 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() + + 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() + } + + return errorType } diff --git a/errors_test.go b/errors_test.go index 3aa9810..2dd62ca 100644 --- a/errors_test.go +++ b/errors_test.go @@ -1,6 +1,7 @@ package ginerr import ( + "context" "errors" "net/http" "testing" @@ -8,68 +9,66 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDefaultErrorGenerator_IsSet(t *testing.T) { - t.Parallel() - // Act - result := DefaultErrorRegistry - - // Assert - assert.NotNil(t, result) -} - // Register functions are tested through NewErrorResponse[E error] -type aError struct { +type ErrorA struct { message string } -func (e aError) Error() string { +func (e ErrorA) Error() string { return e.message } -type bError struct { +type ErrorB struct { message string } -func (e bError) Error() string { +func (e ErrorB) Error() string { return e.message } +type ErrorC error + // The top ones are not parallel because it uses the DefaultErrorRegistry, which is a global -//nolint:paralleltest // Because of global state func TestErrorResponse_UsesDefaultErrorRegistry(t *testing.T) { // Arrange expectedResponse := Response{ Errors: map[string]any{"error": "It was the man with one hand!"}, } - callback := func(err *aError) (int, Response) { + var calledWithError *ErrorA + callback := func(ctx context.Context, err *ErrorA) (int, any) { + calledWithError = err return 634, Response{ Errors: map[string]any{"error": err.Error()}, } } - err := &aError{message: "It was the man with one hand!"} + err := &ErrorA{message: "It was the man with one hand!"} RegisterErrorHandler(callback) // Act - code, response := NewErrorResponse(err) + code, response := NewErrorResponse(context.Background(), err) // Assert + assert.Equal(t, err, calledWithError) assert.Equal(t, 634, code) assert.Equal(t, expectedResponse, response) } -//nolint:paralleltest // Because of global state func TestErrorResponse_UsesDefaultErrorRegistryForStrings(t *testing.T) { // Arrange expectedResponse := Response{ Errors: map[string]any{"error": "my error"}, } - callback := func(err string) (int, Response) { + 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}, } @@ -79,57 +78,92 @@ func TestErrorResponse_UsesDefaultErrorRegistryForStrings(t *testing.T) { RegisterStringErrorHandler("my error", callback) + ctx := context.WithValue(context.Background(), ErrorA{}, "anything") + // Act - code, response := NewErrorResponse(err) + 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) } -//nolint:paralleltest // Because of global state func TestErrorResponse_UsesDefaultErrorRegistryForCustomTypes(t *testing.T) { // Arrange expectedResponse := Response{ - Errors: map[string]any{"error": "my error"}, + Errors: map[string]any{"error": "assert.AnError general error for testing"}, } - callback := func(err error) (int, Response) { + var calledWithError error + callback := func(ctx context.Context, err error) (int, any) { + calledWithError = err return 123, Response{ Errors: map[string]any{"error": err.Error()}, } } - err := errors.New("my error") - - RegisterCustomErrorTypeHandler("*errors.errorString", callback) + RegisterCustomErrorTypeHandler("errors.errorString", callback) // Act - code, response := NewErrorResponse(err) + code, response := NewErrorResponse(context.Background(), assert.AnError) // Assert + assert.Equal(t, assert.AnError, calledWithError) assert.Equal(t, 123, code) assert.Equal(t, expectedResponse, response) } -//nolint:paralleltest // Because of global state +// These are parallel because it uses the 'from' variant + func TestErrorResponseFrom_ReturnsGenericErrorOnNotFound(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() registry.SetDefaultResponse(123, "test") - err := errors.New("test error") // Act - code, response := NewErrorResponseFrom(registry, err) + code, response := NewErrorResponseFrom(registry, context.Background(), assert.AnError) // Assert assert.Equal(t, 123, code) assert.Equal(t, "test", response) } -//nolint:paralleltest // Because of global state -func TestErrorResponseFrom_ReturnsErrorA(t *testing.T) { +func TestErrorResponseFrom_UsesDefaultCallbackOnNotFound(t *testing.T) { + t.Parallel() + // 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 + } + + registry.RegisterDefaultHandler(callback) + + ctx := context.WithValue(context.Background(), ErrorA{}, "good") + + // Act + code, response := NewErrorResponseFrom(registry, ctx, assert.AnError) + + // Assert + assert.Equal(t, expectedResponse, response) + assert.Equal(t, code, http.StatusInternalServerError) + + assert.Equal(t, ctx, calledWithCtx) + assert.Equal(t, assert.AnError, calledWithErr) +} + +func TestErrorResponseFrom_ReturnsErrorAWithContext(t *testing.T) { t.Parallel() // Arrange registry := NewErrorRegistry() @@ -137,23 +171,31 @@ func TestErrorResponseFrom_ReturnsErrorA(t *testing.T) { Errors: map[string]any{"error": "It was the man with one hand!"}, } - callback := func(err *aError) (int, Response) { - return 500, expectedResponse + var calledWithErr *ErrorA + var calledWithCtx context.Context + callback := func(ctx context.Context, err *ErrorA) (int, any) { + calledWithErr = err + calledWithCtx = ctx + return http.StatusInternalServerError, expectedResponse } - err := &aError{message: "It was the man with one hand!"} + err := &ErrorA{message: "It was the man with one hand!"} RegisterErrorHandlerOn(registry, callback) + ctx := context.WithValue(context.Background(), ErrorA{}, "cool") + // Act - code, response := NewErrorResponseFrom(registry, err) + code, response := NewErrorResponseFrom(registry, ctx, err) // Assert assert.Equal(t, http.StatusInternalServerError, code) assert.Equal(t, expectedResponse, response) + + assert.Equal(t, err, calledWithErr) + assert.Equal(t, ctx, calledWithCtx) } -//nolint:paralleltest // Because of global state func TestErrorResponseFrom_ReturnsErrorB(t *testing.T) { t.Parallel() // Arrange @@ -162,23 +204,59 @@ func TestErrorResponseFrom_ReturnsErrorB(t *testing.T) { Errors: map[string]any{"error": "It was the man with one hand!"}, } - callback := func(err *bError) (int, Response) { - return 500, expectedResponse + var calledWithErr *ErrorB + callback := func(ctx context.Context, err *ErrorB) (int, any) { + calledWithErr = err + return http.StatusInternalServerError, expectedResponse + } + + err := &ErrorB{message: "It was the man with one hand!"} + + RegisterErrorHandlerOn(registry, callback) + + // Act + code, response := NewErrorResponseFrom(registry, context.Background(), err) + + // Assert + assert.Equal(t, calledWithErr, err) + + assert.Equal(t, http.StatusInternalServerError, code) + assert.Equal(t, expectedResponse, response) +} + +func TestErrorResponseFrom_ReturnsErrorC(t *testing.T) { + t.Parallel() + // Arrange + registry := NewErrorRegistry() + expectedResponse := Response{ + Errors: map[string]any{"error": "It was the man with one hand!"}, + } + + var calledWithErr []ErrorC + callback := func(ctx context.Context, err ErrorC) (int, any) { + calledWithErr = append(calledWithErr, err) + return http.StatusInternalServerError, expectedResponse } - err := &bError{message: "It was the man with one hand!"} + err := ErrorC(ErrorB{message: "It was the man with one hand!"}) + err2 := ErrorC(errors.New("It was the man with one hand!")) RegisterErrorHandlerOn(registry, callback) // Act - code, response := NewErrorResponseFrom(registry, err) + code, response := NewErrorResponseFrom(registry, context.Background(), err) + code2, response2 := NewErrorResponseFrom(registry, context.Background(), err2) // 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) } -//nolint:paralleltest // Because of global state func TestErrorResponseFrom_ReturnsErrorBInInterface(t *testing.T) { t.Parallel() // Arrange @@ -187,24 +265,26 @@ func TestErrorResponseFrom_ReturnsErrorBInInterface(t *testing.T) { Errors: map[string]any{"error": "It was the man with one hand!"}, } - callback := func(err *bError) (int, Response) { - return 500, expectedResponse + var calledWithErr error + callback := func(ctx context.Context, err *ErrorB) (int, any) { + calledWithErr = err + return http.StatusInternalServerError, expectedResponse } - var err error = &bError{message: "It was the man with one hand!"} + var err error = &ErrorB{message: "It was the man with one hand!"} RegisterErrorHandlerOn(registry, callback) // Act - code, response := NewErrorResponseFrom(registry, err) + code, response := NewErrorResponseFrom(registry, context.Background(), err) // Assert + assert.Equal(t, calledWithErr, err) assert.Equal(t, http.StatusInternalServerError, code) assert.Equal(t, expectedResponse, response) } func TestErrorResponseFrom_ReturnsErrorStrings(t *testing.T) { - t.Parallel() tests := []string{ "Something went completely wrong!", "Record not found", @@ -213,14 +293,17 @@ func TestErrorResponseFrom_ReturnsErrorStrings(t *testing.T) { for _, errorString := range tests { errorString := errorString t.Run(errorString, func(t *testing.T) { - t.Parallel() // Arrange registry := NewErrorRegistry() expectedResponse := Response{ Errors: map[string]any{"error": errorString}, } - callback := func(err string) (int, Response) { + 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}, } @@ -230,10 +313,14 @@ func TestErrorResponseFrom_ReturnsErrorStrings(t *testing.T) { RegisterStringErrorHandlerOn(registry, errorString, callback) + ctx := context.WithValue(context.Background(), ErrorA{}, "good") + // Act - code, response := NewErrorResponseFrom(registry, err) + 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) }) @@ -241,15 +328,22 @@ func TestErrorResponseFrom_ReturnsErrorStrings(t *testing.T) { } func TestErrorResponseFrom_CanConfigureMultipleErrorStrings(t *testing.T) { - t.Parallel() // Arrange registry := NewErrorRegistry() - callback1 := func(err string) (int, Response) { + var callback1CalledWithString string + var callback1CalledWithContext context.Context + callback1 := func(ctx context.Context, err string) (int, any) { + callback1CalledWithContext = ctx + callback1CalledWithString = err return 456, Response{} } - callback2 := func(err string) (int, Response) { + var callback2CalledWithString string + var callback2CalledWithContext context.Context + callback2 := func(ctx context.Context, err string) (int, any) { + callback2CalledWithContext = ctx + callback2CalledWithString = err return 123, Response{} } @@ -259,17 +353,24 @@ func TestErrorResponseFrom_CanConfigureMultipleErrorStrings(t *testing.T) { err1 := errors.New("callback1") err2 := errors.New("callback2") + ctx := context.WithValue(context.Background(), ErrorA{}, "anything") + // Act - code1, _ := NewErrorResponseFrom(registry, err1) - code2, _ := NewErrorResponseFrom(registry, err2) + 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) { - t.Parallel() tests := []string{ "Something went completely wrong!", "Record not found", @@ -278,14 +379,17 @@ func TestErrorResponseFrom_ReturnsCustomErrorHandlers(t *testing.T) { for _, errorString := range tests { errorString := errorString t.Run(errorString, func(t *testing.T) { - t.Parallel() // Arrange registry := NewErrorRegistry() expectedResponse := Response{ Errors: map[string]any{"error": errorString}, } - callback := func(err error) (int, Response) { + 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()}, } @@ -293,12 +397,16 @@ func TestErrorResponseFrom_ReturnsCustomErrorHandlers(t *testing.T) { err := errors.New(errorString) - RegisterCustomErrorTypeHandlerOn(registry, "*errors.errorString", callback) + RegisterCustomErrorTypeHandlerOn(registry, "errors.errorString", callback) + + ctx := context.WithValue(context.Background(), ErrorA{}, "good") // Act - code, response := NewErrorResponseFrom(registry, err) + 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) }) @@ -311,7 +419,7 @@ func TestErrorResponseFrom_ReturnsGenericErrorOnTypeNotFound(t *testing.T) { registry := NewErrorRegistry() // Act - code, response := NewErrorResponseFrom(registry, &bError{}) + code, response := NewErrorResponseFrom(registry, context.Background(), &ErrorB{}) // Assert assert.Equal(t, http.StatusInternalServerError, code) diff --git a/examples_test.go b/examples_test.go index 655bb4c..6b51d83 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,6 +1,9 @@ package ginerr -import "net/http" +import ( + "context" + "net/http" +) type Response struct { Errors map[string]any `json:"errors,omitempty"` @@ -13,7 +16,7 @@ func (m MyError) Error() string { } func ExampleRegisterErrorHandler() { - handler := func(myError *MyError) (int, any) { + handler := func(ctx context.Context, myError *MyError) (int, any) { return http.StatusInternalServerError, Response{ Errors: map[string]any{ "error": myError.Error(), @@ -27,7 +30,7 @@ func ExampleRegisterErrorHandler() { func ExampleRegisterErrorHandlerOn() { registry := NewErrorRegistry() - handler := func(myError *MyError) (int, any) { + handler := func(ctx context.Context, myError *MyError) (int, any) { return http.StatusInternalServerError, Response{ Errors: map[string]any{ "error": myError.Error(), @@ -39,7 +42,7 @@ func ExampleRegisterErrorHandlerOn() { } func ExampleRegisterStringErrorHandler() { - handler := func(myError string) (int, any) { + handler := func(ctx context.Context, myError string) (int, any) { return http.StatusInternalServerError, Response{ Errors: map[string]any{ "error": myError, @@ -53,7 +56,7 @@ func ExampleRegisterStringErrorHandler() { func ExampleRegisterStringErrorHandlerOn() { registry := NewErrorRegistry() - handler := func(myError string) (int, any) { + handler := func(ctx context.Context, myError string) (int, any) { return http.StatusInternalServerError, Response{ Errors: map[string]any{ "error": myError, diff --git a/go.mod b/go.mod index c1c8b78..e5fbc7a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ing-bank/ginerr +module github.com/ing-bank/ginerr/v2 go 1.20 diff --git a/v2/.gitignore b/v2/.gitignore deleted file mode 100644 index f2eb195..0000000 --- a/v2/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -.idea/ -.vscode/ - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out diff --git a/v2/LICENSE b/v2/LICENSE deleted file mode 100644 index 3d02986..0000000 --- a/v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 ING - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/v2/Makefile b/v2/Makefile deleted file mode 100644 index caf5866..0000000 --- a/v2/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -MAKEFLAGS := --no-print-directory --silent - -default: help - -help: - @echo "Please use 'make ' where is one of" - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z\._-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) - -t: test -test: fmt ## Run unit tests, alias: t - go test ./... -timeout=30s -parallel=8 - -fmt: ## Format go code - @go mod tidy - @gofumpt -l -w . - -tools: ## Install extra tools for development - go install mvdan.cc/gofumpt@latest - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - -lint: ## Lint the code locally - golangci-lint run diff --git a/v2/README.md b/v2/README.md deleted file mode 100644 index af48369..0000000 --- a/v2/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# 🦁 Gin Error Registry - -[![Go package](https://github.com/ing-bank/ginerr/actions/workflows/test.yaml/badge.svg)](https://github.com/ing-bank/ginerr/actions/workflows/test.yaml) -![GitHub](https://img.shields.io/github/license/ing-bank/ginerr) -![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/ing-bank/ginerr) - -Sending any error back to the user can pose a [big security risk](https://owasp.org/www-community/Improper_Error_Handling). -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 - -## ⬇️ Installation - -`go get github.com/ing-bank/ginerr/v2` - -## 📋 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)) -} -``` - -## 🚀 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 -4. Run `make lint` to lint your code - -You can run `make` to see a list of useful commands. - -## 🔭 Future Plans - -Nothing here yet! diff --git a/v2/errors.go b/v2/errors.go deleted file mode 100644 index c3eca1f..0000000 --- a/v2/errors.go +++ /dev/null @@ -1,159 +0,0 @@ -package ginerr - -import ( - "context" - "net/http" - "reflect" -) - -const defaultCode = http.StatusInternalServerError - -var DefaultErrorRegistry = NewErrorRegistry() - -type ( - internalHandler func(ctx context.Context, err error) (int, any) - internalStringHandler func(ctx context.Context, err string) (int, any) -) - -func NewErrorRegistry() *ErrorRegistry { - registry := &ErrorRegistry{ - handlers: make(map[string]internalHandler), - stringHandlers: make(map[string]internalStringHandler), - DefaultCode: defaultCode, - } - - // 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()) - } - - return registry.defaultResponse(ctx, err) - } - - return registry -} - -type ErrorRegistry struct { - // handlers are used when we know the type of the error - handlers map[string]internalHandler - - // 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 -} - -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 -} - -// 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) -} - -// 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) - - // If a handler is registered for the error type, use it. - if entry, ok := registry.handlers[errorType]; ok { - return entry(ctx, err) - } - - return registry.defaultResponse(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) -} - -// 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)) - - // 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 -} - -// 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) -} - -// 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 -} - -// 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() - - 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() - } - - return errorType -} diff --git a/v2/errors_test.go b/v2/errors_test.go deleted file mode 100644 index 2dd62ca..0000000 --- a/v2/errors_test.go +++ /dev/null @@ -1,443 +0,0 @@ -package ginerr - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Register functions are tested through NewErrorResponse[E error] - -type ErrorA struct { - message string -} - -func (e ErrorA) Error() string { - return e.message -} - -type ErrorB struct { - message string -} - -func (e ErrorB) Error() string { - return e.message -} - -type ErrorC error - -// The top ones are not parallel because it uses the DefaultErrorRegistry, which is a global - -func TestErrorResponse_UsesDefaultErrorRegistry(t *testing.T) { - // Arrange - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } - - var calledWithError *ErrorA - callback := func(ctx context.Context, err *ErrorA) (int, any) { - calledWithError = err - return 634, Response{ - Errors: map[string]any{"error": err.Error()}, - } - } - - err := &ErrorA{message: "It was the man with one hand!"} - - RegisterErrorHandler(callback) - - // Act - code, response := NewErrorResponse(context.Background(), err) - - // Assert - assert.Equal(t, err, calledWithError) - assert.Equal(t, 634, code) - 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) { - t.Parallel() - // Arrange - registry := NewErrorRegistry() - registry.SetDefaultResponse(123, "test") - - // Act - code, response := NewErrorResponseFrom(registry, context.Background(), assert.AnError) - - // Assert - assert.Equal(t, 123, code) - assert.Equal(t, "test", response) -} - -func TestErrorResponseFrom_UsesDefaultCallbackOnNotFound(t *testing.T) { - t.Parallel() - // 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 - } - - registry.RegisterDefaultHandler(callback) - - ctx := context.WithValue(context.Background(), ErrorA{}, "good") - - // Act - code, response := NewErrorResponseFrom(registry, ctx, assert.AnError) - - // Assert - assert.Equal(t, expectedResponse, response) - assert.Equal(t, code, http.StatusInternalServerError) - - assert.Equal(t, ctx, calledWithCtx) - assert.Equal(t, assert.AnError, calledWithErr) -} - -func TestErrorResponseFrom_ReturnsErrorAWithContext(t *testing.T) { - t.Parallel() - // Arrange - registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } - - var calledWithErr *ErrorA - var calledWithCtx context.Context - callback := func(ctx context.Context, err *ErrorA) (int, any) { - calledWithErr = err - calledWithCtx = ctx - return http.StatusInternalServerError, expectedResponse - } - - err := &ErrorA{message: "It was the man with one hand!"} - - RegisterErrorHandlerOn(registry, callback) - - ctx := context.WithValue(context.Background(), ErrorA{}, "cool") - - // Act - code, response := NewErrorResponseFrom(registry, ctx, err) - - // Assert - assert.Equal(t, http.StatusInternalServerError, code) - assert.Equal(t, expectedResponse, response) - - assert.Equal(t, err, calledWithErr) - assert.Equal(t, ctx, calledWithCtx) -} - -func TestErrorResponseFrom_ReturnsErrorB(t *testing.T) { - t.Parallel() - // Arrange - registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } - - var calledWithErr *ErrorB - callback := func(ctx context.Context, err *ErrorB) (int, any) { - calledWithErr = err - return http.StatusInternalServerError, expectedResponse - } - - err := &ErrorB{message: "It was the man with one hand!"} - - RegisterErrorHandlerOn(registry, callback) - - // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) - - // Assert - assert.Equal(t, calledWithErr, err) - - assert.Equal(t, http.StatusInternalServerError, code) - assert.Equal(t, expectedResponse, response) -} - -func TestErrorResponseFrom_ReturnsErrorC(t *testing.T) { - t.Parallel() - // Arrange - registry := NewErrorRegistry() - expectedResponse := Response{ - Errors: map[string]any{"error": "It was the man with one hand!"}, - } - - var calledWithErr []ErrorC - callback := func(ctx context.Context, err ErrorC) (int, any) { - calledWithErr = append(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!")) - - RegisterErrorHandlerOn(registry, callback) - - // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) - code2, response2 := NewErrorResponseFrom(registry, context.Background(), err2) - - // 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) -} - -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!"}, - } - - var calledWithErr error - callback := func(ctx context.Context, err *ErrorB) (int, any) { - calledWithErr = err - return http.StatusInternalServerError, expectedResponse - } - - var err error = &ErrorB{message: "It was the man with one hand!"} - - RegisterErrorHandlerOn(registry, callback) - - // Act - code, response := NewErrorResponseFrom(registry, context.Background(), err) - - // Assert - assert.Equal(t, calledWithErr, err) - assert.Equal(t, http.StatusInternalServerError, code) - 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{}) - - // 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/v2/examples_test.go b/v2/examples_test.go deleted file mode 100644 index 6b51d83..0000000 --- a/v2/examples_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package ginerr - -import ( - "context" - "net/http" -) - -type Response struct { - Errors map[string]any `json:"errors,omitempty"` -} - -type MyError struct{} - -func (m MyError) Error() string { - return "Something went wrong!" -} - -func ExampleRegisterErrorHandler() { - handler := func(ctx context.Context, myError *MyError) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError.Error(), - }, - } - } - - RegisterErrorHandler(handler) -} - -func ExampleRegisterErrorHandlerOn() { - registry := NewErrorRegistry() - - handler := func(ctx context.Context, myError *MyError) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError.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, - }, - } - } - - RegisterStringErrorHandler("some string error", handler) -} - -func ExampleRegisterStringErrorHandlerOn() { - registry := NewErrorRegistry() - - handler := func(ctx context.Context, myError string) (int, any) { - return http.StatusInternalServerError, Response{ - Errors: map[string]any{ - "error": myError, - }, - } - } - - RegisterStringErrorHandlerOn(registry, "some string error", handler) -} diff --git a/v2/go.mod b/v2/go.mod deleted file mode 100644 index e5fbc7a..0000000 --- a/v2/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/ing-bank/ginerr/v2 - -go 1.20 - -require github.com/stretchr/testify v1.8.1 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/v2/go.sum b/v2/go.sum deleted file mode 100644 index b2d57a1..0000000 --- a/v2/go.sum +++ /dev/null @@ -1,33 +0,0 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=