Skip to content

Commit d32eb68

Browse files
committed
add x/errors.Validation
1 parent ad75479 commit d32eb68

File tree

3 files changed

+115
-10
lines changed

3 files changed

+115
-10
lines changed

HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
2323

2424
Changes apply to `main` branch.
2525

26+
- 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.
2627
- Add `x/errors.Handler`, `CreateHandler`, `NoContentHandler`, `NoContenetOrNotModifiedHandler` and `ListHandler` ready-to-use handlers for service method calls to Iris Handler.
2728
- 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.
2829
- 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).

_examples/routing/http-wire-errors/service/main.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,37 @@ import (
1212

1313
func main() {
1414
app := iris.New()
15-
service := new(myService)
1615

17-
app.Post("/", createHandler(service)) // OR: errors.CreateHandler(service.Create)
18-
app.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{}))
19-
app.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated)
20-
app.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id"))
16+
/*
17+
service := new(myService)
18+
19+
app.Post("/", createHandler(service)) // OR: errors.CreateHandler(service.Create)
20+
app.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{}))
21+
app.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated)
22+
app.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id"))
23+
*/
2124

25+
app.PartyConfigure("/", Party())
2226
app.Listen(":8080")
2327
}
2428

29+
func Party() *party {
30+
return &party{}
31+
}
32+
33+
type party struct{}
34+
35+
func (p *party) Configure(r iris.Party) {
36+
service := new(myService)
37+
38+
r.Post("/", createHandler(service)) // OR: errors.CreateHandler(service.Create)
39+
40+
// add a custom validation function for the CreateRequest struct.
41+
r.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{}))
42+
r.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated)
43+
r.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id"))
44+
}
45+
2546
func createHandler(service *myService) iris.Handler {
2647
return func(ctx iris.Context) {
2748
// What it does?
@@ -97,6 +118,18 @@ type (
97118
// It validates the request body and returns an error if the request body is invalid.
98119
// You can also alter the "r" CreateRequest before calling the service method,
99120
// e.g. give a default value to a field if it's empty or set an ID based on a path parameter.
121+
// OR
122+
// Custom function per route:
123+
//
124+
// r.Post("/", errors.Validation(validateCreateRequest), createHandler(service))
125+
// [more code here...]
126+
// func validateCreateRequest(ctx iris.Context, r *CreateRequest) error {
127+
// return validation.Join(
128+
// validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50),
129+
// validation.Number("age", r.Age).InRange(18, 130),
130+
// validation.Slice("hobbies", r.Hobbies).Length(1, 10),
131+
// )
132+
// }
100133
func (r *CreateRequest) ValidateContext(ctx iris.Context) error {
101134
// To pass custom validation functions:
102135
// return validation.Join(

x/errors/handlers.go

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,22 +117,93 @@ type ResponseOnlyErrorFunc[T any] interface {
117117
func(stdContext.Context, T) error
118118
}
119119

120+
// ContextValidatorFunc is a function which takes a context and a generic type T and returns an error.
121+
// It is used to validate the context before calling a service function.
122+
//
123+
// See Validation package-level function.
124+
type ContextValidatorFunc[T any] func(*context.Context, T) error
125+
126+
const contextValidatorFuncKey = "iris.errors.ContextValidatorFunc"
127+
128+
// Validation adds a context validator function to the context.
129+
// It returns a middleware which can be used to validate the context before calling a service function.
130+
// It panics if the given validators are empty or nil.
131+
//
132+
// Example:
133+
//
134+
// r.Post("/", Validation(validateCreateRequest), createHandler(service))
135+
//
136+
// func validateCreateRequest(ctx iris.Context, r *CreateRequest) error {
137+
// return validation.Join(
138+
// validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50),
139+
// validation.Number("age", r.Age).InRange(18, 130),
140+
// validation.Slice("hobbies", r.Hobbies).Length(1, 10),
141+
// )
142+
// }
143+
func Validation[T any](validators ...ContextValidatorFunc[T]) context.Handler {
144+
validator := joinContextValidators[T](validators)
145+
146+
return func(ctx *context.Context) {
147+
ctx.Values().Set(contextValidatorFuncKey, validator)
148+
ctx.Next()
149+
}
150+
}
151+
152+
func joinContextValidators[T any](validators []ContextValidatorFunc[T]) ContextValidatorFunc[T] {
153+
if len(validators) == 0 || validators[0] == nil {
154+
panic("at least one validator is required")
155+
}
156+
157+
if len(validators) == 1 {
158+
return validators[0]
159+
}
160+
161+
return func(ctx *context.Context, req T) error {
162+
for _, validator := range validators {
163+
if validator == nil {
164+
continue
165+
}
166+
167+
if err := validator(ctx, req); err != nil {
168+
return err
169+
}
170+
}
171+
172+
return nil
173+
}
174+
}
175+
120176
// ContextValidator is an interface which can be implemented by a request payload struct
121177
// in order to validate the context before calling a service function.
122178
type ContextValidator interface {
123179
ValidateContext(*context.Context) error
124180
}
125181

126-
func validateContext(ctx *context.Context, req any) bool {
182+
func validateContext[T any](ctx *context.Context, req T) bool {
183+
var err error
184+
185+
// Always run the request's validator first,
186+
// so dynamic validators can be customized per path and method.
127187
if contextValidator, ok := any(&req).(ContextValidator); ok {
128-
err := contextValidator.ValidateContext(ctx)
129-
if err != nil {
130-
if HandleError(ctx, err) {
131-
return false
188+
err = contextValidator.ValidateContext(ctx)
189+
}
190+
191+
if err == nil {
192+
if v := ctx.Values().Get(contextValidatorFuncKey); v != nil {
193+
if contextValidatorFunc, ok := v.(ContextValidatorFunc[T]); ok {
194+
err = contextValidatorFunc(ctx, req)
195+
} else if contextValidatorFunc, ok := v.(ContextValidatorFunc[*T]); ok { // or a pointer of T.
196+
err = contextValidatorFunc(ctx, &req)
132197
}
133198
}
134199
}
135200

201+
if err != nil {
202+
if HandleError(ctx, err) {
203+
return false
204+
}
205+
}
206+
136207
return true
137208
}
138209

0 commit comments

Comments
 (0)