diff --git a/example_test.go b/example_test.go index f5b8758..1e180c5 100644 --- a/example_test.go +++ b/example_test.go @@ -659,6 +659,41 @@ func ExampleAtLeastOneOf() { // violation at 'brands': This value should not be blank. } +func ExampleAll() { + book := struct { + Name string + Tags []string + }{ + Name: "Very long book name", + Tags: []string{"Fiction", "Thriller", "Science", "Fantasy"}, + } + + err := validator.Validate( + context.Background(), + validation.Sequentially( + // this block passes + validation.All( + validation.StringProperty("name", book.Name, it.IsNotBlank()), + validation.CountableProperty("tags", len(book.Tags), it.IsNotBlank()), + ), + // this block fails + validation.All( + validation.StringProperty("name", book.Name, it.HasMaxLength(10)), + validation.CountableProperty("tags", len(book.Tags), it.HasMaxCount(3)), + ), + ), + ) + + if violations, ok := validation.UnwrapViolationList(err); ok { + for violation := violations.First(); violation != nil; violation = violation.Next() { + fmt.Println(violation) + } + } + // Output: + // violation at 'name': This value is too long. It should have 10 characters or less. + // violation at 'tags': This collection should contain 3 elements or less. +} + func ExampleValidator_Validate_basicValidation() { s := "" diff --git a/flow_control.go b/flow_control.go index 3ca0f9d..9dc57f1 100644 --- a/flow_control.go +++ b/flow_control.go @@ -194,3 +194,48 @@ func (arg AtLeastOneOfArgument) setUp(ctx *executionContext) { return violations, nil }) } + +// AllArgument can be used to interrupt validation process when the first violation is raised. +type AllArgument struct { + isIgnored bool + options []Option + arguments []Argument +} + +// All runs validation for each argument. It works exactly as validator.Validate method. +// It can be helpful to build complex validation process. +func All(arguments ...Argument) AllArgument { + return AllArgument{arguments: arguments} +} + +// With returns a copy of AllArgument with appended options. +func (arg AllArgument) With(options ...Option) AllArgument { + arg.options = append(arg.options, options...) + return arg +} + +// When enables conditional validation of this argument. If the expression evaluates to false, +// then the argument will be ignored. +func (arg AllArgument) When(condition bool) AllArgument { + arg.isIgnored = !condition + return arg +} + +func (arg AllArgument) setUp(ctx *executionContext) { + ctx.addValidator(arg.options, func(scope Scope) (*ViolationList, error) { + if arg.isIgnored { + return nil, nil + } + + violations := &ViolationList{} + + for _, argument := range arg.arguments { + err := violations.AppendFromError(scope.Validate(argument)) + if err != nil { + return nil, err + } + } + + return violations, nil + }) +} diff --git a/test/flow_control_test.go b/test/flow_control_test.go index 3592a7b..f15e722 100644 --- a/test/flow_control_test.go +++ b/test/flow_control_test.go @@ -212,3 +212,45 @@ func TestAtLeastOneOfArgument_WhenValidationIsDisabled_ExpectNoError(t *testing. assert.NoError(t, err) } + +func TestAllArgument_WhenInvalidValueAtFirstConstraint_ExpectAllViolations(t *testing.T) { + err := newValidator(t).Validate( + context.Background(), + validation.All( + validation.String("", it.IsNotBlank().Code("first")), + validation.String("", it.IsNotBlank().Code("second")), + ), + ) + + validationtest.Assert(t, err).IsViolationList().WithCodes("first", "second") +} + +func TestAllArgument_WhenPathIsSet_ExpectOneViolationWithPath(t *testing.T) { + err := newValidator(t).Validate( + context.Background(), + validation.All( + validation.String("", it.IsNotBlank().Code("first")), + validation.String("", it.IsNotBlank().Code("second")), + ).With( + validation.PropertyName("properties"), + validation.ArrayIndex(0), + validation.PropertyName("property"), + ), + ) + + violations := validationtest.Assert(t, err).IsViolationList() + violations.HasViolationAt(0).WithCode("first").WithPropertyPath("properties[0].property") + violations.HasViolationAt(1).WithCode("second").WithPropertyPath("properties[0].property") +} + +func TestAllArgument_WhenValidationIsDisabled_ExpectNoErrors(t *testing.T) { + err := newValidator(t).Validate( + context.Background(), + validation.All( + validation.String("", it.IsNotBlank().Code("first")), + validation.String("", it.IsNotBlank().Code("second")), + ).When(false), + ) + + assert.NoError(t, err) +}