From 345560efbf9d833d3324b62b252d2e4de04dfd44 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Tue, 27 Jun 2023 18:34:36 +0530 Subject: [PATCH 1/7] Add initial functions --- README.md | 2 +- go.mod | 11 +++ go.sum | 10 +++ validation.go | 87 +++++++++++++++++++++++ validation_test.go | 167 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 validation.go create mode 100644 validation_test.go diff --git a/README.md b/README.md index b1d2892..ec1fda1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # cli-validation -Library to help with flag validation of urfave/cli +Library to help with flag validation of urfave/cli(v3 only) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3617726 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/dearchap/cli-validation + +go 1.19 + +require github.com/stretchr/testify v1.8.4 + +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.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/validation.go b/validation.go new file mode 100644 index 0000000..0d3e774 --- /dev/null +++ b/validation.go @@ -0,0 +1,87 @@ +package validation + +import ( + "fmt" +) + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +type Integer interface { + Signed | Unsigned +} + +type Float interface { + ~float32 | ~float64 +} + +type Ordered interface { + Integer | Float +} + +// ConditionOrError ir a helper function to make writing +// validation functions much easier +func ConditionOrError(cond bool, err error) error { + if cond { + return nil + } + return err +} + +// ValidationChainAll allows one to chain a sequence of validation +// functions to construct a single validation function. All the +// individual validations must pass for the validation to succeed +func ValidationChainAll[T any](fns ...func(T) error) func(T) error { + return func(v T) error { + for _, fn := range fns { + if err := fn(v); err != nil { + return err + } + } + return nil + } +} + +// ValidationChainAny allows one to chain a sequence of validation +// functions to construct a single validation function. Atleast one +// of the individual validations must pass for the validation to succeed +func ValidationChainAny[T any](fns ...func(T) error) func(T) error { + return func(v T) error { + var errs []error + for _, fn := range fns { + if err := fn(v); err == nil { + return nil + } else { + errs = append(errs, err) + } + } + return fmt.Errorf("%+v", errs) + } +} + +// Min means that the value to be checked needs to be atleast(and including) +// the checked value +func Min[T Ordered](c T) func(T) error { + return func(v T) error { + return ConditionOrError(v >= c, fmt.Errorf("%v is not less than %v", v, c)) + } +} + +// Max means that the value to be checked needs to be atmost(and including) +// the checked value +func Max[T Ordered](c T) func(T) error { + return func(v T) error { + return ConditionOrError(v <= c, fmt.Errorf("%v is not greater than %v", v, c)) + } +} + +// Max means that the value to be checked needs to be atmost(and including) +// the checked value +func RangeInclusive[T Ordered](a, b T) func(T) error { + return ValidationChainAll[T](Min[T](a), Max[T](b)) +} diff --git a/validation_test.go b/validation_test.go new file mode 100644 index 0000000..a4871c8 --- /dev/null +++ b/validation_test.go @@ -0,0 +1,167 @@ +package validation + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type testcase[T any] struct { + name string + f func(T) error + input T + errExpected bool +} + +func test_with_input[T any](t *testing.T, tcases []testcase[T]) { + r := require.New(t) + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + err := tc.f(tc.input) + if tc.errExpected { + r.Error(err) + } else { + r.NoError(err) + } + }) + } +} + +func TestMin(t *testing.T) { + + inputs := []testcase[int]{ + { + name: "min lower limit", + f: Min[int](10), + input: 10, + }, + { + name: "min normal", + f: Min[int](10), + input: 11, + }, + { + name: "min failure", + f: Min[int](10), + input: 8, + errExpected: true, + }, + } + + test_with_input[int](t, inputs) +} + +func TestMax(t *testing.T) { + + inputs := []testcase[float64]{ + { + name: "max upper limit", + f: Max[float64](10.0), + input: 10.0, + }, + { + name: "max normal", + f: Max[float64](10.0), + input: 9.0, + }, + { + name: "max failure", + f: Max[float64](10.0), + input: 10.00001, + errExpected: true, + }, + } + + test_with_input[float64](t, inputs) +} + +func TestRange(t *testing.T) { + + inputs := []testcase[uint64]{ + { + name: "lower limit", + f: RangeInclusive[uint64](10, 16), + input: 10, + }, + { + name: "upper limit", + f: RangeInclusive[uint64](10, 16), + input: 16, + }, + { + name: "lower limit failure", + f: RangeInclusive[uint64](10, 16), + input: 9, + errExpected: true, + }, + { + name: "upper limit failure", + f: RangeInclusive[uint64](10, 16), + input: 17, + errExpected: true, + }, + { + name: "normal", + f: RangeInclusive[uint64](10, 16), + input: 12, + }, + } + + test_with_input[uint64](t, inputs) +} + +func TestDisjointRange(t *testing.T) { + + inputs := []testcase[int16]{ + { + name: "lower limit lower range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 10, + }, + { + name: "upper limit lower range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 16, + }, + { + name: "lower limit upper range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 56, + }, + { + name: "upper limit upper range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 67, + }, + { + name: "normal lower range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 13, + }, + { + name: "normal upper range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 60, + }, + { + name: "failure below lower range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 1, + errExpected: true, + }, + { + name: "failure in between lower and upper range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 20, + errExpected: true, + }, + { + name: "failure above upper range", + f: ValidationChainAny[int16](RangeInclusive[int16](10, 16), RangeInclusive[int16](56, 67)), + input: 70, + errExpected: true, + }, + } + + test_with_input[int16](t, inputs) +} From dbdd7a190f6f3a3459ca4b89b70ee934b5886cc5 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Tue, 27 Jun 2023 18:41:37 +0530 Subject: [PATCH 2/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec1fda1..8cbeb12 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # cli-validation -Library to help with flag validation of urfave/cli(v3 only) +Library to help with flag validation of urfave/cli(current for v3 only). This ensures that urfave/cli codebase remains compact and clean of optional features From 980148b0db2de17d9c531312b51ae12f5eec3e2c Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Tue, 27 Jun 2023 18:46:27 +0530 Subject: [PATCH 3/7] Code review updates --- go.mod | 2 +- validation_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3617726..d0213a6 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/dearchap/cli-validation +module github.com/urfave/cli-validation go 1.19 diff --git a/validation_test.go b/validation_test.go index a4871c8..55a7fbf 100644 --- a/validation_test.go +++ b/validation_test.go @@ -13,7 +13,7 @@ type testcase[T any] struct { errExpected bool } -func test_with_input[T any](t *testing.T, tcases []testcase[T]) { +func testWithInput[T any](t *testing.T, tcases []testcase[T]) { r := require.New(t) for _, tc := range tcases { t.Run(tc.name, func(t *testing.T) { @@ -48,7 +48,7 @@ func TestMin(t *testing.T) { }, } - test_with_input[int](t, inputs) + testWithInput[int](t, inputs) } func TestMax(t *testing.T) { @@ -72,7 +72,7 @@ func TestMax(t *testing.T) { }, } - test_with_input[float64](t, inputs) + testWithInput[float64](t, inputs) } func TestRange(t *testing.T) { @@ -107,7 +107,7 @@ func TestRange(t *testing.T) { }, } - test_with_input[uint64](t, inputs) + testWithInput[uint64](t, inputs) } func TestDisjointRange(t *testing.T) { @@ -163,5 +163,5 @@ func TestDisjointRange(t *testing.T) { }, } - test_with_input[int16](t, inputs) + testWithInput[int16](t, inputs) } From a08ef713328d4838781a3b229eab4450773908c8 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Tue, 27 Jun 2023 21:08:20 +0530 Subject: [PATCH 4/7] Update readme and remove blank lines --- README.md | 2 +- validation_test.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 8cbeb12..1e21ffd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # cli-validation -Library to help with flag validation of urfave/cli(current for v3 only). This ensures that urfave/cli codebase remains compact and clean of optional features +Library which contains help validators for use with the flag validation feature of urfave/cli(current for v3 only). Validation in urfave/cli is run only after the value has been converted from a string to the flag value type. So these validators need to focus only on validating the value, whether it be within a certain range or picked from a fixed number of values. For slice/map flags validation cannot be used to ensure that the underlying structure is of a certain length(s) since the final slice/map len can be known only after all the flags have been parsed. That kind of validation is a candidate for a Flag Action since those actions are run after all flags have been parsed \ No newline at end of file diff --git a/validation_test.go b/validation_test.go index 55a7fbf..6a80539 100644 --- a/validation_test.go +++ b/validation_test.go @@ -26,7 +26,6 @@ func testWithInput[T any](t *testing.T, tcases []testcase[T]) { }) } } - func TestMin(t *testing.T) { inputs := []testcase[int]{ @@ -50,7 +49,6 @@ func TestMin(t *testing.T) { testWithInput[int](t, inputs) } - func TestMax(t *testing.T) { inputs := []testcase[float64]{ @@ -74,7 +72,6 @@ func TestMax(t *testing.T) { testWithInput[float64](t, inputs) } - func TestRange(t *testing.T) { inputs := []testcase[uint64]{ @@ -109,7 +106,6 @@ func TestRange(t *testing.T) { testWithInput[uint64](t, inputs) } - func TestDisjointRange(t *testing.T) { inputs := []testcase[int16]{ From a08fc5d8f3227d010b82c1c0553859c8db58d780 Mon Sep 17 00:00:00 2001 From: dearchap Date: Wed, 28 Jun 2023 00:00:38 +0530 Subject: [PATCH 5/7] Update README.md Co-authored-by: Anatoli Babenia --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e21ffd..730d6cd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # cli-validation -Library which contains help validators for use with the flag validation feature of urfave/cli(current for v3 only). Validation in urfave/cli is run only after the value has been converted from a string to the flag value type. So these validators need to focus only on validating the value, whether it be within a certain range or picked from a fixed number of values. For slice/map flags validation cannot be used to ensure that the underlying structure is of a certain length(s) since the final slice/map len can be known only after all the flags have been parsed. That kind of validation is a candidate for a Flag Action since those actions are run after all flags have been parsed \ No newline at end of file +Library with additional validators for flag validation feature of `urfave/cli` (current for v3 only). Validation in `urfave/cli` is run only after the value has been converted from a string to the flag value type. So these validators need to focus only on validating the value, whether it be within a certain range or picked from a fixed number of values. Validation cannot be used to check slice/map length, because len can be known only after all the flags have been parsed. That kind of validation is a candidate for a Flag Action since those actions are run after all flags have been parsed \ No newline at end of file From 36ab288a33e69fd72ba71a87acae6d2ee5f0ae1c Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Wed, 28 Jun 2023 00:01:03 +0530 Subject: [PATCH 6/7] Added Enum and Regex validators --- validation.go | 25 +++++++++++++++++++++ validation_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/validation.go b/validation.go index 0d3e774..ec5228e 100644 --- a/validation.go +++ b/validation.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "regexp" ) type Signed interface { @@ -85,3 +86,27 @@ func Max[T Ordered](c T) func(T) error { func RangeInclusive[T Ordered](a, b T) func(T) error { return ValidationChainAll[T](Min[T](a), Max[T](b)) } + +// Enum lets the given value be checked against a given set of values +func Enum[T comparable](values ...T) func(T) error { + return func(v T) error { + for _, value := range values { + if value == v { + return nil + } + } + return fmt.Errorf("%v not in %+v", v, values) + } +} + +// Regex allows for pattern matching on string value fields +func Regex[T ~string](pattern string) func(T) error { + return func(v T) error { + if r, err := regexp.Compile(pattern); err != nil { + return err + } else if !r.Match([]byte(v)) { + return fmt.Errorf("%v is not of pattern %s", v, pattern) + } + return nil + } +} diff --git a/validation_test.go b/validation_test.go index 6a80539..3f17d5d 100644 --- a/validation_test.go +++ b/validation_test.go @@ -161,3 +161,57 @@ func TestDisjointRange(t *testing.T) { testWithInput[int16](t, inputs) } +func TestEnum(t *testing.T) { + + inputs := []testcase[string]{ + { + name: "valid value for enum - 1", + f: Enum[string]("hello", "bar", "foo"), + input: "hello", + }, + { + name: "valid value for enum - 2", + f: Enum[string]("hello", "bar", "foo"), + input: "bar", + }, + { + name: "invalid value for enum", + f: Enum[string]("hello", "bar", "foo"), + input: "helloee", + errExpected: true, + }, + } + + testWithInput[string](t, inputs) +} + +func TestRegex(t *testing.T) { + type myString string + + inputs := []testcase[myString]{ + { + name: "valid value regex match - 1", + f: Regex[myString]("foo[1-7].*y"), + input: "foo1y", + }, + { + name: "valid value regex match - 2", + f: Regex[myString]("foo[1-7].*y"), + input: "foo1ttthsyy", + }, + { + name: "invalid value regex match - 1", + f: Regex[myString]("foo[1-7].*y"), + input: "fooy", + errExpected: true, + }, + { + name: "invalid value regex match - 2", + f: Regex[myString]("foo[1-7].*y"), + input: "fooOy", + errExpected: true, + }, + } + + testWithInput[myString](t, inputs) +} From 0ed7f78b6ba4e0e2db5bbb56c971dd2cb2475b8b Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Wed, 28 Jun 2023 00:12:42 +0530 Subject: [PATCH 7/7] Added Slice validator --- validation.go | 12 ++++++++++++ validation_test.go | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/validation.go b/validation.go index ec5228e..5cde359 100644 --- a/validation.go +++ b/validation.go @@ -110,3 +110,15 @@ func Regex[T ~string](pattern string) func(T) error { return nil } } + +// SliceValidator allows using a simple type validator with a slice +func SliceValidator[T any](f func(T) error) func([]T) error { + return func(values []T) error { + for i, v := range values { + if err := f(v); err != nil { + return fmt.Errorf("value at slice[%d] : %w", i, err) + } + } + return nil + } +} diff --git a/validation_test.go b/validation_test.go index 3f17d5d..eb51af1 100644 --- a/validation_test.go +++ b/validation_test.go @@ -215,3 +215,22 @@ func TestRegex(t *testing.T) { testWithInput[myString](t, inputs) } + +func TestSliceValidator(t *testing.T) { + + inputs := []testcase[[]uint32]{ + { + name: "valid values slices", + f: SliceValidator[uint32](Min[uint32](7)), + input: []uint32{9, 10, 18, 14}, + }, + { + name: "invalid value slice - 1", + f: SliceValidator[uint32](Min[uint32](7)), + input: []uint32{9, 10, 6, 14}, + errExpected: true, + }, + } + + testWithInput[[]uint32](t, inputs) +}