From 4e3c242044be381fcdface3f51e15989478f80eb Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 20 Jan 2024 00:33:59 +0200 Subject: [PATCH] add new errors.Intercept package-level function --- HISTORY.md | 22 +++ .../routing/http-wire-errors/service/main.go | 27 +++- x/errors/handlers.go | 138 ++++++++++++++---- 3 files changed, 149 insertions(+), 38 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e7de5c23..23f2ad9e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -24,6 +24,28 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene Changes apply to `main` branch. +- New `x/errors.Intercept(func(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error{ ... })` package-level function. + +```go +func main() { + app := iris.New() + + // Create a new service and pass it to the handlers. + service := new(myService) + + app.Post("/", errors.Intercept(responseHandler), errors.CreateHandler(service.Create)) + + // [...] +} + +func responseHandler(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error { + fmt.Printf("intercept: request got: %+v\nresponse sent: %#+v\n", req, resp) + return nil +} +``` + +- Rename `x/errors/ContextValidator.ValidateContext(iris.Context) error` to `x/errors/RequestHandler.HandleRequest(iris.Context) error`. + # Thu, 18 Jan 2024 | v12.2.10 - Simplify the `/core/host` subpackage and remove its `DeferFlow` and `RestoreFlow` methods. These methods are replaced with: `Supervisor.Configure(host.NonBlocking())` before `Serve` and ` Supervisor.Wait(context.Context) error` after `Serve`. diff --git a/_examples/routing/http-wire-errors/service/main.go b/_examples/routing/http-wire-errors/service/main.go index 6bbb6cee..f97fcd01 100644 --- a/_examples/routing/http-wire-errors/service/main.go +++ b/_examples/routing/http-wire-errors/service/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "strings" "github.com/kataras/iris/v12" @@ -16,10 +17,10 @@ func main() { // Create a new service and pass it to the handlers. service := new(myService) - app.Post("/", createHandler(service)) // OR: errors.CreateHandler(service.Create) - app.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{})) - app.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated) - app.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id")) + app.Post("/", errors.Intercept(afterServiceCallButBeforeDataSent), createHandler(service)) // OR: errors.CreateHandler(service.Create) + app.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{})) + app.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated) + app.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id")) app.Listen(":8080") } @@ -95,7 +96,7 @@ type ( } ) -// ValidateContext implements the errors.ContextValidator interface. +// HandleRequest implements the errors.RequestHandler interface. // It validates the request body and returns an error if the request body is invalid. // You can also alter the "r" CreateRequest before calling the service method, // e.g. give a default value to a field if it's empty or set an ID based on a path parameter. @@ -111,7 +112,7 @@ type ( // validation.Slice("hobbies", r.Hobbies).Length(1, 10), // ) // } -func (r *CreateRequest) ValidateContext(ctx iris.Context) error { +func (r *CreateRequest) HandleRequest(ctx iris.Context) error { // To pass custom validation functions: // return validation.Join( // validation.String("fullname", r.Fullname).Func(customStringFuncHere), @@ -152,6 +153,20 @@ func (r *CreateRequest) ValidateContext(ctx iris.Context) error { */ } +/* +// HandleResponse implements the errors.ResponseHandler interface. +func (r *CreateRequest) HandleResponse(ctx iris.Context, resp *CreateResponse) error { + fmt.Printf("request got: %+v\nresponse sent: %#+v\n", r, resp) + + return nil // fmt.Errorf("let's fire an internal server error just for the shake of the example") // return nil to continue. +} +*/ + +func afterServiceCallButBeforeDataSent(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error { + fmt.Printf("intercept: request got: %+v\nresponse sent: %#+v\n", req, resp) + return nil +} + func (s *myService) Create(ctx context.Context, in CreateRequest) (CreateResponse, error) { arr := strings.Split(in.Fullname, " ") firstname, lastname := arr[0], arr[1] diff --git a/x/errors/handlers.go b/x/errors/handlers.go index 4ac8795a..53719a89 100644 --- a/x/errors/handlers.go +++ b/x/errors/handlers.go @@ -141,13 +141,13 @@ type ResponseOnlyErrorFunc[T any] interface { func(stdContext.Context, T) error } -// ContextValidatorFunc is a function which takes a context and a generic type T and returns an error. +// ContextRequestFunc is a function which takes a context and a generic type T and returns an error. // It is used to validate the context before calling a service function. // // See Validation package-level function. -type ContextValidatorFunc[T any] func(*context.Context, T) error +type ContextRequestFunc[T any] func(*context.Context, T) error -const contextValidatorFuncKey = "iris.errors.ContextValidatorFunc" +const contextRequestHandlerFuncKey = "iris.errors.ContextRequestHandler" // Validation adds a context validator function to the context. // It returns a middleware which can be used to validate the context before calling a service function. @@ -164,31 +164,31 @@ const contextValidatorFuncKey = "iris.errors.ContextValidatorFunc" // validation.Slice("hobbies", r.Hobbies).Length(1, 10), // ) // } -func Validation[T any](validators ...ContextValidatorFunc[T]) context.Handler { - validator := joinContextValidators[T](validators) +func Validation[T any](validators ...ContextRequestFunc[T]) context.Handler { + validator := joinContextRequestFuncs[T](validators) return func(ctx *context.Context) { - ctx.Values().Set(contextValidatorFuncKey, validator) + ctx.Values().Set(contextRequestHandlerFuncKey, validator) ctx.Next() } } -func joinContextValidators[T any](validators []ContextValidatorFunc[T]) ContextValidatorFunc[T] { - if len(validators) == 0 || validators[0] == nil { - panic("at least one validator is required") +func joinContextRequestFuncs[T any](requestHandlerFuncs []ContextRequestFunc[T]) ContextRequestFunc[T] { + if len(requestHandlerFuncs) == 0 || requestHandlerFuncs[0] == nil { + panic("at least one context request handler function is required") } - if len(validators) == 1 { - return validators[0] + if len(requestHandlerFuncs) == 1 { + return requestHandlerFuncs[0] } return func(ctx *context.Context, req T) error { - for _, validator := range validators { - if validator == nil { + for _, handler := range requestHandlerFuncs { + if handler == nil { continue } - if err := validator(ctx, req); err != nil { + if err := handler(ctx, req); err != nil { return err } } @@ -197,38 +197,102 @@ func joinContextValidators[T any](validators []ContextValidatorFunc[T]) ContextV } } -// ContextValidator is an interface which can be implemented by a request payload struct +// RequestHandler is an interface which can be implemented by a request payload struct // in order to validate the context before calling a service function. -type ContextValidator interface { - ValidateContext(*context.Context) error +type RequestHandler interface { + HandleRequest(*context.Context) error } -func validateContext[T any](ctx *context.Context, req T) bool { +func validateRequest[T any](ctx *context.Context, req T) bool { var err error // Always run the request's validator first, // so dynamic validators can be customized per path and method. - if contextValidator, ok := any(&req).(ContextValidator); ok { - err = contextValidator.ValidateContext(ctx) + if contextRequestHandler, ok := any(&req).(RequestHandler); ok { + err = contextRequestHandler.HandleRequest(ctx) } if err == nil { - if v := ctx.Values().Get(contextValidatorFuncKey); v != nil { - if contextValidatorFunc, ok := v.(ContextValidatorFunc[T]); ok { - err = contextValidatorFunc(ctx, req) - } else if contextValidatorFunc, ok := v.(ContextValidatorFunc[*T]); ok { // or a pointer of T. - err = contextValidatorFunc(ctx, &req) + if v := ctx.Values().Get(contextRequestHandlerFuncKey); v != nil { + if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[T]); ok && contextRequestHandlerFunc != nil { + err = contextRequestHandlerFunc(ctx, req) + } else if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[*T]); ok && contextRequestHandlerFunc != nil { // or a pointer of T. + err = contextRequestHandlerFunc(ctx, &req) } } } - if err != nil { - if HandleError(ctx, err) { - return false + return err == nil || !HandleError(ctx, err) +} + +// ResponseHandler is an interface which can be implemented by a request payload struct +// in order to handle a response before sending it to the client. +type ResponseHandler[R any, RPointer *R] interface { + HandleResponse(ctx *context.Context, response RPointer) error +} + +// ContextResponseFunc is a function which takes a context, a generic type T and a generic type R and returns an error. +type ContextResponseFunc[T, R any, RPointer *R] func(*context.Context, T, RPointer) error + +const contextResponseHandlerFuncKey = "iris.errors.ContextResponseHandler" + +func validateResponse[T, R any, RPointer *R](ctx *context.Context, req T, resp RPointer) bool { + var err error + + if contextResponseHandler, ok := any(&req).(ResponseHandler[R, RPointer]); ok { + err = contextResponseHandler.HandleResponse(ctx, resp) + } + + if err == nil { + if v := ctx.Values().Get(contextResponseHandlerFuncKey); v != nil { + if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[T, R, RPointer]); ok && contextResponseHandlerFunc != nil { + err = contextResponseHandlerFunc(ctx, req, resp) + } else if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[*T, R, RPointer]); ok && contextResponseHandlerFunc != nil { + err = contextResponseHandlerFunc(ctx, &req, resp) + } } } - return true + return err == nil || !HandleError(ctx, err) +} + +// Intercept adds a context response handler function to the context. +// It returns a middleware which can be used to intercept the response before sending it to the client. +// +// Example Code: +// +// app.Post("/", errors.Intercept(func(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error{ ... }), errors.CreateHandler(service.Create)) +func Intercept[T, R any, RPointer *R](responseHandlers ...ContextResponseFunc[T, R, RPointer]) context.Handler { + responseHandler := joinContextResponseFuncs[T, R, RPointer](responseHandlers) + + return func(ctx *context.Context) { + ctx.Values().Set(contextResponseHandlerFuncKey, responseHandler) + ctx.Next() + } +} + +func joinContextResponseFuncs[T, R any, RPointer *R](responseHandlerFuncs []ContextResponseFunc[T, R, RPointer]) ContextResponseFunc[T, R, RPointer] { + if len(responseHandlerFuncs) == 0 || responseHandlerFuncs[0] == nil { + panic("at least one context response handler function is required") + } + + if len(responseHandlerFuncs) == 1 { + return responseHandlerFuncs[0] + } + + return func(ctx *context.Context, req T, resp RPointer) error { + for _, handler := range responseHandlerFuncs { + if handler == nil { + continue + } + + if err := handler(ctx, req, resp); err != nil { + return err + } + } + + return nil + } } func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) (R, bool) { @@ -247,12 +311,18 @@ func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fn panic("invalid number of arguments") } - if !validateContext(ctx, req) { + if !validateRequest(ctx, req) { var resp R return resp, false } resp, err := fn(ctx, req) + if err == nil { + if !validateResponse(ctx, req, &resp) { + return resp, false + } + } + return resp, !HandleError(ctx, err) } @@ -372,7 +442,7 @@ func List[T, R any, C constraints.Integer | constraints.Float, F ListResponseFun return false } - if !validateContext(ctx, filter) { + if !validateRequest(ctx, filter) { return false } @@ -383,7 +453,11 @@ func List[T, R any, C constraints.Integer | constraints.Float, F ListResponseFun } resp := pagination.NewList(items, int64(totalCount), filter, listOpts) - return Handle(ctx, resp, nil) + if !validateResponse(ctx, filter, resp) { + return false + } + + return Handle(ctx, resp, err) } // ListHandler handles a generic response and error from a service paginated call and sends a JSON response to the client.