From 5b900ee30d7c0b2ef71971185c76a12a4c299244 Mon Sep 17 00:00:00 2001 From: Joel Gilbert Date: Thu, 20 Jul 2023 14:50:47 -0400 Subject: [PATCH 1/2] FastFail feature #1042 --- validator.go | 5 ++ validator_instance.go | 12 +++++ validator_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/validator.go b/validator.go index 6f6d53ada..305102c50 100644 --- a/validator.go +++ b/validator.go @@ -27,6 +27,7 @@ type validate struct { fldIsPointer bool // StructLevel & FieldLevel isPartial bool hasExcludes bool + isFailFast bool // Returns on first error encountered } // parent and current will be the same the first run of validateStruct @@ -75,6 +76,10 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur } v.traverseField(ctx, current, current.Field(f.idx), ns, structNs, f, f.cTags) + + if v.isFailFast && len(v.errs) > 0 { + return + } } } diff --git a/validator_instance.go b/validator_instance.go index d9dbf0ce8..efde37be5 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -83,6 +83,7 @@ type Validate struct { pool *sync.Pool hasCustomFuncs bool hasTagNameFunc bool + isFailFast bool tagNameFunc TagNameFunc structLevelFuncs map[reflect.Type]StructLevelFuncCtx customFuncs map[reflect.Type]CustomTypeFunc @@ -149,6 +150,11 @@ func New() *Validate { return v } +// FailFast sets the error response to return the first validation error encountered +func (v *Validate) FailFast() { + v.isFailFast = true +} + // SetTagName allows for changing of the default tag name of 'validate' func (v *Validate) SetTagName(name string) { v.tagName = name @@ -384,6 +390,7 @@ func (v *Validate) StructCtx(ctx context.Context, s interface{}) (err error) { vd := v.pool.Get().(*validate) vd.top = top vd.isPartial = false + vd.isFailFast = v.isFailFast // vd.hasExcludes = false // only need to reset in StructPartial and StructExcept vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil) @@ -430,6 +437,7 @@ func (v *Validate) StructFilteredCtx(ctx context.Context, s interface{}, fn Filt vd.top = top vd.isPartial = true vd.ffn = fn + vd.isFailFast = v.isFailFast // vd.hasExcludes = false // only need to reset in StructPartial and StructExcept vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil) @@ -480,6 +488,7 @@ func (v *Validate) StructPartialCtx(ctx context.Context, s interface{}, fields . vd.ffn = nil vd.hasExcludes = false vd.includeExclude = make(map[string]struct{}) + vd.isFailFast = v.isFailFast typ := val.Type() name := typ.Name() @@ -570,6 +579,7 @@ func (v *Validate) StructExceptCtx(ctx context.Context, s interface{}, fields .. vd.ffn = nil vd.hasExcludes = true vd.includeExclude = make(map[string]struct{}) + vd.isFailFast = v.isFailFast typ := val.Type() name := typ.Name() @@ -641,6 +651,7 @@ func (v *Validate) VarCtx(ctx context.Context, field interface{}, tag string) (e vd := v.pool.Get().(*validate) vd.top = val vd.isPartial = false + vd.isFailFast = v.isFailFast vd.traverseField(ctx, val, val, vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) if len(vd.errs) > 0 { @@ -693,6 +704,7 @@ func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other vd := v.pool.Get().(*validate) vd.top = otherVal vd.isPartial = false + vd.isFailFast = v.isFailFast vd.traverseField(ctx, otherVal, reflect.ValueOf(field), vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) if len(vd.errs) > 0 { diff --git a/validator_test.go b/validator_test.go index 74f494511..0abac4571 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13135,3 +13135,119 @@ func TestCronExpressionValidation(t *testing.T) { } } } + +type Value struct { + Street string `validate:"required"` + City string `validate:"required"` +} + +func TestFailFastSettingStruct(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.Struct(t.v) + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} + +func TestFailFastSettingStructPartialCtx(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.StructPartialCtx(context.TODO(), t.v, "Street", "City") + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} + +func TestFailFastSettingStructExceptCtx(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.StructPartialCtx(context.TODO(), t.v, "Street", "City") + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} From b874becaecbe9a1032f43e2a64b306f55668e10f Mon Sep 17 00:00:00 2001 From: Joel Gilbert Date: Thu, 20 Feb 2025 21:04:12 -0500 Subject: [PATCH 2/2] fix missing brackets --- validator_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/validator_test.go b/validator_test.go index d2be3eaa9..19625c835 100644 --- a/validator_test.go +++ b/validator_test.go @@ -12080,7 +12080,7 @@ func TestExcludedIf(t *testing.T) { test11 := struct { Field1 bool - Field2 *string `validate:"excluded_if=Field1 false"` + Field2 *string `validate:"excluded_if=Field1 false"` }{ Field1: false, Field2: nil, @@ -14068,6 +14068,8 @@ func TestFailFastSettingStructExceptCtx(t *testing.T) { validationErrs := errs.(ValidationErrors) IsEqual(len(validationErrs), t.expectedErrLen) + } +} func TestTimeRequired(t *testing.T) { validate := New()