🔀 Merge branch 'rel/0.1.0' into stable
This commit is contained in:
		
				commit
				
					
						717b585673
					
				
			
		
					 9 changed files with 288 additions and 15 deletions
				
			
		
							
								
								
									
										17
									
								
								CHANGELOG.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| # Changelog | ||||
| 
 | ||||
| ## [0.1.0] - 2024-01-22 | ||||
| 
 | ||||
| 🎉 Initial release | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - HandlerWithError | ||||
|   + HandlerWithErrorWrapper | ||||
| - ErrorMiddleware | ||||
|   + Option | ||||
|     * Transformer | ||||
|       - WithTransformer | ||||
|       - WithDefaultTransformer | ||||
|     * LoggerFunc | ||||
|       - WithLogger | ||||
							
								
								
									
										84
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										84
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,3 +1,87 @@ | |||
| # Gin Error Handler | ||||
| 
 | ||||
| A Gin middleware and wrapper functions to make handling errors easier. | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| Use the module in the usual way: `codeberg.org/danjones000/gin-error-handler`. | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ```go | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
|     "net/http" | ||||
|     handler "codeberg.org/danjones000/gin-error-handler" | ||||
|     rErrors "codeberg.org/danjones000/responsable-errors" | ||||
|     "github.com/go-playground/validator/v10" | ||||
|     "github.com/gin-gonic/gin" | ||||
| ) | ||||
| 
 | ||||
| var _ rErrors.ResponsableError = new(vError) | ||||
| 
 | ||||
| type vError struct { | ||||
|     validator.ValidationErrors | ||||
| } | ||||
| 
 | ||||
| func (ve *vError) Msg() string { | ||||
|     return "Validation Error" | ||||
| } | ||||
| 
 | ||||
| func (ve *vError) Status() int { | ||||
|     return http.StatusBadRequest | ||||
| } | ||||
| 
 | ||||
| func (ve *vError) JSON() any { | ||||
|     errs := make([]map[string]string, len(ve)) | ||||
|     for i, fe := range ve { | ||||
|         errs[i] = map[string]string{"field":fe.Field(),"error":fe.Error()} | ||||
|     } | ||||
|     return map[string][]map[string]string{"errors":errs} | ||||
| } | ||||
| 
 | ||||
| func (ve *vError) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(e.JSON()) | ||||
| } | ||||
| 
 | ||||
| func (ve *vError) Unwrap() error { | ||||
| 	return ve.ValidationErrors | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
|     r := gin.New() | ||||
|     r.Use(handler.ErrorMiddleware( | ||||
|         handler.WithTransformer(func (err error) rErrors.ResponsableError { | ||||
|             var ve validator.ValidationErrors | ||||
|             if !errors.As(err, &ve) { | ||||
|                 return nil | ||||
|             } | ||||
|             return &vError{ve} | ||||
|         }), | ||||
|         handler.WithDefaultTransformer(), | ||||
|         handler.WithLogger(ctx context.Context, err rErrors.ResponsableError) { | ||||
|             log.Print(err) | ||||
|         }) | ||||
| 
 | ||||
|     r.GET("/user", handler.HandlerWithErrorWrapper(func (c *gin.Context) error { | ||||
|         var qu struct { | ||||
|             id int `binding:"required"` | ||||
|         }{} | ||||
|         if err := c.ShouldBindQuery(&qu); err != nil { | ||||
|             return err | ||||
|         } | ||||
|         user, err := user.Get(qu.id) | ||||
|         if err != nil { | ||||
|             return err | ||||
|         } | ||||
|         if user == nil { | ||||
|             return rErrors.NewNotFound("User not found") | ||||
|         } | ||||
| 
 | ||||
|         c.JSON(200, user) | ||||
|     })) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| In our example, if `ShouldBindQuery` fails validation, we'll return a nicely formatted error with each validation error, due to our custom transformer. If no user is found, we'll return a 404, with an error message. And any errors are logged to stdout. | ||||
|  |  | |||
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -3,7 +3,7 @@ module codeberg.org/danjones000/gin-error-handler | |||
| go 1.21.5 | ||||
| 
 | ||||
| require ( | ||||
| 	codeberg.org/danjones000/responsable-errors v0.1.1 | ||||
| 	codeberg.org/danjones000/responsable-errors v0.2.0 | ||||
| 	github.com/gin-gonic/gin v1.9.1 | ||||
| 	github.com/stretchr/testify v1.8.4 | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										2
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -1,5 +1,7 @@ | |||
| codeberg.org/danjones000/responsable-errors v0.1.1 h1:WTWo0egPNsp+kEKRK8R+yc/bfdEDUbwEjMnqgA7IklA= | ||||
| codeberg.org/danjones000/responsable-errors v0.1.1/go.mod h1:susEj39A/bflyej4tRirtuVKkmfUdhR2Skljwd/1ndI= | ||||
| codeberg.org/danjones000/responsable-errors v0.2.0 h1:WUTBSaKQzkXHDYraHt3mxB4QHqVk78sKR/VeteXM1OY= | ||||
| codeberg.org/danjones000/responsable-errors v0.2.0/go.mod h1:susEj39A/bflyej4tRirtuVKkmfUdhR2Skljwd/1ndI= | ||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||
| github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | ||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||
|  |  | |||
							
								
								
									
										17
									
								
								handler.go
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								handler.go
									
										
									
									
									
								
							|  | @ -2,8 +2,25 @@ package handler | |||
| 
 | ||||
| import "github.com/gin-gonic/gin" | ||||
| 
 | ||||
| // This is essentially the same as [gin.HandlerFunc], but allows returning | ||||
| // an error. This allows for the more idiomatic use: | ||||
| // | ||||
| //	func UserHandler (c *gin.Context) error { | ||||
| //	    user, err := db.GetUser(c.Get("user")) | ||||
| //	    if err != nil { | ||||
| //	        return err | ||||
| //	    } | ||||
| //	    c.JSON(200, user) | ||||
| //	} | ||||
| type HandlerWithError func(c *gin.Context) error | ||||
| 
 | ||||
| // Allows you to actually use a [HandlerWithError] as a [gin.HandlerFunc]. | ||||
| // If an error is returned, it adds it to the context with c.Error(err). | ||||
| // This needs to be handled with a middleware, such as [ErrorMiddleware] | ||||
| // | ||||
| // Usage: | ||||
| // | ||||
| //	c.GET("/user", HandlerWithErrorWrapper(UserHandler)) | ||||
| func HandlerWithErrorWrapper(h HandlerWithError) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		err := h(c) | ||||
|  |  | |||
|  | @ -7,7 +7,17 @@ import ( | |||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
| 
 | ||||
| func ErrorMiddleware() gin.HandlerFunc { | ||||
| // Returns a gin middleware which writes a response with the error in the context. | ||||
| func ErrorMiddleware(opts ...Option) gin.HandlerFunc { | ||||
| 	conf := config{} | ||||
| 	for _, opt := range opts { | ||||
| 		conf = opt(conf) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(conf.transformers) == 0 { | ||||
| 		conf.transformers = []Transformer{ginTransformer} | ||||
| 	} | ||||
| 
 | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Next() | ||||
| 		err := c.Errors.Last() | ||||
|  | @ -16,26 +26,36 @@ func ErrorMiddleware() gin.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		var re rErrors.ResponsableError | ||||
| 
 | ||||
| 		errors.As(err, &re) | ||||
| 		// If we have at least one that's a ResponsableError, we should use it | ||||
| 		for _, err = range c.Errors { | ||||
| 			errors.As(err, &re) | ||||
| 			if re != nil { | ||||
| 				break | ||||
| 			} | ||||
| 			errors.As(err, &re) | ||||
| 		} | ||||
| 
 | ||||
| 		// @todo we need to add some way to do custom handling | ||||
| 
 | ||||
| 		// @todo Refactor this with 👆 | ||||
| 		if re == nil { | ||||
| 			switch err.Type { | ||||
| 			case gin.ErrorTypePrivate: | ||||
| 				re = rErrors.NewInternalError("%w", err) | ||||
| 			default: | ||||
| 				re = rErrors.NewBadRequest("%w", err) | ||||
| 		// Next, let's check our transformers | ||||
| 		for _, trans := range conf.transformers { | ||||
| 			if re != nil { | ||||
| 				break | ||||
| 			} | ||||
| 			re = trans(err) | ||||
| 		} | ||||
| 
 | ||||
| 		c.JSON(re.GetStatus(), gin.H{"error": re.GetMsg()}) | ||||
| 		// Still couldn't find one, so it's a 500 | ||||
| 		if re == nil { | ||||
| 			re = rErrors.NewInternalError("%w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		for _, logger := range conf.loggers { | ||||
| 			logger(c, re) | ||||
| 		} | ||||
| 
 | ||||
| 		c.Set("rendered_error", re) | ||||
| 
 | ||||
| 		// @todo check a response hasn't already been sent | ||||
| 		c.JSON(re.Status(), re) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| package handler | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	rErrors "codeberg.org/danjones000/responsable-errors" | ||||
|  | @ -43,6 +44,10 @@ func (s *MiddlewareTestSuite) do(err ...error) { | |||
| 
 | ||||
| func (s *MiddlewareTestSuite) doParse(err ...error) map[string]any { | ||||
| 	s.do(err...) | ||||
| 	return s.parse() | ||||
| } | ||||
| 
 | ||||
| func (s *MiddlewareTestSuite) parse() map[string]any { | ||||
| 	var out map[string]any | ||||
| 	jsonErr := json.Unmarshal(s.w.Body.Bytes(), &out) | ||||
| 	s.Assert().Nil(jsonErr) | ||||
|  | @ -54,7 +59,7 @@ func (s *MiddlewareTestSuite) TestNoError() { | |||
| 	s.do() | ||||
| 	s.Assert().Equal("", s.w.Body.String()) | ||||
| } | ||||
|   | ||||
| 
 | ||||
| func (s *MiddlewareTestSuite) TestResError() { | ||||
| 	msg := "I can't find it" | ||||
| 	err := rErrors.NewNotFound(msg) | ||||
|  | @ -98,3 +103,30 @@ func (s *MiddlewareTestSuite) TestOtherError() { | |||
| 	s.Assert().Equal("Unknown Error", outMsg) | ||||
| 	s.Assert().Equal(http.StatusInternalServerError, s.w.Code) | ||||
| } | ||||
| 
 | ||||
| func (s *MiddlewareTestSuite) TestNoWorkingTransformer() { | ||||
| 	var noop Transformer = func(err error) rErrors.ResponsableError { | ||||
| 		return nil | ||||
| 	} | ||||
| 	err := errors.New("Foo") | ||||
| 	s.ctx.Error(err) | ||||
| 	ErrorMiddleware(WithTransformer(noop))(s.ctx) | ||||
| 
 | ||||
| 	out := s.parse() | ||||
| 	outMsg, ok := out["error"].(string) | ||||
| 	s.Assert().True(ok) | ||||
| 	s.Assert().Equal("Unknown Error", outMsg) | ||||
| 	s.Assert().Equal(http.StatusInternalServerError, s.w.Code) | ||||
| } | ||||
| 
 | ||||
| func (s *MiddlewareTestSuite) TestLogger() { | ||||
| 	buff := strings.Builder{} | ||||
| 	var l LoggerFunc = func(c *gin.Context, err rErrors.ResponsableError) { | ||||
| 		buff.WriteString("Err: " + err.Error()) | ||||
| 	} | ||||
| 	err := errors.New("Foo") | ||||
| 	s.ctx.Error(err) | ||||
| 	ErrorMiddleware(WithLogger(l))(s.ctx) | ||||
| 
 | ||||
| 	s.Assert().Equal("Err: Foo", buff.String()) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										63
									
								
								options.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								options.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| package handler | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 
 | ||||
| 	rErrors "codeberg.org/danjones000/responsable-errors" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
| 
 | ||||
| type config struct { | ||||
| 	loggers      []LoggerFunc | ||||
| 	transformers []Transformer | ||||
| } | ||||
| 
 | ||||
| // A function which should be used to log the responded error. | ||||
| type LoggerFunc func(*gin.Context, rErrors.ResponsableError) | ||||
| 
 | ||||
| // A function which should potentially return a [rErrors.ResponsableError] which wraps the provided error. | ||||
| type Transformer func(error) rErrors.ResponsableError | ||||
| 
 | ||||
| // An option that customizes the behavior of [ErrorMiddleware] | ||||
| type Option func(config) config | ||||
| 
 | ||||
| // Provides an [Option] which adds the specified [LoggerFunc] to the [ErrorMiddleware] | ||||
| // | ||||
| // Multiple [LoggerFunc] may be added. | ||||
| func WithLogger(logger LoggerFunc) Option { | ||||
| 	return func(c config) config { | ||||
| 		c.loggers = append(c.loggers, logger) | ||||
| 		return c | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Provides an [Option] which adds the specified [Transformer] to the [ErrorMiddleware] | ||||
| // | ||||
| // Multiple [Transformer] may (and probably should) be added. | ||||
| func WithTransformer(tr Transformer) Option { | ||||
| 	return func(c config) config { | ||||
| 		c.transformers = append(c.transformers, tr) | ||||
| 		return c | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Provides an [Option] which adds the default [Transformer] to the [ErrorMiddleware] | ||||
| // | ||||
| // This [Transformer] handles a [gin.Error]. | ||||
| func WithDefaultTransformer() Option { | ||||
| 	return WithTransformer(ginTransformer) | ||||
| } | ||||
| 
 | ||||
| func ginTransformer(er error) rErrors.ResponsableError { | ||||
| 	var err *gin.Error | ||||
| 	if !errors.As(er, &err) { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	switch err.Type { | ||||
| 	case gin.ErrorTypePrivate: | ||||
| 		return rErrors.NewInternalError("%w", err) | ||||
| 	default: | ||||
| 		return rErrors.NewBadRequest("%w", err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										38
									
								
								options_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								options_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| package handler | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	rErrors "codeberg.org/danjones000/responsable-errors" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| var someErr error = errors.New("I am a teapot") | ||||
| 
 | ||||
| var noop Transformer = func(err error) rErrors.ResponsableError { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func TestWithTrans(t *testing.T) { | ||||
| 	c := config{} | ||||
| 	c = WithTransformer(noop)(c) | ||||
| 	assert.Len(t, c.transformers, 1) | ||||
| 	exp := fmt.Sprintf("%p", noop) | ||||
| 	fd := fmt.Sprintf("%p", c.transformers[0]) | ||||
| 	assert.Equal(t, exp, fd) | ||||
| } | ||||
| 
 | ||||
| func TestWithDef(t *testing.T) { | ||||
| 	c := config{} | ||||
| 	c = WithDefaultTransformer()(c) | ||||
| 	assert.Len(t, c.transformers, 1) | ||||
| 	exp := fmt.Sprintf("%p", ginTransformer) | ||||
| 	fd := fmt.Sprintf("%p", c.transformers[0]) | ||||
| 	assert.Equal(t, exp, fd) | ||||
| } | ||||
| 
 | ||||
| func TestGinTransNotGinError(t *testing.T) { | ||||
| 	assert.Nil(t, ginTransformer(someErr)) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue