Skip to content

Commit

Permalink
Merge pull request #4 from Emptyless/feature/use-reflect
Browse files Browse the repository at this point in the history
use reflect to deduce the errortype
  • Loading branch information
WilliamDeKeyzer authored Jan 3, 2024
2 parents 01c7924 + 7ff5639 commit b8aa4a2
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 7 deletions.
31 changes: 26 additions & 5 deletions v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package ginerr

import (
"context"
"fmt"
"net/http"
"reflect"
)

const defaultCode = http.StatusInternalServerError
Expand All @@ -23,7 +23,7 @@ func NewErrorRegistry() *ErrorRegistry {
}

// Make sure the stringHandlers are available in the handlers
registry.handlers["*errors.errorString"] = func(ctx context.Context, 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(ctx, err.Error())
Expand Down Expand Up @@ -80,8 +80,8 @@ func NewErrorResponse(ctx context.Context, err error) (int, any) {

// NewErrorResponseFrom Returns an error response using the given registry. If no specific handler could be found,
// it will return the defaults.
func NewErrorResponseFrom(registry *ErrorRegistry, ctx context.Context, err error) (int, any) {
errorType := fmt.Sprintf("%T", err)
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 {
Expand All @@ -99,7 +99,7 @@ func RegisterErrorHandler[E error](handler func(context.Context, E) (int, any))
// 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 := 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.
Expand Down Expand Up @@ -136,3 +136,24 @@ func RegisterStringErrorHandler(errorString string, handler func(ctx context.Con
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
}
39 changes: 37 additions & 2 deletions v2/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ 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) {
Expand Down Expand Up @@ -102,7 +104,7 @@ func TestErrorResponse_UsesDefaultErrorRegistryForCustomTypes(t *testing.T) {
}
}

RegisterCustomErrorTypeHandler("*errors.errorString", callback)
RegisterCustomErrorTypeHandler("errors.errorString", callback)

// Act
code, response := NewErrorResponse(context.Background(), assert.AnError)
Expand Down Expand Up @@ -222,6 +224,39 @@ func TestErrorResponseFrom_ReturnsErrorB(t *testing.T) {
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
Expand Down Expand Up @@ -362,7 +397,7 @@ 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")

Expand Down

0 comments on commit b8aa4a2

Please sign in to comment.