1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-04 18:57:03 +00:00

Add new x/errors/validation package to make your life even more easier (using Generics)

This commit is contained in:
Gerasimos (Makis) Maropoulos
2024-01-07 15:08:03 +02:00
parent 8f2deb6873
commit 104bea0a58
14 changed files with 443 additions and 282 deletions

View 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
}

View 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)
}

View 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 ""
}
}

View 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 ""
}
}