From d32eb68ed48d728a1211abd1004c140af9be3b2b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 10 Jan 2024 01:11:32 +0200 Subject: [PATCH] add x/errors.Validation --- HISTORY.md | 1 + .../routing/http-wire-errors/service/main.go | 43 ++++++++-- x/errors/handlers.go | 81 +++++++++++++++++-- 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d8e484ca..0f798363 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -23,6 +23,7 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene Changes apply to `main` branch. +- Add `x/errors.Validation` package-level function to add one or more validations for the request payload before a service call of the below methods. - Add `x/errors.Handler`, `CreateHandler`, `NoContentHandler`, `NoContenetOrNotModifiedHandler` and `ListHandler` ready-to-use handlers for service method calls to Iris Handler. - Add `x/errors.List` package-level function to support `ListObjects(ctx context.Context, opts pagination.ListOptions, f Filter) ([]Object, int64, error)` type of service calls. - Simplify how validation errors on `/x/errors` package works. A new `x/errors/validation` sub-package added to make your life easier (using the powerful Generics feature). diff --git a/_examples/routing/http-wire-errors/service/main.go b/_examples/routing/http-wire-errors/service/main.go index a48eb8d8..cde07d62 100644 --- a/_examples/routing/http-wire-errors/service/main.go +++ b/_examples/routing/http-wire-errors/service/main.go @@ -12,14 +12,35 @@ import ( func main() { app := iris.New() + + /* + 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.PartyConfigure("/", Party()) + app.Listen(":8080") +} + +func Party() *party { + return &party{} +} + +type party struct{} + +func (p *party) Configure(r iris.Party) { 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")) + r.Post("/", createHandler(service)) // OR: errors.CreateHandler(service.Create) - app.Listen(":8080") + // add a custom validation function for the CreateRequest struct. + r.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{})) + r.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated) + r.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id")) } func createHandler(service *myService) iris.Handler { @@ -97,6 +118,18 @@ type ( // 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. +// OR +// Custom function per route: +// +// r.Post("/", errors.Validation(validateCreateRequest), createHandler(service)) +// [more code here...] +// func validateCreateRequest(ctx iris.Context, r *CreateRequest) error { +// return validation.Join( +// validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50), +// validation.Number("age", r.Age).InRange(18, 130), +// validation.Slice("hobbies", r.Hobbies).Length(1, 10), +// ) +// } func (r *CreateRequest) ValidateContext(ctx iris.Context) error { // To pass custom validation functions: // return validation.Join( diff --git a/x/errors/handlers.go b/x/errors/handlers.go index 688f37db..8f3aa2a3 100644 --- a/x/errors/handlers.go +++ b/x/errors/handlers.go @@ -117,22 +117,93 @@ 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. +// 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 + +const contextValidatorFuncKey = "iris.errors.ContextValidatorFunc" + +// 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. +// It panics if the given validators are empty or nil. +// +// Example: +// +// r.Post("/", Validation(validateCreateRequest), createHandler(service)) +// +// func validateCreateRequest(ctx iris.Context, r *CreateRequest) error { +// return validation.Join( +// validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50), +// validation.Number("age", r.Age).InRange(18, 130), +// validation.Slice("hobbies", r.Hobbies).Length(1, 10), +// ) +// } +func Validation[T any](validators ...ContextValidatorFunc[T]) context.Handler { + validator := joinContextValidators[T](validators) + + return func(ctx *context.Context) { + ctx.Values().Set(contextValidatorFuncKey, 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") + } + + if len(validators) == 1 { + return validators[0] + } + + return func(ctx *context.Context, req T) error { + for _, validator := range validators { + if validator == nil { + continue + } + + if err := validator(ctx, req); err != nil { + return err + } + } + + return nil + } +} + // ContextValidator 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 } -func validateContext(ctx *context.Context, req any) bool { +func validateContext[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 err != nil { - if HandleError(ctx, err) { - return false + err = contextValidator.ValidateContext(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 err != nil { + if HandleError(ctx, err) { + return false + } + } + return true }