From c74486d12275730f6517ed2553acc05183159c59 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Sat, 7 Oct 2023 13:42:17 +0100 Subject: [PATCH] add all validator, simplify error messages --- all.go | 14 ++++++++++++++ all_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ chars.go | 9 +++++++-- email.go | 7 ++++++- in.go | 13 +++++++++---- in_test.go | 4 ++-- length.go | 17 ++++++++--------- uuid.go | 7 ++++++- 8 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 all.go create mode 100644 all_test.go diff --git a/all.go b/all.go new file mode 100644 index 0000000..ef1e4fa --- /dev/null +++ b/all.go @@ -0,0 +1,14 @@ +package validate + +// All validates a value using a sequence of validation functions. +// If any validation function returns an error, the sequence stops and the error is returned. +func All[T any](fs ...func(T) error) func(T) error { + return func(value T) error { + for _, f := range fs { + if err := f(value); err != nil { + return err + } + } + return nil + } +} diff --git a/all_test.go b/all_test.go new file mode 100644 index 0000000..1ba9133 --- /dev/null +++ b/all_test.go @@ -0,0 +1,46 @@ +package validate + +import "testing" + +func TestAll(t *testing.T) { + type TestCase[T any] struct { + Input T + F func(T) error + Err error + } + + f := All( + MinLength(4), + MaxLength(8), + Chars("0123456789abcdef"), + In("abcd", "abcdef", "12345678"), + ) + + testCases := []TestCase[string]{ + {Input: "abcd", F: f}, + {Input: "abcdef", F: f}, + {Input: "12345678", F: f}, + {Input: "abc", F: f, Err: ErrTooFewChars}, + {Input: "abcdef012", F: f, Err: ErrTooManyChars}, + {Input: "abcdefgh", F: f, Err: ErrDisallowedChars}, + {Input: "01abcd", F: f, Err: ErrValueNotAllowed}, + } + + for _, tc := range testCases { + t.Logf("%q", tc.Input) + + err := tc.F(tc.Input) + if tc.Err != nil { + if err == nil { + t.Errorf("Expected %s; got nil", tc.Err) + } + if err != tc.Err { + t.Errorf("Expected %s; got %s", tc.Err, err) + } + } else { + if err != nil { + t.Errorf("Expected nil; got %s", err) + } + } + } +} diff --git a/chars.go b/chars.go index 83f0086..db77bfb 100644 --- a/chars.go +++ b/chars.go @@ -1,17 +1,22 @@ package validate import ( - "fmt" + "errors" "strings" ) +// Validation error. +var ( + ErrDisallowedChars = errors.New("contains disallowed characters") +) + // Chars validates whether a string contains only allowed characters. func Chars(allow string) func(string) error { return func(value string) error { rs := []rune(value) for _, r := range rs { if !strings.ContainsRune(allow, r) { - return fmt.Errorf("Contains disallowed characters") + return ErrDisallowedChars } } return nil diff --git a/email.go b/email.go index 2365d62..63715fa 100644 --- a/email.go +++ b/email.go @@ -5,13 +5,18 @@ import ( "regexp" ) +// Validation error. +var ( + ErrInvalidEmail = errors.New("invalid email address") +) + // Based on https://stackoverflow.com/a/201378 var emailRegexp = regexp.MustCompile("^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])$") // Email validates an email address. func Email(value string) error { if !emailRegexp.MatchString(value) { - return errors.New("Invalid email address") + return ErrInvalidEmail } return nil diff --git a/in.go b/in.go index 4583c40..8f8dfdd 100644 --- a/in.go +++ b/in.go @@ -2,24 +2,29 @@ package validate import "errors" +// Validation error. +var ( + ErrValueNotAllowed = errors.New("not allowed") +) + // In validates whether a value is found in a slice of allowed values. -func In[T comparable](allow []T) func(T) error { +func In[T comparable](allow ...T) func(T) error { return func(value T) error { for _, cmp := range allow { if cmp == value { return nil } } - return errors.New("Not an allowed value") + return ErrValueNotAllowed } } // NotIn validates whether a value is not found in a slice of disallowed values. -func NotIn[T comparable](allow []T) func(T) error { +func NotIn[T comparable](allow ...T) func(T) error { return func(value T) error { for _, cmp := range allow { if cmp == value { - return errors.New("Not an allowed value") + return ErrValueNotAllowed } } return nil diff --git a/in_test.go b/in_test.go index 229bc1f..be6c56f 100644 --- a/in_test.go +++ b/in_test.go @@ -24,7 +24,7 @@ func TestIn(t *testing.T) { for _, tc := range strTestCases { t.Logf("%q in %s", tc.Input, strings.Join(tc.S, ", ")) - f := In(tc.S) + f := In(tc.S...) err := f(tc.Input) if tc.Err { @@ -53,7 +53,7 @@ func TestIn(t *testing.T) { } t.Logf("%d in %s", tc.Input, strings.Join(intf, ", ")) - f := In(tc.S) + f := In(tc.S...) err := f(tc.Input) if tc.Err { diff --git a/length.go b/length.go index 3036fce..a07c329 100644 --- a/length.go +++ b/length.go @@ -2,17 +2,19 @@ package validate import ( "errors" - "fmt" +) + +// Validation error. +var ( + ErrTooFewChars = errors.New("too few characters") + ErrTooManyChars = errors.New("too many characters") ) // MaxLength validates the length of a string as being less than or equal to a given maximum. func MaxLength(l int) func(string) error { return func(value string) error { if len(value) > l { - if l != 1 { - return fmt.Errorf("Must not be longer than %d characters", l) - } - return errors.New("Must not be longer than 1 character") + return ErrTooManyChars } return nil } @@ -22,10 +24,7 @@ func MaxLength(l int) func(string) error { func MinLength(l int) func(string) error { return func(value string) error { if len(value) < l { - if l != 1 { - return fmt.Errorf("Must not be shorter than %d characters", l) - } - return errors.New("Must not be shorter than 1 character") + return ErrTooFewChars } return nil } diff --git a/uuid.go b/uuid.go index d6915e6..27381ae 100644 --- a/uuid.go +++ b/uuid.go @@ -5,13 +5,18 @@ import ( "regexp" ) +// Validation error. +var ( + ErrInvalidUUID = errors.New("invalid UUID") +) + var uuidRegexp = regexp.MustCompile("^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$") // UUID validates a UUID string. // The UUID must be formatted with separators. func UUID(value string) error { if !uuidRegexp.MatchString(value) { - return errors.New("Invalid UUID") + return ErrInvalidUUID } return nil