add all validator, simplify error messages

This commit is contained in:
Aneurin Barker Snook
2023-10-07 13:42:17 +01:00
parent c2efe16dc0
commit c74486d122
8 changed files with 98 additions and 19 deletions
+14
View File
@@ -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
}
}
+46
View File
@@ -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)
}
}
}
}
+7 -2
View File
@@ -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
+6 -1
View File
@@ -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
+9 -4
View File
@@ -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
+2 -2
View File
@@ -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 {
+8 -9
View File
@@ -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
}
+6 -1
View File
@@ -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