Преглед на файлове

add all validator, simplify error messages

Aneurin Barker Snook преди 1 година
родител
ревизия
c74486d122
променени са 8 файла, в които са добавени 98 реда и са изтрити 19 реда
  1. 14 0
      all.go
  2. 46 0
      all_test.go
  3. 7 2
      chars.go
  4. 6 1
      email.go
  5. 9 4
      in.go
  6. 2 2
      in_test.go
  7. 8 9
      length.go
  8. 6 1
      uuid.go

+ 14 - 0
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
+	}
+}

+ 46 - 0
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)
+			}
+		}
+	}
+}

+ 7 - 2
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

+ 6 - 1
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

+ 9 - 4
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

+ 2 - 2
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 {

+ 8 - 9
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
 	}

+ 6 - 1
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