mirror of
https://github.com/kataras/iris.git
synced 2026-01-07 20:17:05 +00:00
Add new x/errors/validation package to make your life even more easier (using Generics)
This commit is contained in:
@@ -174,8 +174,21 @@ func HandleError(ctx *context.Context, err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if vErrs, ok := AsValidationErrors(err); ok {
|
||||
InvalidArgument.Data(ctx, "validation failure", vErrs)
|
||||
if vErr, ok := err.(ValidationError); ok {
|
||||
if vErr == nil {
|
||||
return false // consider as not error for any case, this should never happen.
|
||||
}
|
||||
|
||||
InvalidArgument.Validation(ctx, vErr)
|
||||
return true
|
||||
}
|
||||
|
||||
if vErrs, ok := err.(ValidationErrors); ok {
|
||||
if len(vErrs) == 0 {
|
||||
return false // consider as not error for any case, this should never happen.
|
||||
}
|
||||
|
||||
InvalidArgument.Validation(ctx, vErrs...)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -283,9 +296,20 @@ func (e ErrorCodeName) Err(ctx *context.Context, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErrors, ok := AsValidationErrors(err); ok {
|
||||
e.validation(ctx, validationErrors)
|
||||
return
|
||||
if vErr, ok := err.(ValidationError); ok {
|
||||
if vErr == nil {
|
||||
return // consider as not error for any case, this should never happen.
|
||||
}
|
||||
|
||||
e.Validation(ctx, vErr)
|
||||
}
|
||||
|
||||
if vErrs, ok := err.(ValidationErrors); ok {
|
||||
if len(vErrs) == 0 {
|
||||
return // consider as not error for any case, this should never happen.
|
||||
}
|
||||
|
||||
e.Validation(ctx, vErrs...)
|
||||
}
|
||||
|
||||
// If it's already an Error type then send it directly.
|
||||
|
||||
@@ -115,6 +115,12 @@ type ResponseOnlyErrorFunc[T any] interface {
|
||||
func(stdContext.Context, T) error
|
||||
}
|
||||
|
||||
// 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 bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) (R, bool) {
|
||||
var req T
|
||||
switch len(fnInput) {
|
||||
@@ -131,6 +137,16 @@ func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fn
|
||||
panic("invalid number of arguments")
|
||||
}
|
||||
|
||||
if contextValidator, ok := any(&req).(ContextValidator); ok {
|
||||
err := contextValidator.ValidateContext(ctx)
|
||||
if err != nil {
|
||||
if HandleError(ctx, err) {
|
||||
var resp R
|
||||
return resp, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := fn(ctx, req)
|
||||
return resp, !HandleError(ctx, err)
|
||||
}
|
||||
@@ -208,14 +224,7 @@ func ReadPayload[T any](ctx *context.Context) (T, bool) {
|
||||
return payload, false
|
||||
}
|
||||
|
||||
if !handleJSONError(ctx, err) {
|
||||
if vErrs, ok := AsValidationErrors(err); ok {
|
||||
InvalidArgument.Data(ctx, "validation failure", vErrs)
|
||||
} else {
|
||||
InvalidArgument.Details(ctx, "unable to parse body", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
HandleError(ctx, err)
|
||||
return payload, false
|
||||
}
|
||||
|
||||
@@ -229,11 +238,7 @@ func ReadQuery[T any](ctx *context.Context) (T, bool) {
|
||||
var payload T
|
||||
err := ctx.ReadQuery(&payload)
|
||||
if err != nil {
|
||||
if vErrs, ok := AsValidationErrors(err); ok {
|
||||
InvalidArgument.Data(ctx, "validation failure", vErrs)
|
||||
} else {
|
||||
InvalidArgument.Details(ctx, "unable to parse query", err.Error())
|
||||
}
|
||||
HandleError(ctx, err)
|
||||
return payload, false
|
||||
}
|
||||
|
||||
|
||||
92
x/errors/validation/error.go
Normal file
92
x/errors/validation/error.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kataras/iris/v12/x/errors"
|
||||
)
|
||||
|
||||
// FieldError describes a field validation error.
|
||||
// It completes the errors.ValidationError interface.
|
||||
type FieldError[T any] struct {
|
||||
Field string `json:"field"`
|
||||
Value T `json:"value"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Field returns a new validation error.
|
||||
//
|
||||
// Use its Func method to add validations over this field.
|
||||
func Field[T any](field string, value T) *FieldError[T] {
|
||||
return &FieldError[T]{Field: field, Value: value}
|
||||
}
|
||||
|
||||
// Error completes the standard error interface.
|
||||
func (e *FieldError[T]) Error() string {
|
||||
return fmt.Sprintf("field %q got invalid value of %v: reason: %s", e.Field, e.Value, e.Reason)
|
||||
}
|
||||
|
||||
// GetField returns the field name.
|
||||
func (e *FieldError[T]) GetField() string {
|
||||
return e.Field
|
||||
}
|
||||
|
||||
// GetValue returns the value of the field.
|
||||
func (e *FieldError[T]) GetValue() any {
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// GetReason returns the reason of the validation error.
|
||||
func (e *FieldError[T]) GetReason() string {
|
||||
return e.Reason
|
||||
}
|
||||
|
||||
// IsZero reports whether the error is nil or has an empty reason.
|
||||
func (e *FieldError[T]) IsZero() bool {
|
||||
return e == nil || e.Reason == ""
|
||||
}
|
||||
|
||||
func (e *FieldError[T]) joinReason(reason string) {
|
||||
if reason == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if e.Reason == "" {
|
||||
e.Reason = reason
|
||||
} else {
|
||||
e.Reason += ", " + reason
|
||||
}
|
||||
}
|
||||
|
||||
// Func accepts a variadic number of functions which accept the value of the field
|
||||
// and return a string message if the value is invalid.
|
||||
// It joins the reasons into one.
|
||||
func (e *FieldError[T]) Func(fns ...func(value T) string) *FieldError[T] {
|
||||
for _, fn := range fns {
|
||||
e.joinReason(fn(e.Value))
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Join joins the given validation errors into one.
|
||||
func Join(errs ...errors.ValidationError) error { // note that here we return the standard error type instead of the errors.ValidationError in order to make the error nil instead of ValidationErrors(nil) on empty slice.
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedErrs := make(errors.ValidationErrors, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
if err == nil || err.GetReason() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
joinedErrs = append(joinedErrs, err)
|
||||
}
|
||||
|
||||
if len(joinedErrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return joinedErrs
|
||||
}
|
||||
105
x/errors/validation/number.go
Normal file
105
x/errors/validation/number.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// NumberValue is a type constraint that accepts any numeric type.
|
||||
type NumberValue interface {
|
||||
constraints.Integer | constraints.Float
|
||||
}
|
||||
|
||||
// NumberError describes a number field validation error.
|
||||
type NumberError[T NumberValue] struct{ *FieldError[T] }
|
||||
|
||||
// Number returns a new number validation error.
|
||||
func Number[T NumberValue](field string, value T) *NumberError[T] {
|
||||
return &NumberError[T]{Field(field, value)}
|
||||
}
|
||||
|
||||
// Positive adds an error if the value is not positive.
|
||||
func (e *NumberError[T]) Positive() *NumberError[T] {
|
||||
e.Func(Positive)
|
||||
return e
|
||||
}
|
||||
|
||||
// Negative adds an error if the value is not negative.
|
||||
func (e *NumberError[T]) Negative() *NumberError[T] {
|
||||
e.Func(Negative)
|
||||
return e
|
||||
}
|
||||
|
||||
// Zero reports whether the value is zero.
|
||||
func (e *NumberError[T]) Zero() *NumberError[T] {
|
||||
e.Func(Zero)
|
||||
return e
|
||||
}
|
||||
|
||||
// NonZero adds an error if the value is zero.
|
||||
func (e *NumberError[T]) NonZero() *NumberError[T] {
|
||||
e.Func(NonZero)
|
||||
return e
|
||||
}
|
||||
|
||||
// InRange adds an error if the value is not in the range.
|
||||
func (e *NumberError[T]) InRange(min, max T) *NumberError[T] {
|
||||
e.Func(InRange(min, max))
|
||||
return e
|
||||
}
|
||||
|
||||
// Positive accepts any numeric type and
|
||||
// returns a message if the value is not positive.
|
||||
func Positive[T NumberValue](n T) string {
|
||||
if n <= 0 {
|
||||
return "must be positive"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Negative accepts any numeric type and returns a message if the value is not negative.
|
||||
func Negative[T NumberValue](n T) string {
|
||||
if n >= 0 {
|
||||
return "must be negative"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Zero accepts any numeric type and returns a message if the value is not zero.
|
||||
func Zero[T NumberValue](n T) string {
|
||||
if n != 0 {
|
||||
return "must be zero"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// NonZero accepts any numeric type and returns a message if the value is not zero.
|
||||
func NonZero[T NumberValue](n T) string {
|
||||
if n == 0 {
|
||||
return "must not be zero"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// InRange accepts any numeric type and returns a message if the value is not in the range.
|
||||
func InRange[T NumberValue](min, max T) func(T) string {
|
||||
return func(n T) string {
|
||||
if n < min || n > max {
|
||||
return "must be in range of " + FormatRange(min, max)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// FormatRange returns a string representation of a range of values, such as "[1, 10]".
|
||||
// It uses a type constraint NumberValue, which means that the parameters must be numeric types
|
||||
// that support comparison and formatting operations.
|
||||
func FormatRange[T NumberValue](min, max T) string {
|
||||
return fmt.Sprintf("[%v, %v]", min, max)
|
||||
}
|
||||
57
x/errors/validation/slice.go
Normal file
57
x/errors/validation/slice.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package validation
|
||||
|
||||
import "fmt"
|
||||
|
||||
type SliceValue[T any] interface {
|
||||
~[]T
|
||||
}
|
||||
|
||||
// SliceError describes a slice field validation error.
|
||||
type SliceError[T any, V SliceValue[T]] struct{ *FieldError[V] }
|
||||
|
||||
// Slice returns a new slice validation error.
|
||||
func Slice[T any, V SliceValue[T]](field string, value V) *SliceError[T, V] {
|
||||
return &SliceError[T, V]{Field(field, value)}
|
||||
}
|
||||
|
||||
// NotEmpty adds an error if the slice is empty.
|
||||
func (e *SliceError[T, V]) NotEmpty() *SliceError[T, V] {
|
||||
e.Func(NotEmptySlice)
|
||||
return e
|
||||
}
|
||||
|
||||
// Length adds an error if the slice length is not in the given range.
|
||||
func (e *SliceError[T, V]) Length(min, max int) *SliceError[T, V] {
|
||||
e.Func(SliceLength[T, V](min, max))
|
||||
return e
|
||||
}
|
||||
|
||||
// NotEmptySlice accepts any slice and returns a message if the value is empty.
|
||||
func NotEmptySlice[T any, V SliceValue[T]](s V) string {
|
||||
if len(s) == 0 {
|
||||
return "must not be empty"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SliceLength accepts any slice and returns a message if the length is not in the given range.
|
||||
func SliceLength[T any, V SliceValue[T]](min, max int) func(s V) string {
|
||||
return func(s V) string {
|
||||
n := len(s)
|
||||
|
||||
if min == max {
|
||||
if n != min {
|
||||
return fmt.Sprintf("must be %d elements", min)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
if n < min || n > max {
|
||||
return fmt.Sprintf("must be between %d and %d elements", min, max)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
69
x/errors/validation/string.go
Normal file
69
x/errors/validation/string.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringError describes a string field validation error.
|
||||
type StringError struct{ *FieldError[string] }
|
||||
|
||||
// String returns a new string validation error.
|
||||
func String(field string, value string) *StringError {
|
||||
return &StringError{Field(field, value)}
|
||||
}
|
||||
|
||||
// NotEmpty adds an error if the string is empty.
|
||||
func (e *StringError) NotEmpty() *StringError {
|
||||
e.Func(NotEmpty)
|
||||
return e
|
||||
}
|
||||
|
||||
// Fullname adds an error if the string is not a full name.
|
||||
func (e *StringError) Fullname() *StringError {
|
||||
e.Func(Fullname)
|
||||
return e
|
||||
}
|
||||
|
||||
// Length adds an error if the string length is not in the given range.
|
||||
func (e *StringError) Length(min, max int) *StringError {
|
||||
e.Func(StringLength(min, max))
|
||||
return e
|
||||
}
|
||||
|
||||
// NotEmpty accepts any string and returns a message if the value is empty.
|
||||
func NotEmpty(s string) string {
|
||||
if s == "" {
|
||||
return "must not be empty"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Fullname accepts any string and returns a message if the value is not a full name.
|
||||
func Fullname(s string) string {
|
||||
if len(strings.Split(s, " ")) < 2 {
|
||||
return "must contain first and last name"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// StringLength accepts any string and returns a message if the length is not in the given range.
|
||||
func StringLength(min, max int) func(s string) string {
|
||||
return func(s string) string {
|
||||
n := len(s)
|
||||
|
||||
if min == max {
|
||||
if n != min {
|
||||
return fmt.Sprintf("must be %d characters", min)
|
||||
}
|
||||
}
|
||||
|
||||
if n < min || n > max {
|
||||
return fmt.Sprintf("must be between %d and %d characters", min, max)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -11,10 +10,6 @@ import (
|
||||
// it can by mapped to a validation error.
|
||||
//
|
||||
// A validation error(s) can be given by ErrorCodeName's Validation or Err methods.
|
||||
//
|
||||
// Example can be found at:
|
||||
//
|
||||
// https://github.com/kataras/iris/tree/main/_examples/routing/http-wire-errors/custom-validation-errors
|
||||
type ValidationError interface {
|
||||
error
|
||||
|
||||
@@ -43,123 +38,3 @@ func (errs ValidationErrors) Error() string {
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ValidationErrorMapper is the interface which
|
||||
// custom validation error mappers should complete.
|
||||
type ValidationErrorMapper interface {
|
||||
// The implementation must check the given "err"
|
||||
// and make decision if it's an error of validation
|
||||
// and if so it should return the value (err or another one)
|
||||
// and true as the last output argument.
|
||||
//
|
||||
// Outputs:
|
||||
// 1. the validation error(s) value
|
||||
// 2. true if the interface{} is an array, otherise false
|
||||
// 3. true if it's a validation error or false if not.
|
||||
MapValidationErrors(err error) (interface{}, bool, bool)
|
||||
}
|
||||
|
||||
// ValidationErrorMapperFunc is an "ValidationErrorMapper" but in type of a function.
|
||||
type ValidationErrorMapperFunc func(err error) (interface{}, bool, bool)
|
||||
|
||||
// MapValidationErrors completes the "ValidationErrorMapper" interface.
|
||||
func (v ValidationErrorMapperFunc) MapValidationErrors(err error) (interface{}, bool, bool) {
|
||||
return v(err)
|
||||
}
|
||||
|
||||
// read-only at serve time, holds the validation error mappers.
|
||||
var validationErrorMappers []ValidationErrorMapper = []ValidationErrorMapper{
|
||||
ValidationErrorMapperFunc(func(err error) (interface{}, bool, bool) {
|
||||
switch e := err.(type) {
|
||||
case ValidationError:
|
||||
return e, false, true
|
||||
case ValidationErrors:
|
||||
return e, true, true
|
||||
default:
|
||||
return nil, false, false
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// RegisterValidationErrorMapper registers a custom
|
||||
// implementation of validation error mapping.
|
||||
// Call it on program initilization, main() or init() functions.
|
||||
func RegisterValidationErrorMapper(m ValidationErrorMapper) {
|
||||
validationErrorMappers = append(validationErrorMappers, m)
|
||||
}
|
||||
|
||||
// RegisterValidationErrorMapperFunc registers a custom
|
||||
// function implementation of validation error mapping.
|
||||
// Call it on program initilization, main() or init() functions.
|
||||
func RegisterValidationErrorMapperFunc(fn func(err error) (interface{}, bool, bool)) {
|
||||
validationErrorMappers = append(validationErrorMappers, ValidationErrorMapperFunc(fn))
|
||||
}
|
||||
|
||||
type validationErrorTypeMapper struct {
|
||||
types []reflect.Type
|
||||
}
|
||||
|
||||
var _ ValidationErrorMapper = (*validationErrorTypeMapper)(nil)
|
||||
|
||||
func (v *validationErrorTypeMapper) MapValidationErrors(err error) (interface{}, bool, bool) {
|
||||
errType := reflect.TypeOf(err)
|
||||
for _, typ := range v.types {
|
||||
if equalTypes(errType, typ) {
|
||||
return err, false, true
|
||||
}
|
||||
|
||||
// a slice is given but the underline type is registered.
|
||||
if errType.Kind() == reflect.Slice {
|
||||
if equalTypes(errType.Elem(), typ) {
|
||||
return err, true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func equalTypes(err reflect.Type, binding reflect.Type) bool {
|
||||
return err == binding
|
||||
// return binding.AssignableTo(err)
|
||||
}
|
||||
|
||||
// NewValidationErrorTypeMapper returns a validation error mapper
|
||||
// which compares the error with one or more of the given "types",
|
||||
// through reflection. Each of the given types MUST complete the
|
||||
// standard error type, so it can be passed through the error code.
|
||||
func NewValidationErrorTypeMapper(types ...error) ValidationErrorMapper {
|
||||
typs := make([]reflect.Type, 0, len(types))
|
||||
for _, typ := range types {
|
||||
v, ok := typ.(reflect.Type)
|
||||
if !ok {
|
||||
v = reflect.TypeOf(typ)
|
||||
}
|
||||
|
||||
typs = append(typs, v)
|
||||
}
|
||||
|
||||
return &validationErrorTypeMapper{
|
||||
types: typs,
|
||||
}
|
||||
}
|
||||
|
||||
// AsValidationErrors reports wheether the given "err" is a type of validation error(s).
|
||||
// Its behavior can be modified before serve-time
|
||||
// through the "RegisterValidationErrorMapper" function.
|
||||
func AsValidationErrors(err error) (interface{}, bool) {
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, m := range validationErrorMappers {
|
||||
if errs, isArray, ok := m.MapValidationErrors(err); ok {
|
||||
if !isArray { // ensure always-array on Validation field of the http error.
|
||||
return []interface{}{errs}, true
|
||||
}
|
||||
return errs, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user