Skip to content

Commit

Permalink
feature: async argument
Browse files Browse the repository at this point in the history
  • Loading branch information
strider2038 committed Jun 22, 2022
1 parent a332144 commit 7dfe2c7
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ jobs:
version: v1.46

- name: Run tests
run: go test -v $(go list ./... | grep -v vendor)
run: go test -race -v $(go list ./... | grep -v vendor)
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ versions `n` 0.n.m may contain breaking changes. Patch versions `m` 0.n.m may co
Goals before making stable release:

* [x] implementation of static type arguments by generics;
* [ ] mechanism for asynchronous validation (lazy violations by async/await pattern);
* [x] mechanism for asynchronous validation (lazy violations by async/await pattern);
* [ ] implement all common constraints;
* [ ] stable production usage for at least 6 months.

## Installation
Expand Down
68 changes: 68 additions & 0 deletions flow_control.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package validation

import (
"context"
"sync"
)

// WhenArgument is used to build conditional validation. Use the When function to initiate a conditional check.
// If the condition is true, then the arguments passed through the Then function will be processed.
// Otherwise, the arguments passed through the Else function will be processed.
Expand Down Expand Up @@ -239,3 +244,66 @@ func (arg AllArgument) setUp(ctx *executionContext) {
return violations, nil
})
}

// AsyncArgument can be used to interrupt validation process when the first violation is raised.
type AsyncArgument struct {
isIgnored bool
options []Option
arguments []Argument
}

// Async implements async/await pattern and runs validation for each argument in a separate goroutine.
func Async(arguments ...Argument) AsyncArgument {
return AsyncArgument{arguments: arguments}
}

// With returns a copy of AsyncArgument with appended options.
func (arg AsyncArgument) With(options ...Option) AsyncArgument {
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 AsyncArgument) When(condition bool) AsyncArgument {
arg.isIgnored = !condition
return arg
}

func (arg AsyncArgument) setUp(executionCtx *executionContext) {
executionCtx.addValidator(arg.options, func(scope Scope) (*ViolationList, error) {
if arg.isIgnored {
return nil, nil
}

ctx, cancel := context.WithCancel(scope.context)
defer cancel()
scope = scope.withContext(ctx)

waiter := &sync.WaitGroup{}
waiter.Add(len(arg.arguments))
errs := make(chan error)
for _, argument := range arg.arguments {
go func(argument Argument) {
defer waiter.Done()
errs <- scope.Validate(argument)
}(argument)
}

go func() {
waiter.Wait()
close(errs)
}()

violations := &ViolationList{}

for violation := range errs {
err := violations.AppendFromError(violation)
if err != nil {
return nil, err
}
}

return violations, nil
})
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ go 1.18

require (
github.com/muonsoft/language v0.3.0
github.com/stretchr/testify v1.6.1
github.com/stretchr/testify v1.7.4
golang.org/x/text v0.3.6
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ github.com/muonsoft/language v0.3.0/go.mod h1:be1X9wxDjKiqqKfXU6eiUHxu4BrreJZx1G
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
69 changes: 69 additions & 0 deletions test/flow_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package test

import (
"context"
"fmt"
"testing"
"time"

"github.com/muonsoft/validation"
"github.com/muonsoft/validation/code"
Expand Down Expand Up @@ -254,3 +256,70 @@ func TestAllArgument_WhenValidationIsDisabled_ExpectNoErrors(t *testing.T) {

assert.NoError(t, err)
}

func TestAsyncArgument_WhenInvalidValueAtFirstConstraint_ExpectAllViolations(t *testing.T) {
err := newValidator(t).Validate(
context.Background(),
validation.Async(
validation.String("", it.IsNotBlank().Code("first")),
),
)

validationtest.Assert(t, err).IsViolationList().WithCodes("first")
}

func TestAsyncArgument_WhenPathIsSet_ExpectOneViolationWithPath(t *testing.T) {
err := newValidator(t).Validate(
context.Background(),
validation.Async(
validation.String("", it.IsNotBlank().Code("first")),
).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")
}

func TestAsyncArgument_WhenValidationIsDisabled_ExpectNoErrors(t *testing.T) {
err := newValidator(t).Validate(
context.Background(),
validation.Async(
validation.String("", it.IsNotBlank().Code("first")),
validation.String("", it.IsNotBlank().Code("second")),
).When(false),
)

assert.NoError(t, err)
}

func TestAsyncArgument_WhenFatalError_ExpectContextCanceled(t *testing.T) {
cancellation := make(chan bool, 1)
fatal := fmt.Errorf("fatal")

err := newValidator(t).Validate(
context.Background(),
validation.Async(
validation.String("", asyncConstraint(func(value *string, scope validation.Scope) error {
return fatal
})),
validation.String("", asyncConstraint(func(value *string, scope validation.Scope) error {
select {
case <-time.After(10 * time.Millisecond):
cancellation <- false
case <-scope.Context().Done():
cancellation <- true
}
return nil
})),
),
)

assert.ErrorIs(t, err, fatal)
if isCanceled, ok := <-cancellation; !isCanceled || !ok {
assert.Fail(t, "context is expected to be canceled")
}
}
6 changes: 6 additions & 0 deletions test/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,9 @@ type mockTranslator struct {
func (m mockTranslator) Translate(tag language.Tag, message string, pluralCount int) string {
return m.translate(tag, message, pluralCount)
}

type asyncConstraint func(value *string, scope validation.Scope) error

func (f asyncConstraint) ValidateString(value *string, scope validation.Scope) error {
return f(value, scope)
}

0 comments on commit 7dfe2c7

Please sign in to comment.