/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"fmt"
"reflect"
"strings"
)
func indirectValue(v any) (value any, isValid bool) {
val := reflect.ValueOf(v)
if !val.IsValid() || (val.Kind() == reflect.Ptr && val.IsNil()) {
return nil, false
}
val = reflect.Indirect(val)
if !val.IsValid() {
return nil, false
}
return val.Interface(), true
}
func valueIsEmpty(value reflect.Value) bool {
if !value.IsValid() {
return true
}
switch value.Kind() {
case reflect.Slice, reflect.Map:
return value.IsNil() || value.Len() == 0
case reflect.Array, reflect.Struct:
return value.IsZero()
case reflect.String:
return len(strings.TrimSpace(value.String())) == 0
case reflect.Ptr:
return value.IsNil() || valueIsEmpty(value.Elem())
default:
if value.CanInterface() {
if i, ok := value.Interface().(fmt.Stringer); ok {
return len(strings.TrimSpace(i.String())) == 0
}
}
return false
}
}
func toString(v any) (string, bool) {
if v == nil {
return "", false
}
switch v := v.(type) {
case string:
return v, true
case *string:
return *v, true
}
if i, ok := v.(fmt.Stringer); ok {
return i.String(), true
}
return "", false
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"github.com/pkg/errors"
)
type CallbackFunc[T any] func(ctx context.Context, value T) error
type Callback[T any] struct {
f CallbackFunc[T]
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewCallback[T any](f CallbackFunc[T]) *Callback[T] {
return &Callback[T]{
f: f,
}
}
func (r *Callback[T]) When(v WhenFunc) *Callback[T] {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Callback[T]) when() WhenFunc {
return r.whenFunc
}
func (r *Callback[T]) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Callback[T]) SkipOnEmpty() *Callback[T] {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Callback[T]) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Callback[T]) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Callback[T]) SkipOnError() *Callback[T] {
rs := *r
rs.skipError = true
return &rs
}
func (r *Callback[T]) shouldSkipOnError() bool {
return r.skipError
}
func (r *Callback[T]) setSkipOnError(v bool) {
r.skipError = v
}
func (c *Callback[T]) ValidateValue(ctx context.Context, value any) error {
v, ok := value.(T)
if !ok {
var v T
return errors.Wrapf(ErrCallbackUnexpectedValueType, "got %T want %T", value, v)
}
if err := c.f(ctx, v); err != nil {
var vErr *ValidationError
if errors.As(err, &vErr) {
return NewResult().WithError(vErr)
}
var result *Result
if errors.As(err, &result) {
return result
}
return err
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"time"
)
const (
OperatorEqual = "=="
OperatorNotEqual = "!="
OperatorGreaterThan = ">"
OperatorGreaterThanEqual = ">="
OperatorLessThan = "<"
OperatorLessThanEqual = "<="
)
type Compare struct {
targetValue any
targetAttribute string
operator string
message string
operatorIsValid bool
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewCompare(targetValue any, targetAttribute, operator string) *Compare {
c := &Compare{
targetValue: targetValue,
targetAttribute: targetAttribute,
operator: operator,
operatorIsValid: true,
}
switch operator {
case OperatorEqual:
c.message = MessageCompareEqual
case OperatorNotEqual:
c.message = MessageCompareNotEqual
case OperatorGreaterThan:
c.message = MessageCompareGreaterThan
case OperatorGreaterThanEqual:
c.message = MessageCompareGreaterThanEqual
case OperatorLessThan:
c.message = MessageCompareLessThan
case OperatorLessThanEqual:
c.message = MessageCompareLessThanEqual
default:
c.operatorIsValid = false
}
return c
}
func (r *Compare) WithMessage(v string) *Compare {
rc := *r
rc.message = v
return &rc
}
func (r *Compare) When(v WhenFunc) *Compare {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Compare) when() WhenFunc {
return r.whenFunc
}
func (r *Compare) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Compare) SkipOnEmpty() *Compare {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Compare) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Compare) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Compare) SkipOnError() *Compare {
rs := *r
rs.skipError = true
return &rs
}
func (r *Compare) shouldSkipOnError() bool {
return r.skipError
}
func (r *Compare) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Compare) ValidateValue(ctx context.Context, value any) error {
if !r.operatorIsValid {
return ErrUnknownOperator
}
var (
targetValue any
targetValueOrAttr any
err error
)
targetValue = r.targetValue
targetValueOrAttr = r.targetAttribute
if r.targetValue == nil {
dataSet, ok := ExtractDataSet[DataSet](ctx)
if !ok {
return ErrNotExistsDataSetIntoContext
}
targetValue, err = dataSet.FieldValue(r.targetAttribute)
if err != nil {
return err
}
targetValueOrAttr = targetValue
}
switch r.operator {
case OperatorEqual:
if r.eq(value, targetValue) {
return nil
}
case OperatorNotEqual:
if !r.eq(value, targetValue) {
return nil
}
case OperatorGreaterThan:
if r.gt(value, targetValue) {
return nil
}
case OperatorGreaterThanEqual:
if r.eq(value, targetValue) || r.gt(value, targetValue) {
return nil
}
case OperatorLessThan:
if !r.eq(value, targetValue) && !r.gt(value, targetValue) {
return nil
}
case OperatorLessThanEqual:
if r.eq(value, targetValue) || !r.gt(value, targetValue) {
return nil
}
}
return NewResult().
WithError(
NewValidationError(r.message).
WithParams(map[string]any{
"targetValue": r.targetValue,
"targetAttribute": r.targetAttribute,
"targetValueOrAttribute": targetValueOrAttr,
}),
)
}
func (r *Compare) eq(a, b any) bool {
if ia, ok := a.(int); ok {
if ib, ok := b.(int); ok {
return ia == ib
}
}
if ia, ok := a.(uint); ok {
if ib, ok := b.(uint); ok {
return ia == ib
}
}
if ia, ok := a.(int64); ok {
if ib, ok := b.(int64); ok {
return ia == ib
}
}
if ia, ok := a.(float64); ok {
if ib, ok := b.(float64); ok {
return ia == ib
}
}
if ia, ok := toString(a); ok {
if ib, ok := toString(b); ok {
return ia == ib
}
}
if ia, ok := a.(time.Time); ok {
if ib, ok := b.(time.Time); ok {
return ia.Equal(ib)
}
}
return a == b
}
func (r *Compare) gt(a, b any) bool {
if ia, ok := a.(int); ok {
if ib, ok := b.(int); ok {
return ia > ib
}
}
if ia, ok := a.(uint); ok {
if ib, ok := b.(uint); ok {
return ia > ib
}
}
if ia, ok := a.(int64); ok {
if ib, ok := b.(int64); ok {
return ia > ib
}
}
if ia, ok := a.(float64); ok {
if ib, ok := b.(float64); ok {
return ia > ib
}
}
if ia, ok := toString(a); ok {
if ib, ok := toString(b); ok {
return ia > ib
}
}
if ia, ok := a.(time.Time); ok {
if ib, ok := b.(time.Time); ok {
return ia.After(ib)
}
}
return false
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"github.com/raoptimus/validator.go/v2/set"
)
type Key uint8
const (
KeyDataSet Key = iota + 1
KeyPreviousRulesErrored
KeyRootDataSet
KeyNestedDataSet
KeyPrevNestedDataSet
)
type DataSet interface {
FieldValue(name string) (any, error)
FieldAliasName(name string) string
Name() set.Name
Data() any
}
type Context struct {
context.Context
ds DataSet
}
func NewContext(ctx context.Context) *Context {
return &Context{Context: ctx}
}
func (c *Context) Value(key any) any {
if key == KeyDataSet {
return c.ds
}
return c.Context.Value(key)
}
func (c *Context) withDataSet(ds DataSet) *Context {
cc := *c
cc.ds = ds
return &cc
}
func (c *Context) dataSet() (DataSet, bool) { //nolint:ireturn // DataSet is internal interface
return c.ds, c.ds != nil
}
func DataSetFromContext[T DataSet](ctx *Context) (T, bool) { //nolint:ireturn // generic interface
if ds, ok := ctx.dataSet(); ok {
if dsT, ok2 := ds.(T); ok2 {
return dsT, true
}
}
var v T
return v, false
}
// todo: write funcs if context.Context interface
func withDataSet(ctx context.Context, ds DataSet) context.Context {
return NewContext(ctx).withDataSet(ds)
// return context.WithValue(ctx, KeyDataSet, ds)
}
func ExtractDataSet[T any](ctx context.Context) (T, bool) { //nolint:ireturn // generic interface
var v T
if ctx == nil {
return v, false
}
ds, ok := ctx.Value(KeyDataSet).(DataSet)
if !ok {
return v, false
}
if dst, ok := ds.(T); ok {
return dst, true
}
if dt, ok := ds.Data().(T); ok {
return dt, true
}
return v, false
}
func withPreviousRulesErrored(ctx context.Context) context.Context {
return context.WithValue(ctx, KeyPreviousRulesErrored, true)
}
func previousRulesErrored(ctx context.Context) bool {
if y, ok := ctx.Value(KeyPreviousRulesErrored).(bool); ok {
return y
}
return false
}
func contextWithRootDataSet(ctx context.Context, ds any) context.Context {
if prev := ctx.Value(KeyRootDataSet); prev != nil {
ctx = context.WithValue(ctx, KeyPrevNestedDataSet, prev)
}
return context.WithValue(ctx, KeyRootDataSet, ds)
}
func RootDataSetFromContext[T any](ctx context.Context) (T, bool) { //nolint:ireturn // generic interface
v, ok := ctx.Value(KeyRootDataSet).(T)
return v, ok
}
func contextWithNestedDataSet(ctx context.Context, ds any) context.Context {
return context.WithValue(ctx, KeyNestedDataSet, ds)
}
func NestedDataSetFromContext[T any](ctx context.Context) (T, bool) { //nolint:ireturn // generic interface
v, ok := ctx.Value(KeyNestedDataSet).(T)
return v, ok
}
func PrevNestedDataSetFromContext[T any](ctx context.Context) (T, bool) { //nolint:ireturn // generic interface
v, ok := ctx.Value(KeyPrevNestedDataSet).(T)
return v, ok
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"errors"
"reflect"
"strconv"
)
type Each struct {
message string
incorrectInputMessage string
rules Rules
normalizeRulesEnabled bool
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewEach(rules ...Rule) *Each {
return &Each{
message: MessageInvalid,
incorrectInputMessage: MessageEachIncorrectInput,
rules: rules,
normalizeRulesEnabled: true,
}
}
func (r *Each) WithMessage(message string) *Each {
rc := *r
rc.message = message
return &rc
}
func (r *Each) WithIncorrectInputMessage(incorrectInputMessage string) *Each {
rc := *r
rc.incorrectInputMessage = incorrectInputMessage
return &rc
}
func (r *Each) When(v WhenFunc) *Each {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Each) when() WhenFunc {
return r.whenFunc
}
func (r *Each) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Each) SkipOnEmpty() *Each {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Each) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Each) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Each) SkipOnError() *Each {
rs := *r
rs.skipError = true
return &rs
}
func (r *Each) shouldSkipOnError() bool {
return r.skipError
}
func (r *Each) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Each) ValidateValue(ctx context.Context, value any) error {
r.normalizeRules()
result := NewResult()
if value == nil || reflect.TypeOf(value).Kind() != reflect.Slice {
return result.WithError(
NewValidationError(r.incorrectInputMessage).
WithParams(map[string]any{
// "attribute": "",//todo
"value": value,
}),
)
}
vs := reflect.ValueOf(value)
for i := 0; i < vs.Len(); i++ {
v := vs.Index(i).Interface()
if err := ValidateValue(ctx, v, r.rules...); err != nil {
var r Result
if errors.As(err, &r) {
for _, err := range r.Errors() {
valuePath := []string{strconv.Itoa(i)}
if len(err.ValuePath) > 0 {
valuePath = append(valuePath, err.ValuePath...)
}
err.ValuePath = valuePath
result = result.WithError(err)
}
continue
}
return err
}
}
if result.IsValid() {
return nil
}
return result
}
func (r *Each) normalizeRules() {
if !r.normalizeRulesEnabled {
return
}
r.normalizeRulesEnabled = false
for i, rule := range r.rules {
if rse, ok := rule.(RuleSkipEmpty); ok {
rse.setSkipOnEmpty(r.skipEmpty)
}
if rser, ok := rule.(RuleSkipError); ok {
rser.setSkipOnError(r.skipError)
}
if rw, ok := rule.(RuleWhen); ok {
rw.setWhen(r.whenFunc)
}
r.rules[i] = rule
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
const emailRegexp = `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{1,61}$`
type Email struct {
*MatchRegularExpression
}
func NewEmail() *Email {
return &Email{
MatchRegularExpression: NewMatchRegularExpression(emailRegexp).
WithMessage(MessageInvalidEmail),
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"errors"
)
var (
ErrNotExistsDataSetIntoContext = errors.New("not exists data set into context")
ErrUnknownOperator = errors.New("unknown operator")
ErrCallbackUnexpectedValueType = errors.New("callback unexpected value type")
)
type ValidationError struct {
Message string
Params map[string]any
ValuePath []string
}
func NewValidationError(message string) *ValidationError {
return &ValidationError{
Message: message,
}
}
func (v *ValidationError) Error() string {
return v.Message
}
func (v *ValidationError) WithParams(params map[string]any) *ValidationError {
v.Params = params
return v
}
func (v *ValidationError) WithValuePath(valuePath []string) *ValidationError {
v.ValuePath = valuePath
return v
}
// IsError - проверяет на ошибку валидации и возвращает аттрибуты, где ключ равняется полю, а значения ошибкам валидации.
//
// {
// "client_id": [
// "Value cannot be blank.",
// "Value is invalid."
// ]
// }
func IsError(err error) (map[string][]string, bool) {
var result Result
if errors.As(err, &result) {
return result.ErrorMessagesIndexedByPath(), true
}
return nil, false
}
package validator
import (
"math"
"reflect"
)
// Inlined FNV-64a constants. Using the raw state instead of hash.Hash64
// avoids the interface allocation and per-field Reset cost of fnv.New64a.
const (
fnvOffset64 = 14695981039346656037
fnvPrime64 = 1099511628211
lowByteMask = 0xff
)
// Type tags disambiguate values across kinds so that, e.g., the uint 65
// does not collide with the string "A". Written before each value.
const (
tagNil byte = iota
tagBool
tagInt
tagUint
tagFloat
tagComplex
tagString
tagStruct
tagStructEnd
tagSlice
tagMap
)
// hasher is a zero-alloc FNV-64a streaming hasher. Its output is only
// used as a bucket key — equality is verified with reflect.DeepEqual on
// hash collisions in validateHashKey, so collisions are correctness-safe.
type hasher struct {
state uint64
}
func newHasher() hasher {
return hasher{state: fnvOffset64}
}
func (hw *hasher) reset() {
hw.state = fnvOffset64
}
func (hw *hasher) writeByte(b byte) {
hw.state = (hw.state ^ uint64(b)) * fnvPrime64
}
// writeUint64 absorbs the 8 little-endian bytes of v into the FNV-64a state.
// Manually unrolled: keeping state in a local register avoids reloading
// hw.state through the pointer on every byte, and elides the loop overhead
// (which is comparable to the per-byte work itself).
//
//nolint:mnd // bit-shift constants are inherent to a uint64 byte-by-byte unroll
func (hw *hasher) writeUint64(v uint64) {
s := hw.state
s = (s ^ (v & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 8) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 16) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 24) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 32) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 40) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 48) & lowByteMask)) * fnvPrime64
s = (s ^ ((v >> 56) & lowByteMask)) * fnvPrime64
hw.state = s
}
// writeString hoists hw.state into a local so the compiler can keep it in
// a register across the loop. Through the pointer receiver it would be
// reloaded on every iteration.
func (hw *hasher) writeString(s string) {
state := hw.state
for i := 0; i < len(s); i++ {
state = (state ^ uint64(s[i])) * fnvPrime64
}
hw.state = state
}
// maxHashDepth caps recursion so cyclic graphs (a map containing itself,
// a struct linked to itself via a pointer) cannot stack-overflow. Any
// subtree past the cap collapses to tagNil — a bucket-key collision that
// reflect.DeepEqual still resolves correctly in validateHashKey, at the
// cost of degraded bucketing for pathologically deep inputs.
const maxHashDepth = 256
// hashValue streams v into hw by walking its structure via reflection.
// Each kind writes a type tag followed by its primitive bytes or its
// recursively-hashed children, so the final hw.state is an order-dependent
// FNV-64a digest of the whole value. Type tags prevent cross-kind
// collisions (e.g. the uint 65 vs the string "A").
func hashValue(hw *hasher, v reflect.Value) {
hashValueAt(hw, v, 0)
}
func hashValueAt(hw *hasher, v reflect.Value, depth int) {
if depth > maxHashDepth {
hw.writeByte(tagNil)
return
}
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
if v.IsNil() {
hw.writeByte(tagNil)
return
}
v = v.Elem()
}
if !v.IsValid() {
hw.writeByte(tagNil)
return
}
switch v.Kind() {
case reflect.Bool:
hw.writeByte(tagBool)
if v.Bool() {
hw.writeByte(1)
} else {
hw.writeByte(0)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
hw.writeByte(tagInt)
//nolint:gosec // intentional int64 → uint64 bit reinterpretation
hw.writeUint64(uint64(v.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
hw.writeByte(tagUint)
hw.writeUint64(v.Uint())
case reflect.Float32, reflect.Float64:
hw.writeByte(tagFloat)
hw.writeUint64(math.Float64bits(v.Float()))
case reflect.Complex64, reflect.Complex128:
c := v.Complex()
hw.writeByte(tagComplex)
hw.writeUint64(math.Float64bits(real(c)))
hw.writeUint64(math.Float64bits(imag(c)))
case reflect.String:
hw.writeByte(tagString)
//nolint:gosec // Len is non-negative
hw.writeUint64(uint64(v.Len()))
hw.writeString(v.String())
case reflect.Struct:
// Walk every field, including unexported ones, to match
// reflect.DeepEqual semantics. Safe because hashValue only uses
// low-level accessors (.Bool, .Int, .Field, ...) which work on
// read-only reflect.Values — it never calls .Interface(), which
// is the only operation that panics on unexported fields.
hw.writeByte(tagStruct)
n := v.NumField()
for i := 0; i < n; i++ {
hashValueAt(hw, v.Field(i), depth+1)
}
hw.writeByte(tagStructEnd)
case reflect.Slice, reflect.Array:
hw.writeByte(tagSlice)
n := v.Len()
//nolint:gosec // Len is non-negative
hw.writeUint64(uint64(n))
for i := 0; i < n; i++ {
hashValueAt(hw, v.Index(i), depth+1)
}
case reflect.Map:
hw.writeByte(tagMap)
//nolint:gosec // Len is non-negative
hw.writeUint64(uint64(v.Len()))
// XOR over per-entry sub-hashes for order independence.
// The stack-allocated sub hasher keeps this alloc-free.
var xored uint64
iter := v.MapRange()
for iter.Next() {
sub := newHasher()
hashValueAt(&sub, iter.Key(), depth+1)
hashValueAt(&sub, iter.Value(), depth+1)
xored ^= sub.state
}
hw.writeUint64(xored)
case reflect.Invalid,
reflect.Interface,
reflect.Pointer,
reflect.Chan,
reflect.Func,
reflect.UnsafePointer:
// Invalid/Interface/Ptr are unreachable here (the top-of-function
// unwrap loop and IsValid check already handled them). Chan, Func,
// and UnsafePointer are opaque runtime handles that DeepEqual
// compares by pointer identity, not structure — collapsing them
// to tagNil forces DeepEqual to verify any bucket collision.
hw.writeByte(tagNil)
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
const humanRegexp = "^[\\p{L}\\d !?-~-–—:;#()‘.,'\"«»„“’`´′″\\[\\]\\/]+$"
type HumanText struct {
*MatchRegularExpression
}
func NewHumanText() *HumanText {
return &HumanText{
MatchRegularExpression: NewMatchRegularExpression(humanRegexp).
WithMessage(MessageInvalidHumanText),
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"slices"
"strings"
)
const (
ImageMimeTypePNG = "image/png"
ImageMimeTypeJPG = "image/jpeg"
ImageMimeTypeGIF = "image/gif"
)
type ImageMetaData struct {
Name string
MimeType string
Size uint64
Width int
Height int
}
type ImageMeta struct {
message string
invalidMimeTypeMessage string
tooLongSizeMessage string
mimeTypes []string
maxFileSizeBytes uint64
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewImageMeta() *ImageMeta {
return &ImageMeta{
message: MessageInvalidImageMeta,
invalidMimeTypeMessage: MessageInvalidMimeType,
tooLongSizeMessage: MessageFileTooLarge,
mimeTypes: []string{
ImageMimeTypeJPG,
ImageMimeTypePNG,
},
maxFileSizeBytes: 0, // no limit
}
}
func (r *ImageMeta) WithMessage(message string) *ImageMeta {
rc := *r
rc.message = message
return &rc
}
func (r *ImageMeta) When(v WhenFunc) *ImageMeta {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *ImageMeta) when() WhenFunc {
return r.whenFunc
}
func (r *ImageMeta) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *ImageMeta) SkipOnEmpty() *ImageMeta {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *ImageMeta) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *ImageMeta) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *ImageMeta) SkipOnError() *ImageMeta {
rs := *r
rs.skipError = true
return &rs
}
func (r *ImageMeta) shouldSkipOnError() bool {
return r.skipError
}
func (r *ImageMeta) setSkipOnError(v bool) {
r.skipError = v
}
func (r *ImageMeta) ValidateValue(_ context.Context, value any) error {
meta, ok := value.(*ImageMetaData)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
if !slices.Contains(r.mimeTypes, meta.MimeType) {
return NewResult().
WithError(
NewValidationError(r.invalidMimeTypeMessage).
WithParams(map[string]any{
"mimeTypes": strings.Join(r.mimeTypes, ", "),
}),
)
}
if meta.Size > r.maxFileSizeBytes {
return NewResult().
WithError(
NewValidationError(r.tooLongSizeMessage).
WithParams(map[string]any{
"maxFileSizeBytes": r.maxFileSizeBytes,
}),
)
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import "context"
type InRange struct {
message string
rangeValues []any
not bool
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewInRange(rangeValues []any) *InRange {
return &InRange{
message: MessageInRangeInvalid,
rangeValues: rangeValues,
not: false,
}
}
func (r *InRange) WithMessage(message string) *InRange {
rc := *r
rc.message = message
return &rc
}
func (r *InRange) Not() *InRange {
rc := *r
rc.not = true
return &rc
}
func (r *InRange) When(v WhenFunc) *InRange {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *InRange) when() WhenFunc {
return r.whenFunc
}
func (r *InRange) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *InRange) SkipOnEmpty() *InRange {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *InRange) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *InRange) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *InRange) SkipOnError() *InRange {
rs := *r
rs.skipError = true
return &rs
}
func (r *InRange) shouldSkipOnError() bool {
return r.skipError
}
func (r *InRange) setSkipOnError(v bool) {
r.skipError = v
}
func (r *InRange) ValidateValue(_ context.Context, value any) error {
v, valid := indirectValue(value)
if !valid {
return NewResult().WithError(NewValidationError(r.message))
}
var in bool
for _, rv := range r.rangeValues {
if rv == v {
in = true
break
}
}
if r.not == in {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"net"
)
type IP struct {
message string
ipv4NotAllowedMessage string
ipv6NotAllowedMessage string
allowV4 bool
allowV6 bool
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewIP() *IP {
return &IP{
message: MessageInvalidIP,
ipv4NotAllowedMessage: MessageIPv4NotAllowed,
ipv6NotAllowedMessage: MessageIPv6NotAllowed,
allowV4: true,
allowV6: true,
}
}
func (r *IP) WithMessage(v string) *IP {
rc := *r
rc.message = v
return &rc
}
func (r *IP) When(v WhenFunc) *IP {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *IP) when() WhenFunc {
return r.whenFunc
}
func (r *IP) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *IP) SkipOnEmpty() *IP {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *IP) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *IP) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *IP) SkipOnError() *IP {
rs := *r
rs.skipError = true
return &rs
}
func (r *IP) shouldSkipOnError() bool {
return r.skipError
}
func (r *IP) setSkipOnError(v bool) {
r.skipError = v
}
func (r *IP) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
ip := net.ParseIP(v)
if ip == nil {
return NewResult().WithError(NewValidationError(r.message))
}
// TODO: implement ipv4 and ipv4 validations
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"encoding/json"
"fmt"
)
type JSON struct {
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewJSON() *JSON {
return &JSON{
message: MessageInvalidJSON,
}
}
func (j *JSON) ValidateValue(_ context.Context, value any) error {
var bytes []byte
switch v := value.(type) {
case string:
bytes = []byte(v)
case *string:
bytes = []byte(*v)
case []byte:
bytes = v
case *[]byte:
bytes = *v
case json.RawMessage:
bytes = v
case *json.RawMessage:
bytes = *v
case fmt.Stringer:
bytes = []byte(v.String())
default:
return NewResult().WithError(NewValidationError(j.message))
}
if isValid := json.Valid(bytes); !isValid {
return NewResult().WithError(NewValidationError(j.message))
}
return nil
}
func (j *JSON) When(v WhenFunc) *JSON {
rc := *j
rc.whenFunc = v
return &rc
}
func (j *JSON) when() WhenFunc {
return j.whenFunc
}
func (j *JSON) setWhen(v WhenFunc) {
j.whenFunc = v
}
func (j *JSON) SkipOnEmpty() *JSON {
rc := *j
rc.skipEmpty = true
return &rc
}
func (j *JSON) skipOnEmpty() bool {
return j.skipEmpty
}
func (j *JSON) setSkipOnEmpty(v bool) {
j.skipEmpty = v
}
func (j *JSON) SkipOnError() *JSON {
rs := *j
rs.skipError = true
return &rs
}
func (j *JSON) shouldSkipOnError() bool {
return j.skipError
}
func (j *JSON) setSkipOnError(v bool) {
j.skipError = v
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import "context"
type Language string
const (
LanguageEN Language = "en"
LanguageRU Language = "ru"
)
type ctxKeyLanguage struct{}
func WithLanguage(ctx context.Context, lang Language) context.Context {
return context.WithValue(ctx, ctxKeyLanguage{}, lang)
}
func LanguageFromContext(ctx context.Context) Language {
if lang, ok := ctx.Value(ctxKeyLanguage{}).(Language); ok {
return lang
}
return LanguageEN
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"net"
)
type MAC struct {
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewMAC() *MAC {
return &MAC{
message: MessageInvalidMAC,
}
}
func (m *MAC) WithMessage(v string) *MAC {
rc := *m
rc.message = v
return &rc
}
func (m *MAC) When(v WhenFunc) *MAC {
rc := *m
rc.whenFunc = v
return &rc
}
func (m *MAC) when() WhenFunc {
return m.whenFunc
}
func (m *MAC) setWhen(v WhenFunc) {
m.whenFunc = v
}
func (m *MAC) SkipOnEmpty() *MAC {
rc := *m
rc.skipEmpty = true
return &rc
}
func (m *MAC) skipOnEmpty() bool {
return m.skipEmpty
}
func (m *MAC) setSkipOnEmpty(v bool) {
m.skipEmpty = v
}
func (m *MAC) SkipOnError() *MAC {
rs := *m
rs.skipError = true
return &rs
}
func (m *MAC) shouldSkipOnError() bool {
return m.skipError
}
func (m *MAC) setSkipOnError(v bool) {
m.skipError = v
}
func (m *MAC) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(m.message))
}
if _, err := net.ParseMAC(v); err != nil {
return NewResult().WithError(NewValidationError(m.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"github.com/raoptimus/validator.go/v2/regexpc"
)
type MatchRegularExpression struct {
message string
pattern string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewMatchRegularExpression(pattern string) *MatchRegularExpression {
return &MatchRegularExpression{
message: MessageValueInvalid,
pattern: pattern,
}
}
func (r *MatchRegularExpression) WithPattern(pattern string) *MatchRegularExpression {
rc := *r
rc.pattern = pattern
return &rc
}
func (r *MatchRegularExpression) WithMessage(message string) *MatchRegularExpression {
rc := *r
rc.message = message
return &rc
}
func (r *MatchRegularExpression) When(v WhenFunc) *MatchRegularExpression {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *MatchRegularExpression) when() WhenFunc {
return r.whenFunc
}
func (r *MatchRegularExpression) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *MatchRegularExpression) SkipOnEmpty() *MatchRegularExpression {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *MatchRegularExpression) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *MatchRegularExpression) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *MatchRegularExpression) SkipOnError() *MatchRegularExpression {
rs := *r
rs.skipError = true
return &rs
}
func (r *MatchRegularExpression) shouldSkipOnError() bool {
return r.skipError
}
func (r *MatchRegularExpression) setSkipOnError(v bool) {
r.skipError = v
}
func (r *MatchRegularExpression) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
rg, err := regexpc.Compile(r.pattern)
if err != nil {
return err
}
if !rg.MatchString(v) {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
func init() {
Translations.Register(LanguageEN, map[string]string{
MessageRequired: MessageRequired,
MessageStringType: MessageStringType,
MessageTooShort: MessageTooShort,
MessageTooLong: MessageTooLong,
MessageCompareEqual: MessageCompareEqual,
MessageCompareNotEqual: MessageCompareNotEqual,
MessageCompareGreaterThan: MessageCompareGreaterThan,
MessageCompareGreaterThanEqual: MessageCompareGreaterThanEqual,
MessageCompareLessThan: MessageCompareLessThan,
MessageCompareLessThanEqual: MessageCompareLessThanEqual,
MessageNotNumber: MessageNotNumber,
MessageNumberTooBig: MessageNumberTooBig,
MessageNumberTooSmall: MessageNumberTooSmall,
MessageNotNumeric: MessageNotNumeric,
MessageInvalidURL: MessageInvalidURL,
MessageInvalidDeepLink: MessageInvalidDeepLink,
MessageInvalidIP: MessageInvalidIP,
MessageIPv4NotAllowed: MessageIPv4NotAllowed,
MessageIPv6NotAllowed: MessageIPv6NotAllowed,
MessageInvalidUUID: MessageInvalidUUID,
MessageInvalidUUIDVersion: MessageInvalidUUIDVersion,
MessageInvalidJSON: MessageInvalidJSON,
MessageInvalidMAC: MessageInvalidMAC,
MessageValueInvalid: MessageValueInvalid,
MessageInvalidEmail: MessageInvalidEmail,
MessageInvalid: MessageInvalid,
MessageTimeFormat: MessageTimeFormat,
MessageTimeTooBig: MessageTimeTooBig,
MessageTimeTooSmall: MessageTimeTooSmall,
MessageInvalidImageMeta: MessageInvalidImageMeta,
MessageInvalidMimeType: MessageInvalidMimeType,
MessageFileTooLarge: MessageFileTooLarge,
MessageInRangeInvalid: MessageInRangeInvalid,
MessageEachIncorrectInput: MessageEachIncorrectInput,
MessageUniqueValues: MessageUniqueValues,
MessageInvalidOGRN: MessageInvalidOGRN,
MessageInvalidOGRNIP: MessageInvalidOGRNIP,
MessageInvalidOGRNLength: MessageInvalidOGRNLength,
MessageInvalidSQL: MessageInvalidSQL,
MessageInvalidMSISDN: MessageInvalidMSISDN,
MessageInvalidHumanText: MessageInvalidHumanText,
MessageNestedNotStruct: MessageNestedNotStruct,
})
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
func init() {
Translations.Register(LanguageRU, map[string]string{
MessageRequired: "Значение не должно быть пустым.",
MessageStringType: "Значение должно быть строкой.",
MessageTooShort: "Значение должно содержать минимум {min} символов.",
MessageTooLong: "Значение должно содержать максимум {max} символов.",
MessageCompareEqual: "Значение должно быть равно «{targetValueOrAttribute}».",
MessageCompareNotEqual: "Значение не должно быть равно «{targetValueOrAttribute}».",
MessageCompareGreaterThan: "Значение должно быть больше «{targetValueOrAttribute}».",
MessageCompareGreaterThanEqual: "Значение должно быть больше или равно «{targetValueOrAttribute}».",
MessageCompareLessThan: "Значение должно быть меньше «{targetValueOrAttribute}».",
MessageCompareLessThanEqual: "Значение должно быть меньше или равно «{targetValueOrAttribute}».",
MessageNotNumber: "Значение должно быть числом.",
MessageNumberTooBig: "Значение не должно превышать {max}.",
MessageNumberTooSmall: "Значение не должно быть меньше {min}.",
MessageNotNumeric: "Значение должно быть числовым.",
MessageInvalidURL: "Значение не является допустимым URL.",
MessageInvalidDeepLink: "Значение не является допустимым deep link URL.",
MessageInvalidIP: "Значение должно быть допустимым IP-адресом.",
MessageIPv4NotAllowed: "Значение не должно быть IPv4-адресом.",
MessageIPv6NotAllowed: "Значение не должно быть IPv6-адресом.",
MessageInvalidUUID: "Недопустимый формат UUID.",
MessageInvalidUUIDVersion: "Версия UUID должна быть равна {version}.",
MessageInvalidJSON: "Значение должно быть допустимым JSON.",
MessageInvalidMAC: "Значение должно быть допустимым MAC-адресом.",
MessageValueInvalid: "Значение недопустимо.",
MessageInvalidEmail: "Значение не является допустимым email-адресом.",
MessageInvalid: "Значение недопустимо.",
MessageTimeFormat: "Формат времени должен соответствовать {format}.",
MessageTimeTooBig: "Время не должно превышать {max}.",
MessageTimeTooSmall: "Время не должно быть меньше {min}.",
MessageInvalidImageMeta: "Значение должно быть структурой ImageMetaData.",
MessageInvalidMimeType: "MimeType должен соответствовать одному из [{mimeTypes}].",
MessageFileTooLarge: "Размер файла не должен превышать {maxFileSizeBytes} байт.",
MessageInRangeInvalid: "Значение недопустимо.",
MessageEachIncorrectInput: "Значение должно быть массивом.",
MessageUniqueValues: "Список значений должен быть уникальным.",
MessageInvalidOGRN: "Значение не является допустимым ОГРН.",
MessageInvalidOGRNIP: "Значение не является допустимым ОГРНИП.",
MessageInvalidOGRNLength: "Значение должно содержать 13 или 15 символов.",
MessageInvalidSQL: "Значение не является допустимым SQL.",
MessageInvalidMSISDN: "Недопустимый формат MSISDN.",
MessageInvalidHumanText: "Значение должно быть обычным текстом.",
MessageNestedNotStruct: "Значение должно быть структурой. Получен %T.",
})
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
const msisdnRegexp = `^\d+$`
type MSISDN struct {
*MatchRegularExpression
}
func NewMSISDN() MSISDN {
return MSISDN{
MatchRegularExpression: NewMatchRegularExpression(msisdnRegexp).
WithMessage(MessageInvalidMSISDN),
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"github.com/raoptimus/validator.go/v2/set"
)
const (
separator = "."
NestedShortcut = "*"
)
type Nested struct {
// normalizeOnce guards lazy rule normalization so that concurrent
// ValidateValue calls on a shared *Nested are safe. Stored as a pointer
// because the builder methods (WithMessage, SkipOnEmpty, etc.) copy the
// struct — sync.Once must not be copied after first use.
normalizeOnce *sync.Once
normalizeErr error
rules RuleSet
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewNested(rules RuleSet) *Nested {
return &Nested{
normalizeOnce: &sync.Once{},
rules: rules,
}
}
func (r *Nested) WithMessage(message string) *Nested {
rc := *r
rc.message = message
return &rc
}
func (r *Nested) When(v WhenFunc) *Nested {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Nested) when() WhenFunc {
return r.whenFunc
}
func (r *Nested) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Nested) SkipOnEmpty() *Nested {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Nested) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Nested) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Nested) notNormalizeRules() *Nested {
rc := *r
rc.normalizeOnce.Do(func() {}) // mark as already normalized
return &rc
}
func (r *Nested) SkipOnError() *Nested {
rs := *r
rs.skipError = true
return &rs
}
func (r *Nested) shouldSkipOnError() bool {
return r.skipError
}
func (r *Nested) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Nested) ValidateValue(ctx context.Context, value any) error {
r.normalizeOnce.Do(func() {
rules, err := r.normalizeRules()
if err != nil {
r.normalizeErr = err
return
}
r.rules = rules
})
if r.normalizeErr != nil {
return r.normalizeErr
}
if value == nil {
return NewResult().WithError(
NewValidationError(fmt.Sprintf(MessageNestedNotStruct, value)),
)
}
vt := reflect.TypeOf(value)
if vt.Kind() == reflect.Pointer {
vt = vt.Elem()
}
if len(r.rules) == 0 {
if vt.Kind() != reflect.Struct {
return fmt.Errorf("nested rule without rules could be used for structs only. %s given",
vt.Kind().String(),
)
}
var err error
data, ok := value.(*set.DataSetStruct)
if !ok {
data, err = set.NewDataSetStruct(value)
if err != nil {
return err
}
}
return Validate(ctx, data, r.rules)
}
if vt.Kind() != reflect.Struct {
return NewResult().WithError(
NewValidationError(fmt.Sprintf(MessageNestedNotStruct, value)).
WithParams(map[string]any{
"attribute": "", // todo: get attribute
"value": value,
}),
)
}
ctx = contextWithNestedDataSet(ctx, value)
var err error
data, ok := value.(*set.DataSetStruct)
if !ok {
data, err = set.NewDataSetStruct(value)
if err != nil {
return err
}
}
compoundResult := NewResult()
fieldNames := make([]string, 0, len(r.rules))
for fieldName := range r.rules {
fieldNames = append(fieldNames, fieldName)
}
sort.Strings(fieldNames)
for _, fieldName := range fieldNames {
rules := r.rules[fieldName]
// todo: parse valuePath
validatedValue, err := data.FieldValue(fieldName)
if err != nil { // todo: check after parsed
return err
}
valuePath := data.FieldAliasName(fieldName)
if err := ValidateValue(ctx, validatedValue, rules...); err != nil {
var itemResult Result
if errors.As(err, &itemResult) {
for _, itemError := range itemResult.Errors() {
var errorValuePath []string
if _, err := strconv.Atoi(valuePath); err != nil {
errorValuePath = strings.Split(valuePath, separator)
} else {
errorValuePath = []string{valuePath}
}
if len(itemError.ValuePath) > 0 {
errorValuePath = append(errorValuePath, itemError.ValuePath...)
}
itemError.ValuePath = errorValuePath
compoundResult = compoundResult.WithError(itemError)
}
continue
}
return err
}
}
if !compoundResult.IsValid() {
return compoundResult
}
return nil
}
func (r *Nested) normalizeRules() (RuleSet, error) {
nRules := r.rules
for {
rulesMap := make(map[string]RuleSet, len(nRules))
needBreak := true
for valuePath, rules := range nRules {
if valuePath == NestedShortcut {
return nil, errors.New("bare shortcut is prohibited. Use 'Nested' rule instead")
}
if valuePath == "" {
continue
}
idx := strings.LastIndex(valuePath, separator)
if idx < 0 {
continue
}
needBreak = false
lastValuePath := valuePath[idx+1:]
remainingValuePath := strings.ReplaceAll(valuePath[:idx], separator, NestedShortcut)
if _, ok := rulesMap[remainingValuePath]; !ok {
rulesMap[remainingValuePath] = RuleSet{lastValuePath: rules}
} else {
rulesMap[remainingValuePath][lastValuePath] = rules
}
delete(nRules, valuePath)
}
for valuePath, nestedRules := range rulesMap {
nRules[valuePath] = []Rule{
NewEach(NewNested(nestedRules).notNormalizeRules()),
}
}
if needBreak {
break
}
}
return nRules, nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
)
type Number struct {
minValue int64
maxValue int64
notNumberMessage string
tooBigMessage string
tooSmallMessage string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewNumber(minVal, maxVal int64) *Number {
return &Number{
minValue: minVal,
maxValue: maxVal,
notNumberMessage: MessageNotNumber,
tooBigMessage: MessageNumberTooBig,
tooSmallMessage: MessageNumberTooSmall,
}
}
func (r *Number) WithTooBigMessage(message string) *Number {
rc := *r
rc.tooBigMessage = message
return &rc
}
func (r *Number) WithTooSmallMessage(message string) *Number {
rc := *r
rc.tooSmallMessage = message
return &rc
}
func (r *Number) WithNotNumberMessage(message string) *Number {
rc := *r
rc.notNumberMessage = message
return &rc
}
func (r *Number) When(v WhenFunc) *Number {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Number) when() WhenFunc {
return r.whenFunc
}
func (r *Number) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Number) SkipOnEmpty() *Number {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Number) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Number) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Number) SkipOnError() *Number {
rs := *r
rs.skipError = true
return &rs
}
func (r *Number) shouldSkipOnError() bool {
return r.skipError
}
func (r *Number) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Number) ValidateValue(_ context.Context, value any) error {
if value == nil {
return NewResult().WithError(NewValidationError(r.notNumberMessage))
}
var i int64
switch v := value.(type) {
case *int8:
i = int64(*v)
case *uint8:
i = int64(*v)
case *int:
i = int64(*v)
case *uint:
i = int64(*v) //nolint:gosec // overflow acceptable for validation
case *int16:
i = int64(*v)
case *uint16:
i = int64(*v)
case *int32:
i = int64(*v)
case *uint32:
i = int64(*v)
case *int64:
i = *v
case *uint64:
i = int64(*v) //nolint:gosec // overflow acceptable for validation
case int8:
i = int64(v)
case uint8:
i = int64(v)
case int:
i = int64(v)
case uint:
i = int64(v) //nolint:gosec // overflow acceptable for validation
case int16:
i = int64(v)
case uint16:
i = int64(v)
case int32:
i = int64(v)
case uint32:
i = int64(v)
case int64:
i = v
case uint64:
i = int64(v) //nolint:gosec // overflow acceptable for validation
default:
return NewResult().WithError(NewValidationError(r.notNumberMessage))
}
result := NewResult()
if i < r.minValue {
result = result.WithError(
NewValidationError(r.tooSmallMessage).
WithParams(map[string]any{
"min": r.minValue,
"max": r.maxValue,
}),
)
}
if i > r.maxValue {
result = result.WithError(
NewValidationError(r.tooBigMessage).
WithParams(map[string]any{
"min": r.minValue,
"max": r.maxValue,
}),
)
}
if !result.IsValid() {
return result
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
)
type Numeric struct {
minValue float64
maxValue float64
notNumericMessage string
tooBigMessage string
tooSmallMessage string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewNumeric(minVal, maxVal float64) *Numeric {
return &Numeric{
minValue: minVal,
maxValue: maxVal,
notNumericMessage: MessageNotNumeric,
tooBigMessage: MessageNumberTooBig,
tooSmallMessage: MessageNumberTooSmall,
}
}
func (n *Numeric) WithTooBigMessage(message string) *Numeric {
rc := *n
rc.tooBigMessage = message
return &rc
}
func (n *Numeric) WithTooSmallMessage(message string) *Numeric {
rc := *n
rc.tooSmallMessage = message
return &rc
}
func (n *Numeric) WithNotNumericMessage(message string) *Numeric {
rc := *n
rc.notNumericMessage = message
return &rc
}
func (n *Numeric) When(v WhenFunc) *Numeric {
rc := *n
rc.whenFunc = v
return &rc
}
func (n *Numeric) when() WhenFunc {
return n.whenFunc
}
func (n *Numeric) setWhen(v WhenFunc) {
n.whenFunc = v
}
func (n *Numeric) SkipOnEmpty() *Numeric {
rc := *n
rc.skipEmpty = true
return &rc
}
func (n *Numeric) skipOnEmpty() bool {
return n.skipEmpty
}
func (n *Numeric) setSkipOnEmpty(v bool) {
n.skipEmpty = v
}
func (n *Numeric) SkipOnError() *Numeric {
rs := *n
rs.skipError = true
return &rs
}
func (n *Numeric) shouldSkipOnError() bool {
return n.skipError
}
func (n *Numeric) setSkipOnError(v bool) {
n.skipError = v
}
func (n *Numeric) ValidateValue(_ context.Context, value any) error {
if value == nil {
return NewResult().WithError(NewValidationError(n.notNumericMessage))
}
var i float64
switch v := value.(type) {
case *float32:
i = float64(*v)
case *float64:
i = *v
case float32:
i = float64(v)
case float64:
i = v
default:
return NewResult().WithError(NewValidationError(n.notNumericMessage))
}
result := NewResult()
if i < n.minValue {
result = result.WithError(
NewValidationError(n.tooSmallMessage).
WithParams(map[string]any{
"min": n.minValue,
"max": n.maxValue,
}),
)
}
if i > n.maxValue {
result = result.WithError(
NewValidationError(n.tooBigMessage).
WithParams(map[string]any{
"min": n.minValue,
"max": n.maxValue,
}),
)
}
if !result.IsValid() {
return result
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"strconv"
"github.com/raoptimus/validator.go/v2/regexpc"
)
const (
ogrnNumberRegExp = "^[0-9]+$"
ogrnNumberLen = 13
ogrnipNumberLen = 15
ogrnDivisor = 11
ogrnipDivisor = 13
ogrnControlModulo = 10
ogrnPrefixLen = 12
ogrnipPrefixLen = 14
)
type OGRN struct {
ogrnMessage string
ogrnipMessage string
invalidOGRNLengthMessage string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewOGRN() *OGRN {
return &OGRN{
ogrnMessage: MessageInvalidOGRN,
ogrnipMessage: MessageInvalidOGRNIP,
invalidOGRNLengthMessage: MessageInvalidOGRNLength,
}
}
func (o *OGRN) WithOGRNMessage(v string) *OGRN {
rc := *o
rc.ogrnMessage = v
return &rc
}
func (o *OGRN) WithOGRNIPMessage(v string) *OGRN {
rc := *o
rc.ogrnipMessage = v
return &rc
}
func (o *OGRN) When(v WhenFunc) *OGRN {
rc := *o
rc.whenFunc = v
return &rc
}
func (o *OGRN) when() WhenFunc {
return o.whenFunc
}
func (o *OGRN) setWhen(v WhenFunc) {
o.whenFunc = v
}
func (o *OGRN) SkipOnEmpty() *OGRN {
rc := *o
rc.skipEmpty = true
return &rc
}
func (o *OGRN) skipOnEmpty() bool {
return o.skipEmpty
}
func (o *OGRN) setSkipOnEmpty(v bool) {
o.skipEmpty = v
}
func (o *OGRN) SkipOnError() *OGRN {
rs := *o
rs.skipError = true
return &rs
}
func (o *OGRN) shouldSkipOnError() bool {
return o.skipError
}
func (o *OGRN) setSkipOnError(v bool) {
o.skipError = v
}
func (o *OGRN) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
rg, err := regexpc.Compile(ogrnNumberRegExp)
if err != nil {
return err
}
if !rg.MatchString(v) {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
switch len(v) {
case ogrnNumberLen:
return o.validateOGRN(v)
case ogrnipNumberLen:
return o.validateOGRNIP(v)
default:
return NewResult().WithError(NewValidationError(o.invalidOGRNLengthMessage))
}
}
func (o *OGRN) validateOGRN(ogrn string) error {
firstDigit := ogrn[0]
if firstDigit != '1' && firstDigit != '5' {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
num, err := strconv.ParseInt(ogrn[:ogrnPrefixLen], 10, 64)
if err != nil {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
remainder := num % ogrnDivisor
controlDigit := int(remainder % ogrnControlModulo)
// Если остаток равен 10, то контрольное число должно быть 0
if remainder == ogrnControlModulo {
controlDigit = 0
}
lastDigit, err := strconv.Atoi(ogrn[ogrnPrefixLen:])
if err != nil {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
if controlDigit != lastDigit {
return NewResult().WithError(NewValidationError(o.ogrnMessage))
}
return nil
}
func (o *OGRN) validateOGRNIP(ogrnip string) error {
if ogrnip[0] != '3' {
return NewResult().WithError(NewValidationError(o.ogrnipMessage))
}
num, err := strconv.ParseInt(ogrnip[:ogrnipPrefixLen], 10, 64)
if err != nil {
return NewResult().WithError(NewValidationError(o.ogrnipMessage))
}
remainder := num % ogrnipDivisor
controlDigit := int(remainder % ogrnControlModulo)
lastDigit, err := strconv.Atoi(ogrnip[ogrnipPrefixLen:])
if err != nil {
return NewResult().WithError(NewValidationError(o.ogrnipMessage))
}
if controlDigit != lastDigit {
return NewResult().WithError(NewValidationError(o.ogrnipMessage))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
)
type OR struct {
rules []Rule
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewOR(message string, rules ...Rule) *OR {
return &OR{
message: message,
rules: rules,
}
}
func (o *OR) WithMessage(message string) *OR {
rc := *o
rc.message = message
return &rc
}
func (o *OR) When(v WhenFunc) *OR {
rc := *o
rc.whenFunc = v
return &rc
}
func (o *OR) when() WhenFunc {
return o.whenFunc
}
func (o *OR) setWhen(v WhenFunc) {
o.whenFunc = v
}
func (o *OR) SkipOnEmpty() *OR {
rc := *o
rc.skipEmpty = true
return &rc
}
func (o *OR) skipOnEmpty() bool {
return o.skipEmpty
}
func (o *OR) setSkipOnEmpty(v bool) {
o.skipEmpty = v
}
func (o *OR) SkipOnError() *OR {
rs := *o
rs.skipError = true
return &rs
}
func (o *OR) shouldSkipOnError() bool {
return o.skipError
}
func (o *OR) setSkipOnError(v bool) {
o.skipError = v
}
func (o *OR) ValidateValue(ctx context.Context, value any) error {
for _, r := range o.rules {
if err := r.ValidateValue(ctx, value); err == nil {
return nil
}
}
return NewResult().WithError(NewValidationError(o.message))
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package regexpc
import (
"regexp"
"sync"
)
type regexpCache struct {
mu sync.Locker
data map[string]*regexp.Regexp
}
var regexpCacheDefault = regexpCache{mu: &sync.Mutex{}, data: make(map[string]*regexp.Regexp)}
func Compile(pattern string) (*regexp.Regexp, error) {
regexpCacheDefault.mu.Lock()
defer regexpCacheDefault.mu.Unlock()
if r, ok := regexpCacheDefault.data[pattern]; ok {
return r, nil
}
if r, err := regexp.Compile(pattern); err != nil {
return nil, err
} else {
regexpCacheDefault.data[pattern] = r
return r, nil
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"reflect"
)
type Required struct {
message string
whenFunc WhenFunc
skipError bool
}
func NewRequired() *Required {
return &Required{
message: MessageRequired,
}
}
func (r *Required) When(v WhenFunc) *Required {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Required) when() WhenFunc {
return r.whenFunc
}
func (r *Required) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Required) WithMessage(message string) *Required {
rc := *r
rc.message = message
return &rc
}
// Deprecated: should be removed
func (r *Required) WithAllowZeroValue() *Required {
return r
}
func (r *Required) SkipOnError() *Required {
rs := *r
rs.skipError = true
return &rs
}
func (r *Required) shouldSkipOnError() bool {
return r.skipError
}
func (r *Required) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Required) ValidateValue(_ context.Context, value any) error {
v := reflect.ValueOf(value)
if valueIsEmpty(v) {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"strings"
)
//nolint:errname // Result is a well-known type in the public API
type Result struct {
errors []*ValidationError
}
func NewResult() Result {
return Result{
errors: make([]*ValidationError, 0),
}
}
func (s Result) WithError(errs ...*ValidationError) Result {
s.errors = append(s.errors, errs...)
return s
}
func (s Result) Error() string {
var summary strings.Builder
for _, v := range s.errors {
if len(v.ValuePath) > 0 {
summary.WriteString(strings.Join(v.ValuePath, "."))
summary.WriteString(": ")
}
summary.WriteString(strings.TrimRight(v.Message, "."))
summary.WriteString(". ")
}
return strings.TrimRight(summary.String(), " ")
}
func (s Result) IsValid() bool {
return len(s.errors) == 0
}
func (s Result) Errors() []*ValidationError {
r := s.errors
return r
}
func (s Result) ErrorMessagesIndexedByPath() map[string][]string {
errList := make(map[string][]string)
for _, err := range s.errors {
stringValuePath := strings.Join(err.ValuePath, separator)
if _, ok := errList[stringValuePath]; !ok {
errList[stringValuePath] = []string{err.Message}
} else {
errList[stringValuePath] = append(errList[stringValuePath], err.Message)
}
}
return errList
}
func (s Result) AttributeErrorMessagesIndexedByPath(attribute string) map[string][]string {
errList := make(map[string][]string)
for _, err := range s.errors {
var first string
if len(err.ValuePath) > 0 {
first = err.ValuePath[0]
}
if first != attribute {
continue
}
stringValuePath := strings.Join(err.ValuePath[1:], separator)
if _, ok := errList[stringValuePath]; !ok {
errList[stringValuePath] = []string{err.Message}
} else {
errList[stringValuePath] = append(errList[stringValuePath], err.Message)
}
}
return errList
}
func (s Result) CommonErrorMessages() []string {
errList := make([]string, 0, len(s.errors))
for _, err := range s.errors {
var first string
if len(err.ValuePath) > 0 {
first = err.ValuePath[0]
}
if first != "" {
continue
}
errList = append(errList, err.Message)
}
return errList
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
type (
Rules []Rule
RuleSet map[string]Rules
)
func (rs Rules) SkipOnError() {
for i, r := range rs {
if rse, ok := r.(RuleSkipError); ok {
rse.setSkipOnError(true)
}
rs[i] = r
}
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package set
import "errors"
type DataSetAny struct {
data any
}
func NewDataSetAny(data any) *DataSetAny {
return &DataSetAny{
data: data,
}
}
func (ds *DataSetAny) FieldValue(_ string) (any, error) {
return nil, errors.New("not supported")
}
func (ds *DataSetAny) FieldAliasName(name string) string {
return name
}
func (ds *DataSetAny) Name() Name {
return NameAny
}
func (ds *DataSetAny) Data() any {
return ds.data
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package set
import (
"errors"
"fmt"
)
var ErrUndefinedField = errors.New("undefined field")
type UndefinedFieldError struct {
dataSetName string
attributeName string
}
func NewUndefinedFieldError(dataSetPtr any, attributeName string) *UndefinedFieldError {
return &UndefinedFieldError{
dataSetName: fmt.Sprintf("%T", dataSetPtr),
attributeName: attributeName,
}
}
func (u *UndefinedFieldError) Error() string {
return ErrUndefinedField.Error() + ": " + u.dataSetName + "." + u.attributeName
}
func (u *UndefinedFieldError) Unwrap() error {
return ErrUndefinedField
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package set
type DataSetMap struct {
data map[string]any
}
func NewDataSetMap(data map[string]any) *DataSetMap {
return &DataSetMap{
data: data,
}
}
func (ds *DataSetMap) FieldValue(name string) (any, error) {
v, ok := ds.data[name]
if !ok {
return nil, NewUndefinedFieldError(ds.data, name)
}
return v, nil
}
func (ds *DataSetMap) FieldAliasName(name string) string {
return name
}
func (ds *DataSetMap) Name() Name {
return NameMap
}
func (ds *DataSetMap) Data() any {
return ds.data
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package set
import (
"errors"
"fmt"
"reflect"
"strings"
)
var ErrDataMustBeStructPointer = errors.New("dataSet must be a struct pointer")
type DataSetStruct struct {
dataPtr any // pointer on struct
dataStruct reflect.Value // struct
dataType reflect.Type
}
func NewDataSetStruct(data any) (*DataSetStruct, error) {
var dataPtr any
dataType := reflect.TypeOf(data)
if dataType.Kind() == reflect.Pointer {
dataType = dataType.Elem()
dataPtr = data
} else {
dataPtr = &data
}
if dataType.Kind() != reflect.Struct {
return nil, fmt.Errorf("%w, got %T", ErrDataMustBeStructPointer, data)
}
return &DataSetStruct{
dataPtr: dataPtr,
dataType: dataType,
dataStruct: reflect.Indirect(reflect.ValueOf(data)),
}, nil
}
//
// func (ds *DataSetStruct) Map() map[string]any {
// l := ds.dataType.NumField()
// data := make(map[string]any, l)
// for i := 0; i < l; i++ {
// f := ds.dataType.Field(i)
// name := ds.FieldAliasName(f.Name)
// data[name] = ds.dataStruct.Field(i).Interface()
// }
//
// return data
// }
func (ds *DataSetStruct) FieldValue(name string) (any, error) {
fieldValue := ds.dataStruct.FieldByName(name)
if !fieldValue.IsValid() {
return nil, NewUndefinedFieldError(ds.dataStruct.Interface(), name)
}
if fieldValue.Kind() == reflect.Pointer && fieldValue.IsNil() {
return nil, nil
}
return fieldValue.Interface(), nil
}
func (ds *DataSetStruct) FieldAliasName(name string) string {
if field, ok := ds.dataType.FieldByName(name); ok {
if v, ok := field.Tag.Lookup("json"); ok {
if name, _, found := strings.Cut(v, ","); found {
v = name
}
name = v
}
}
return name
}
func (ds *DataSetStruct) Name() Name {
return NameStruct
}
func (ds *DataSetStruct) Data() any {
return ds.dataPtr
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"github.com/xwb1989/sqlparser"
)
type SQL struct {
message string
whenFunc WhenFunc
asWhereClause bool
skipEmpty bool
skipError bool
}
func NewSQL() *SQL {
return &SQL{
message: MessageInvalidSQL,
}
}
func (r *SQL) AsWhereClause() *SQL {
rc := *r
rc.asWhereClause = true
return &rc
}
func (r *SQL) WithMessage(message string) *SQL {
rc := *r
rc.message = message
return &rc
}
func (r *SQL) When(v WhenFunc) *SQL {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *SQL) when() WhenFunc {
return r.whenFunc
}
func (r *SQL) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *SQL) SkipOnEmpty() *SQL {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *SQL) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *SQL) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *SQL) SkipOnError() *SQL {
rs := *r
rs.skipError = true
return &rs
}
func (r *SQL) shouldSkipOnError() bool {
return r.skipError
}
func (r *SQL) setSkipOnError(v bool) {
r.skipError = v
}
func (r *SQL) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
if r.asWhereClause {
v = "select * from x where " + v
}
if _, err := sqlparser.Parse(v); err != nil {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"strings"
"unicode/utf8"
)
type StringLength struct {
// string user-defined error message used when the value is not a string.
message string
// string user-defined error message used when the length of the value is smaller than {see min}.
tooShortMessage string
// string user-defined error message used when the length of the value is greater than {see max}.
tooLongMessage string
minLength, maxLength int
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewStringLength(minLen, maxLen int) *StringLength {
return &StringLength{
message: MessageStringType,
tooShortMessage: MessageTooShort,
tooLongMessage: MessageTooLong,
minLength: minLen,
maxLength: maxLen,
}
}
func (r *StringLength) WithMessage(message string) *StringLength {
rc := *r
rc.message = message
return &rc
}
func (r *StringLength) WithTooShortMessage(message string) *StringLength {
rc := *r
rc.tooShortMessage = message
return &rc
}
func (r *StringLength) WithTooLongMessage(message string) *StringLength {
rc := *r
rc.tooLongMessage = message
return &rc
}
func (r *StringLength) When(v WhenFunc) *StringLength {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *StringLength) when() WhenFunc {
return r.whenFunc
}
func (r *StringLength) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *StringLength) SkipOnEmpty() *StringLength {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *StringLength) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *StringLength) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *StringLength) SkipOnError() *StringLength {
rs := *r
rs.skipError = true
return &rs
}
func (r *StringLength) shouldSkipOnError() bool {
return r.skipError
}
func (r *StringLength) setSkipOnError(v bool) {
r.skipError = v
}
func (r *StringLength) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
result := NewResult()
v = strings.TrimSpace(v)
l := utf8.RuneCountInString(v)
if l < r.minLength {
result = NewResult().
WithError(
NewValidationError(r.tooShortMessage).
WithParams(map[string]any{
"min": r.minLength,
"max": r.maxLength,
}),
)
}
if l > r.maxLength {
result = NewResult().
WithError(
NewValidationError(r.tooLongMessage).
WithParams(map[string]any{
"min": r.minLength,
"max": r.maxLength,
}),
)
}
if !result.IsValid() {
return result
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"time"
"github.com/raoptimus/validator.go/v2/vtype"
)
type TimeFunc func(ctx context.Context) (time.Time, error)
type Time struct {
message string
formatMessage string
tooBigMessage string
tooSmallMessage string
format string
min TimeFunc
max TimeFunc
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewTime() *Time {
return &Time{
message: MessageInvalid,
formatMessage: MessageTimeFormat,
tooBigMessage: MessageTimeTooBig,
tooSmallMessage: MessageTimeTooSmall,
format: time.RFC3339,
min: nil,
max: nil,
}
}
func (r *Time) WithMessage(message string) *Time {
rc := *r
rc.message = message
return &rc
}
func (r *Time) WithFormatMessage(message string) *Time {
rc := *r
rc.formatMessage = message
return &rc
}
func (r *Time) WithTooSmallMessage(message string) *Time {
rc := *r
rc.tooSmallMessage = message
return &rc
}
func (r *Time) WithTooBigMessage(message string) *Time {
rc := *r
rc.tooBigMessage = message
return &rc
}
func (r *Time) WithFormat(format string) *Time {
rc := *r
rc.format = format
return &rc
}
func (r *Time) WithMin(minFunc TimeFunc) *Time {
rc := *r
rc.min = minFunc
return &rc
}
func (r *Time) WithMax(maxFunc TimeFunc) *Time {
rc := *r
rc.max = maxFunc
return &rc
}
func (r *Time) When(v WhenFunc) *Time {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *Time) when() WhenFunc {
return r.whenFunc
}
func (r *Time) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *Time) SkipOnEmpty() *Time {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *Time) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *Time) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *Time) SkipOnError() *Time {
rs := *r
rs.skipError = true
return &rs
}
func (r *Time) shouldSkipOnError() bool {
return r.skipError
}
func (r *Time) setSkipOnError(v bool) {
r.skipError = v
}
func (r *Time) ValidateValue(ctx context.Context, value any) error {
v, valid := indirectValue(value)
if !valid {
return NewResult().WithError(NewValidationError(r.message))
}
vStr, okStr := toString(value)
vObj, okObj := v.(vtype.Time)
if !okStr && !okObj {
return NewResult().WithError(NewValidationError(r.message))
}
if okObj {
vStr = vObj.String()
}
vt, err := time.Parse(r.format, vStr)
if err != nil {
return NewResult().WithError(
NewValidationError(r.formatMessage).
WithParams(
map[string]any{
"format": r.format,
},
),
)
}
result := NewResult()
if r.min != nil {
minTime, err := r.min(ctx)
if err != nil {
return err
}
if vt.Before(minTime) {
result = result.WithError(
NewValidationError(r.tooSmallMessage).
WithParams(
map[string]any{
"min": minTime,
},
),
)
}
}
if r.max != nil {
maxTime, err := r.max(ctx)
if err != nil {
return err
}
if vt.After(maxTime) {
result = result.WithError(
NewValidationError(r.tooBigMessage).
WithParams(
map[string]any{
"max": maxTime,
},
),
)
}
}
if result.IsValid() {
if okObj {
*vObj.Time() = vt
}
return nil
}
return result
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"fmt"
"maps"
"strings"
"sync"
)
type Translator interface {
Translate(
ctx context.Context,
id string,
params map[string]any,
) string
}
type DummyTranslator struct {
}
func (d *DummyTranslator) Translate(_ context.Context, id string, params map[string]any) string {
return replacePlaceholders(id, params)
}
type CatalogTranslator struct {
catalog *TranslationCatalog
}
func NewCatalogTranslator(catalog *TranslationCatalog) *CatalogTranslator {
return &CatalogTranslator{catalog: catalog}
}
func (t *CatalogTranslator) Translate(ctx context.Context, id string, params map[string]any) string {
lang := LanguageFromContext(ctx)
if msg, ok := t.catalog.Get(lang, id); ok {
return replacePlaceholders(msg, params)
}
return replacePlaceholders(id, params)
}
type TranslationCatalog struct {
mu sync.RWMutex
translations map[Language]map[string]string
}
func NewTranslationCatalog() *TranslationCatalog {
return &TranslationCatalog{
translations: make(map[Language]map[string]string),
}
}
func (c *TranslationCatalog) Register(lang Language, messages map[string]string) {
c.mu.Lock()
defer c.mu.Unlock()
existing, ok := c.translations[lang]
if !ok {
existing = make(map[string]string, len(messages))
c.translations[lang] = existing
}
maps.Copy(existing, messages)
}
func (c *TranslationCatalog) Get(lang Language, id string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
msgs, ok := c.translations[lang]
if !ok {
return "", false
}
msg, ok := msgs[id]
return msg, ok
}
func (c *TranslationCatalog) Missing(lang Language) []string {
c.mu.RLock()
defer c.mu.RUnlock()
enMsgs := c.translations[LanguageEN]
langMsgs := c.translations[lang]
var missing []string
for id := range enMsgs {
if _, ok := langMsgs[id]; !ok {
missing = append(missing, id)
}
}
return missing
}
var Translations = NewTranslationCatalog()
var DefaultTranslator Translator = NewCatalogTranslator(Translations)
func SetTranslator(t Translator) {
DefaultTranslator = t
}
func replacePlaceholders(id string, params map[string]any) string {
for name, value := range params {
attrPlaceholder := "{" + name + "}"
if !strings.Contains(id, attrPlaceholder) {
continue
}
id = strings.ReplaceAll(id, attrPlaceholder, fmt.Sprintf("%v", value))
}
return id
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"reflect"
)
type UniqueValues struct {
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewUniqueValues() *UniqueValues {
return &UniqueValues{
message: MessageUniqueValues,
}
}
func (r *UniqueValues) WithMessage(message string) *UniqueValues {
rc := *r
rc.message = message
return &rc
}
func (r *UniqueValues) When(v WhenFunc) *UniqueValues {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *UniqueValues) when() WhenFunc {
return r.whenFunc
}
func (r *UniqueValues) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *UniqueValues) SkipOnEmpty() *UniqueValues {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *UniqueValues) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *UniqueValues) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *UniqueValues) SkipOnError() *UniqueValues {
rs := *r
rs.skipError = true
return &rs
}
func (r *UniqueValues) shouldSkipOnError() bool {
return r.skipError
}
func (r *UniqueValues) setSkipOnError(v bool) {
r.skipError = v
}
// ValidateValue checks that all elements in a slice are unique.
// For pointer slices ([]*T), elements are dereferenced before comparison
// so that two distinct pointers with equal values are detected as duplicates.
//
// The element type determines the comparison strategy:
// - Statically comparable types (primitives, fixed-size structs, pointers):
// O(n) hash map lookup via map[any]struct{}.
// - Everything else (structs with slice/map fields, interface slices):
// O(n) bucketed FNV-64a hash lookup with reflect.DeepEqual collision
// fallback — see validateHashKey.
//
// Interface-typed slices ([]any, []io.Reader, ...) deliberately skip the
// comparable fast path: reflect.Type.Comparable reports true for interface
// kinds because they are syntactically comparable, but the runtime hash
// panics on non-comparable dynamic values. Routing them through the
// bucketed-hash path handles any dynamic type correctly.
func (r *UniqueValues) ValidateValue(_ context.Context, value any) error {
// Untyped nil is rejected here (no type info means we cannot treat it
// as a slice). A typed nil slice (var s []T = nil) has Kind == Slice
// and reaches the validation loops below with Len == 0, which passes
// as "trivially unique" — matches the empty-slice behavior.
if value == nil {
return NewResult().WithError(NewValidationError(r.message))
}
// Type-specialized fast paths skip reflection and the per-element
// any-boxing that map[any]struct{} forces. Covers the full set of
// comparable primitives — everything else falls through to reflect.
// Note: []byte is []uint8, so the uint8 case handles it too.
switch s := value.(type) {
case []string:
return uniquePrimitive(s, r.message)
case []int:
return uniquePrimitive(s, r.message)
case []int8:
return uniquePrimitive(s, r.message)
case []int16:
return uniquePrimitive(s, r.message)
case []int32:
return uniquePrimitive(s, r.message)
case []int64:
return uniquePrimitive(s, r.message)
case []uint:
return uniquePrimitive(s, r.message)
case []uint8:
return uniquePrimitive(s, r.message)
case []uint16:
return uniquePrimitive(s, r.message)
case []uint32:
return uniquePrimitive(s, r.message)
case []uint64:
return uniquePrimitive(s, r.message)
case []float32:
return uniquePrimitive(s, r.message)
case []float64:
return uniquePrimitive(s, r.message)
case []bool:
return uniquePrimitive(s, r.message)
}
if reflect.TypeOf(value).Kind() != reflect.Slice {
return NewResult().WithError(NewValidationError(r.message))
}
vs := reflect.ValueOf(value)
// Determine the actual element type, dereferencing one pointer level
// to check comparability of the underlying struct, not the pointer.
elemType := vs.Type().Elem()
if elemType.Kind() == reflect.Pointer {
elemType = elemType.Elem()
}
if elemType.Kind() != reflect.Interface && elemType.Comparable() {
return r.validateComparable(vs)
}
return r.validateHashKey(vs)
}
// uniquePrimitive is a generic, allocation-minimal duplicate check for
// slices of comparable primitives. Avoids both reflect and the per-element
// any-boxing of validateComparable.
func uniquePrimitive[T comparable](s []T, message string) error {
set := make(map[T]struct{}, len(s))
for _, v := range s {
if _, ok := set[v]; ok {
return NewResult().WithError(NewValidationError(message))
}
set[v] = struct{}{}
}
return nil
}
// validateComparable uses a hash map for O(n) duplicate detection.
// Pointer elements are dereferenced so that two *T with equal fields
// produce the same map key (the underlying value, not the address).
func (r *UniqueValues) validateComparable(vs reflect.Value) error {
n := vs.Len()
isPtr := vs.Type().Elem().Kind() == reflect.Pointer
set := make(map[any]struct{}, n)
for i := 0; i < n; i++ {
v := vs.Index(i)
if isPtr && !v.IsNil() {
v = v.Elem()
}
key := v.Interface()
if _, ok := set[key]; ok {
return NewResult().WithError(NewValidationError(r.message))
}
set[key] = struct{}{}
}
return nil
}
// validateHashKey handles slices of non-comparable or interface-typed
// elements using O(n) average-case bucketed hashing. Elements are grouped
// by an FNV-64a hash of their field structure (see hashvalue.go).
//
// The common case — every element produces a unique 64-bit hash — is
// handled by a single map[uint64]int (firstIdx). Only on a hash collision
// is the lazily-allocated overflow map populated, so n unique elements
// trigger n map writes and zero slice allocations.
//
// Because a 64-bit hash cannot prove equality, any collision is verified
// with reflect.DeepEqual: a hash collision between unequal values is not
// a false duplicate, it just degrades that bucket to O(k) probing.
func (r *UniqueValues) validateHashKey(vs reflect.Value) error {
n := vs.Len()
isPtr := vs.Type().Elem().Kind() == reflect.Pointer
firstIdx := make(map[uint64]int, n)
var overflow map[uint64][]int
hw := newHasher()
for i := 0; i < n; i++ {
v := vs.Index(i)
if isPtr && !v.IsNil() {
v = v.Elem()
}
hw.reset()
hashValue(&hw, v)
key := hw.state
j, seen := firstIdx[key]
if !seen {
firstIdx[key] = i
continue
}
curr := v.Interface()
if r.equalAt(vs, j, curr, isPtr) {
return NewResult().WithError(NewValidationError(r.message))
}
for _, k := range overflow[key] {
if r.equalAt(vs, k, curr, isPtr) {
return NewResult().WithError(NewValidationError(r.message))
}
}
if overflow == nil {
overflow = make(map[uint64][]int)
}
overflow[key] = append(overflow[key], i)
}
return nil
}
// equalAt compares the element at index j against curr using DeepEqual,
// dereferencing j's pointer when the slice element type is a pointer.
func (r *UniqueValues) equalAt(vs reflect.Value, j int, curr any, isPtr bool) bool {
prev := vs.Index(j)
if isPtr && !prev.IsNil() {
prev = prev.Elem()
}
return reflect.DeepEqual(prev.Interface(), curr)
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"net/url"
"slices"
"strings"
"golang.org/x/net/idna"
"github.com/raoptimus/validator.go/v2/regexpc"
)
var regexpDomain, _ = regexpc.Compile(`://([^/]+)`) //nolint:errcheck // pattern is constant
const AllowAnyURLSchema = "*"
const defaultURLRegexpPattern = `^{schemes}:\/\/(([a-zA-Z0-9][a-zA-Z0-9_-]*)(\.[a-zA-Z0-9][a-zA-Z0-9_-]*)+)(?::\d{1,5})?([?\/#].*$|$)`
type URL struct {
pattern string
validSchemes []string
enableIDN bool
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewURL() *URL {
return &URL{
pattern: defaultURLRegexpPattern,
validSchemes: []string{"http", "https"},
enableIDN: false,
message: MessageInvalidURL,
}
}
func (r *URL) WithPattern(pattern string) *URL {
rc := *r
rc.pattern = pattern
return &rc
}
func (r *URL) WithValidScheme(scheme ...string) *URL {
rc := *r
for i, sh := range scheme {
if sh == AllowAnyURLSchema {
scheme[i] = ".*?"
}
}
rc.validSchemes = scheme
return &rc
}
func (r *URL) WithMessage(message string) *URL {
rc := *r
rc.message = message
return &rc
}
func (r *URL) WithEnableIDN() *URL {
rc := *r
rc.enableIDN = true
return &rc
}
func (r *URL) When(v WhenFunc) *URL {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *URL) when() WhenFunc {
return r.whenFunc
}
func (r *URL) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *URL) SkipOnEmpty() *URL {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *URL) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *URL) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *URL) SkipOnError() *URL {
rs := *r
rs.skipError = true
return &rs
}
func (r *URL) shouldSkipOnError() bool {
return r.skipError
}
func (r *URL) setSkipOnError(v bool) {
r.skipError = v
}
func (r *URL) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
// make sure the length is limited to avoid DOS attacks
if !ok || len(v) >= 2000 {
return NewResult().WithError(NewValidationError(r.message))
}
if r.enableIDN {
v = r.convertIDN(v)
}
pattern := r.getPattern()
rgxp, err := regexpc.Compile(pattern)
if err != nil {
return NewResult().WithError(NewValidationError(r.message))
}
if !rgxp.MatchString(v) {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
func (r *URL) convertIDN(value string) string {
if !strings.Contains(value, "://") {
return r.idnToASCII(value)
}
return regexpDomain.ReplaceAllStringFunc(value, func(m string) string {
p := regexpDomain.FindStringSubmatch(m)
return "://" + r.idnToASCII(p[1])
})
}
func (r *URL) idnToASCII(idn string) string {
d, err := idna.ToASCII(idn)
if err == nil {
return d
}
return idn
}
func (r *URL) getPattern() string {
if !strings.Contains(r.pattern, "{schemes}") {
return r.pattern
}
return strings.ReplaceAll(
r.pattern,
"{schemes}",
"((?i)"+strings.Join(r.validSchemes, "|")+")",
)
}
type DeepLinkURL struct {
invalidSchemes []string
message string
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewDeepLinkURL() *DeepLinkURL {
return &DeepLinkURL{
invalidSchemes: []string{"http", "https", "ws"},
message: MessageInvalidDeepLink,
}
}
func (r *DeepLinkURL) WithInvalidSchemes(schemes []string) *DeepLinkURL {
rc := *r
rc.invalidSchemes = schemes
return &rc
}
func (r *DeepLinkURL) WithMessage(message string) *DeepLinkURL {
rc := *r
rc.message = message
return &rc
}
func (r *DeepLinkURL) When(v WhenFunc) *DeepLinkURL {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *DeepLinkURL) when() WhenFunc {
return r.whenFunc
}
func (r *DeepLinkURL) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *DeepLinkURL) SkipOnEmpty() *DeepLinkURL {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *DeepLinkURL) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *DeepLinkURL) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *DeepLinkURL) SkipOnError() *DeepLinkURL {
rs := *r
rs.skipError = true
return &rs
}
func (r *DeepLinkURL) shouldSkipOnError() bool {
return r.skipError
}
func (r *DeepLinkURL) setSkipOnError(v bool) {
r.skipError = v
}
func (r *DeepLinkURL) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
// make sure the length is limited to avoid DOS attacks
if !ok || len(v) >= 2000 {
return NewResult().WithError(NewValidationError(r.message))
}
uri, err := url.Parse(v)
if err != nil {
return NewResult().WithError(NewValidationError(r.message))
}
if len(uri.Scheme) == 0 || (len(uri.Host) == 0 && len(uri.Opaque) == 0) {
return NewResult().WithError(NewValidationError(r.message))
}
if slices.Contains(r.invalidSchemes, uri.Scheme) {
return NewResult().WithError(NewValidationError(r.message))
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"github.com/gofrs/uuid"
)
type UUIDVersion byte
const (
UUIDVersionV1 UUIDVersion = 1
UUIDVersionV3 UUIDVersion = 3
UUIDVersionV4 UUIDVersion = 4
UUIDVersionV5 UUIDVersion = 5
UUIDVersionV6 UUIDVersion = 6
UUIDVersionV7 UUIDVersion = 7
)
type UUID struct {
message string
invalidVersionMessage string
version UUIDVersion
whenFunc WhenFunc
skipEmpty bool
skipError bool
}
func NewUUID() *UUID {
return &UUID{
message: MessageInvalidUUID,
invalidVersionMessage: MessageInvalidUUIDVersion,
}
}
func (r *UUID) WithMessage(message string) *UUID {
rc := *r
rc.message = message
return &rc
}
func (r *UUID) WithInvalidVersionMessage(message string) *UUID {
rc := *r
rc.invalidVersionMessage = message
return &rc
}
func (r *UUID) WithVersion(version UUIDVersion) *UUID {
rc := *r
rc.version = version
return &rc
}
func (r *UUID) When(v WhenFunc) *UUID {
rc := *r
rc.whenFunc = v
return &rc
}
func (r *UUID) when() WhenFunc {
return r.whenFunc
}
func (r *UUID) setWhen(v WhenFunc) {
r.whenFunc = v
}
func (r *UUID) SkipOnEmpty() *UUID {
rc := *r
rc.skipEmpty = true
return &rc
}
func (r *UUID) skipOnEmpty() bool {
return r.skipEmpty
}
func (r *UUID) setSkipOnEmpty(v bool) {
r.skipEmpty = v
}
func (r *UUID) SkipOnError() *UUID {
rs := *r
rs.skipError = true
return &rs
}
func (r *UUID) shouldSkipOnError() bool {
return r.skipError
}
func (r *UUID) setSkipOnError(v bool) {
r.skipError = v
}
func (r *UUID) ValidateValue(_ context.Context, value any) error {
v, ok := toString(value)
if !ok {
return NewResult().WithError(NewValidationError(r.message))
}
parsedUUID, err := uuid.FromString(v)
if err != nil {
return NewResult().WithError(NewValidationError(r.message))
}
if parsedUUID.IsNil() {
return NewResult().WithError(NewValidationError(r.message))
}
if r.version > 0 && byte(r.version) != parsedUUID.Version() {
return NewResult().
WithError(
NewValidationError(r.message).
WithParams(map[string]any{
"version": r.version,
}),
)
}
return nil
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package validator
import (
"context"
"errors"
"reflect"
"sort"
"github.com/raoptimus/validator.go/v2/set"
)
func ValidateValue(ctx context.Context, value any, rules ...Rule) error {
if len(rules) == 0 {
return nil
}
if extDS, ok := ExtractDataSet[DataSet](ctx); ok && value == extDS {
dataSet, err := normalizeDataSet(value)
if err != nil {
return err
}
ctx = withDataSet(ctx, dataSet)
} else {
ctx = withDataSet(ctx, extDS)
}
rules = normalizeRules(rules)
result := NewResult()
for _, r := range rules {
if isSkipValidate(ctx, value, r) {
continue
}
if err := r.ValidateValue(ctx, value); err != nil {
var errRes Result
if errors.As(err, &errRes) {
result = result.WithError(errRes.Errors()...)
continue
}
// why? only if rule is required
return err
}
}
if result.IsValid() {
return nil
}
for _, err := range result.Errors() {
err.Message = DefaultTranslator.Translate(ctx, err.Message, err.Params)
}
return result
}
func Validate(ctx context.Context, dataSet any, rules RuleSet) error {
normalizedDS, err := normalizeDataSet(dataSet) // 2 allocs
if err != nil {
return err
}
ctx = contextWithRootDataSet(ctx, dataSet)
ctx = withDataSet(ctx, normalizedDS)
results := make([]Result, 0, len(rules))
fields := make([]string, 0, len(rules))
for field := range rules {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
fieldRules := rules[field]
fieldValue, err := normalizedDS.FieldValue(field) // 2 allocs
if err != nil {
return err
}
aliasFieldName := normalizedDS.FieldAliasName(field)
result := NewResult()
fieldRules = normalizeRules(fieldRules)
for _, validatorRule := range fieldRules {
if isSkipValidate(ctx, fieldValue, validatorRule) {
continue
}
if err := validatorRule.ValidateValue(ctx, fieldValue); err != nil {
ctx = withPreviousRulesErrored(ctx)
var errRes Result
if errors.As(err, &errRes) {
for _, rErr := range errRes.Errors() {
if aliasFieldName != "" {
valuePath := make([]string, 0, len(rErr.ValuePath)+1)
valuePath = append(valuePath, aliasFieldName)
valuePath = append(valuePath, rErr.ValuePath...)
rErr.ValuePath = valuePath
}
result = result.WithError(rErr)
}
} else {
return err
}
if _, ok := validatorRule.(*Required); ok {
break
}
}
}
results = append(results, result)
}
summaryResult := NewResult()
for i := range results {
errs := (&results[i]).Errors()
for _, err := range errs {
err.Message = DefaultTranslator.Translate(ctx, err.Message, err.Params)
summaryResult = summaryResult.WithError(err)
// summaryResult = summaryResult.WithError(
// NewValidationError(DefaultTranslator.Translate(ctx, err.Message, err.Params)).
// WithParams(err.Params).
// WithValuePath(err.ValuePath),
// )
}
}
if !summaryResult.IsValid() {
return summaryResult
}
return nil
}
func normalizeDataSet(ds any) (DataSet, error) { //nolint:ireturn // DataSet is internal interface
if ds == nil {
return set.NewDataSetAny(ds), nil
}
rt := reflect.TypeOf(ds)
if rt.Kind() == reflect.Pointer {
rt = rt.Elem()
}
switch rt.Kind() {
case reflect.Struct:
if v, ok := ds.(DataSet); ok {
return v, nil
}
return set.NewDataSetStruct(ds)
case reflect.Map:
if v, ok := ds.(map[string]any); ok {
return set.NewDataSetMap(v), nil
}
}
return set.NewDataSetAny(ds), nil
}
func normalizeRules(rules []Rule) []Rule {
if len(rules) <= 1 {
return rules
}
for i := range rules {
r, ok := rules[i].(*Required)
if !ok {
continue
}
if i == 0 {
break
}
ret := make([]Rule, 0, len(rules))
ret = append(ret, r)
ret = append(ret, rules[:i]...)
ret = append(ret, rules[i:]...)
return ret
}
return rules
}
func isSkipValidate(ctx context.Context, value any, r Rule) bool {
if rse, ok := r.(RuleSkipEmpty); ok {
if rse.skipOnEmpty() && valueIsEmpty(reflect.ValueOf(value)) {
return true
}
}
if rser, ok := r.(RuleSkipError); ok {
if rser.shouldSkipOnError() && previousRulesErrored(ctx) {
return true
}
}
if rw, ok := r.(RuleWhen); ok {
return rw.when() != nil && !rw.when()(ctx, value)
}
return false
}
/**
* This file is part of the raoptimus/validator.go library
*
* @copyright Copyright (c) Evgeniy Urvantsev
* @license https://github.com/raoptimus/validator.go/blob/master/LICENSE.md
* @link https://github.com/raoptimus/validator.go
*/
package vtype
import (
"strings"
"time"
)
func NewTime(unvalidatedTime string) Time {
return Time{
validatedTime: &time.Time{},
unvalidatedTime: unvalidatedTime,
}
}
type Time struct {
validatedTime *time.Time
unvalidatedTime string
}
func (t *Time) Time() *time.Time {
if t.validatedTime == nil {
t.validatedTime = &time.Time{}
}
return t.validatedTime
}
func (t *Time) String() string {
return t.unvalidatedTime
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (t *Time) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" {
return nil
}
t.unvalidatedTime = strings.Trim(string(data), "\"")
if t.validatedTime == nil {
t.validatedTime = &time.Time{}
}
return nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (t *Time) UnmarshalText(data []byte) error {
t.unvalidatedTime = string(data)
if t.validatedTime == nil {
t.validatedTime = &time.Time{}
}
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (t *Time) MarshalJSON() ([]byte, error) {
data := []byte("\"" + t.unvalidatedTime + "\"")
return data, nil
}
// MarshalText implements the encoding.TextMarshaler interface.
func (t *Time) MarshalText() ([]byte, error) {
data := []byte("\"" + t.unvalidatedTime + "\"")
return data, nil
}