From db902e3c1cbec640bfb0b01be2da61118b3589ba Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Sun, 19 Nov 2023 20:27:33 +0000 Subject: [PATCH] add validation error type, fix tests, add number tests --- all_test.go | 7 ++- chars.go | 6 +- chars_test.go | 7 ++- email_test.go | 7 ++- equal_test.go | 9 ++- error.go | 50 +++++++++++++++ error_test.go | 49 +++++++++++++++ in_test.go | 5 +- length_test.go | 11 ++-- number.go | 45 ++++++++----- number_test.go | 168 +++++++++++++++++++++++++++++++++++++++++++++++++ size_test.go | 9 ++- uuid_test.go | 7 ++- 13 files changed, 340 insertions(+), 40 deletions(-) create mode 100644 error.go create mode 100644 error_test.go create mode 100644 number_test.go diff --git a/all_test.go b/all_test.go index ef84ef4..b87f880 100644 --- a/all_test.go +++ b/all_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestAll(t *testing.T) { type TestCase[T any] struct { @@ -31,7 +34,7 @@ func TestAll(t *testing.T) { err := tc.F(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/chars.go b/chars.go index db77bfb..967a4cf 100644 --- a/chars.go +++ b/chars.go @@ -1,20 +1,18 @@ package validate import ( - "errors" "strings" ) // Validation error. var ( - ErrDisallowedChars = errors.New("contains disallowed characters") + ErrDisallowedChars = NewError("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 { + for _, r := range value { if !strings.ContainsRune(allow, r) { return ErrDisallowedChars } diff --git a/chars_test.go b/chars_test.go index 09556e6..23ccd3f 100644 --- a/chars_test.go +++ b/chars_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestChars(t *testing.T) { type TestCase struct { @@ -22,7 +25,7 @@ func TestChars(t *testing.T) { f := Chars(tc.C) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/email_test.go b/email_test.go index 9378418..02b5358 100644 --- a/email_test.go +++ b/email_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestEmail(t *testing.T) { type TestCase struct { @@ -18,7 +21,7 @@ func TestEmail(t *testing.T) { err := Email(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/equal_test.go b/equal_test.go index 6da691a..29134ee 100644 --- a/equal_test.go +++ b/equal_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestEqualInt(t *testing.T) { type TestCase struct { @@ -21,7 +24,7 @@ func TestEqualInt(t *testing.T) { f := Equal(tc.C) err := f(tc.I) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } @@ -46,7 +49,7 @@ func TestEqualStr(t *testing.T) { f := Equal(tc.C) err := f(tc.I) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/error.go b/error.go new file mode 100644 index 0000000..45265f5 --- /dev/null +++ b/error.go @@ -0,0 +1,50 @@ +package validate + +import "fmt" + +// Validation error. +var ( + Err = Error{} +) + +// Error represents a validation error. +type Error struct { + Message string + Data []any +} + +// Error retrieves the message of a validation Error. +// If it has Data, the message will be formatted. +func (e Error) Error() string { + if len(e.Data) > 0 { + return fmt.Sprintf(e.Message, e.Data...) + } + return e.Message +} + +// Is determines whether the Error is an instance of the target. +// https://pkg.go.dev/errors#Is +// +// If the target is a validation error and specifies a message, this function returns true if the messages match. +// If the target is an empty validation error, this function always returns true. +func (e Error) Is(target error) bool { + if t, ok := target.(Error); ok { + return t.Message == e.Message || t.Message == "" + } + return false +} + +func (e Error) With(value any) Error { + if e.Data == nil { + e.Data = []any{} + } + e.Data = append(e.Data, value) + return e +} + +// NewError creates a new validation error. +func NewError(message string) Error { + return Error{ + Message: message, + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..8a1428c --- /dev/null +++ b/error_test.go @@ -0,0 +1,49 @@ +package validate + +import ( + "errors" + "testing" +) + +func TestErrorIs(t *testing.T) { + type TestCase struct { + Err error + Target error + Is bool + } + + testCases := []TestCase{ + // Is any validation error + {Err: Err, Target: Err, Is: true}, + {Err: ErrDisallowedChars, Target: Err, Is: true}, + {Err: ErrMustBeGreater, Target: Err, Is: true}, + + // Is specific validation error + {Err: ErrDisallowedChars, Target: ErrDisallowedChars, Is: true}, + {Err: ErrMustBeGreater, Target: ErrMustBeGreater, Is: true}, + + // Is not specific validation error + {Err: Err, Target: ErrDisallowedChars}, + {Err: Err, Target: ErrMustBeGreater}, + {Err: ErrMustBeGreater, Target: ErrDisallowedChars}, + {Err: ErrDisallowedChars, Target: ErrMustBeGreater}, + + // Is not any other error + {Err: ErrDisallowedChars, Target: errors.New("contains disallowed characters")}, + {Err: ErrMustBeGreater, Target: errors.New("must be greater than %v")}, + } + + for i, tc := range testCases { + t.Logf("(%d) Testing %v against %v", i, tc.Err, tc.Target) + + if errors.Is(tc.Err, tc.Target) { + if !tc.Is { + t.Errorf("%v should not equal %v", tc.Err, tc.Target) + } + } else { + if tc.Is { + t.Errorf("%v should equal %v", tc.Err, tc.Target) + } + } + } +} diff --git a/in_test.go b/in_test.go index c8bde32..14065b6 100644 --- a/in_test.go +++ b/in_test.go @@ -1,6 +1,7 @@ package validate import ( + "errors" "testing" ) @@ -25,7 +26,7 @@ func TestInInt(t *testing.T) { f := In(tc.A...) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } @@ -52,7 +53,7 @@ func TestInString(t *testing.T) { f := In(tc.A...) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/length_test.go b/length_test.go index 0d16f14..027ca1c 100644 --- a/length_test.go +++ b/length_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestMaxLength(t *testing.T) { type TestCase struct { @@ -22,7 +25,7 @@ func TestMaxLength(t *testing.T) { f := MaxLength(tc.L) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } @@ -30,8 +33,8 @@ func TestMaxLength(t *testing.T) { func TestMinLength(t *testing.T) { type TestCase struct { - L int Input string + L int Err error } @@ -48,7 +51,7 @@ func TestMinLength(t *testing.T) { f := MinLength(tc.L) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/number.go b/number.go index 9df668a..6d16321 100644 --- a/number.go +++ b/number.go @@ -1,91 +1,104 @@ package validate -import "errors" - +// Validation error. var ( - ErrTooHigh = errors.New("too high") - ErrTooLow = errors.New("too low") + ErrMustBeGreater = NewError("must be greater than %v") + ErrMustBeGreaterOrEqual = NewError("must be greater than or equal to %v") + ErrMustBeLess = NewError("must be less than %v") + ErrMustBeLessOrEqual = NewError("must be less than or equal to %v") ) +// Max validates whether an integer is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func Max(n int, exclusive bool) func(int) error { return func(value int) error { if exclusive { if value >= n { - return ErrTooHigh + return ErrMustBeLess.With(n) } } if value > n { - return ErrTooHigh + return ErrMustBeLessOrEqual.With(n) } return nil } } +// MaxFloat32 validates whether a float32 is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func MaxFloat32(n float32, exclusive bool) func(float32) error { return func(value float32) error { if exclusive { if value >= n { - return ErrTooHigh + return ErrMustBeLess.With(n) } } if value > n { - return ErrTooHigh + return ErrMustBeLessOrEqual.With(n) } return nil } } +// MaxFloat64 validates whether a float64 is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func MaxFloat64(n float64, exclusive bool) func(float64) error { return func(value float64) error { if exclusive { if value >= n { - return ErrTooHigh + return ErrMustBeLess.With(n) } } if value > n { - return ErrTooHigh + return ErrMustBeLessOrEqual.With(n) } return nil } } +// Min validates whether an integer is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func Min(n int, exclusive bool) func(int) error { return func(value int) error { if exclusive { if value <= n { - return ErrTooHigh + return ErrMustBeGreater.With(n) } } if value < n { - return ErrTooHigh + return ErrMustBeGreaterOrEqual.With(n) } return nil } } +// MinFloat32 validates whether a float32 is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func MinFloat32(n float32, exclusive bool) func(float32) error { return func(value float32) error { if exclusive { if value <= n { - return ErrTooHigh + return ErrMustBeGreater.With(n) } } if value < n { - return ErrTooHigh + return ErrMustBeGreaterOrEqual.With(n) } return nil } } +// MinFloat64 validates whether a float64 is less than or equal to a given maximum. +// If exclusive is true, an equal value will also produce an error. func MinFloat64(n float64, exclusive bool) func(float64) error { return func(value float64) error { if exclusive { if value <= n { - return ErrTooHigh + return ErrMustBeGreater.With(n) } } if value < n { - return ErrTooHigh + return ErrMustBeGreaterOrEqual.With(n) } return nil } diff --git a/number_test.go b/number_test.go new file mode 100644 index 0000000..712ae62 --- /dev/null +++ b/number_test.go @@ -0,0 +1,168 @@ +package validate + +import ( + "errors" + "testing" +) + +func TestMax(t *testing.T) { + type TestCase struct { + Input int + N int + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0, Err: ErrMustBeLessOrEqual}, + {Input: 10, N: 10}, + {Input: 10, N: 15}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeLess}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %d against maximum of %d", n, tc.Input, tc.N) + + f := Max(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} + +func TestMaxFloat32(t *testing.T) { + type TestCase struct { + Input float32 + N float32 + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0, Err: ErrMustBeLessOrEqual}, + {Input: 10, N: 10}, + {Input: 10, N: 15}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeLess}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %g against maximum of %g", n, tc.Input, tc.N) + + f := MaxFloat32(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} + +func TestMaxFloat64(t *testing.T) { + type TestCase struct { + Input float64 + N float64 + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0, Err: ErrMustBeLessOrEqual}, + {Input: 10, N: 10}, + {Input: 10, N: 15}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeLess}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %g against maximum of %g", n, tc.Input, tc.N) + + f := MaxFloat64(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} + +func TestMin(t *testing.T) { + type TestCase struct { + Input int + N int + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0}, + {Input: 10, N: 10}, + {Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %d against minimum of %d", n, tc.Input, tc.N) + + f := Min(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} + +func TestMinFloat32(t *testing.T) { + type TestCase struct { + Input float32 + N float32 + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0}, + {Input: 10, N: 10}, + {Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %g against minimum of %g", n, tc.Input, tc.N) + + f := MinFloat32(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} + +func TestMinFloat64(t *testing.T) { + type TestCase struct { + Input float64 + N float64 + Excl bool + Err error + } + + testCases := []TestCase{ + {Input: 10, N: 0}, + {Input: 10, N: 10}, + {Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual}, + {Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater}, + } + + for n, tc := range testCases { + t.Logf("(%d) Testing %g against minimum of %g", n, tc.Input, tc.N) + + f := MinFloat64(tc.N, tc.Excl) + err := f(tc.Input) + + if !errors.Is(err, tc.Err) { + t.Errorf("Expected error %v, got %v", tc.Err, err) + } + } +} diff --git a/size_test.go b/size_test.go index 98467ba..cba80a3 100644 --- a/size_test.go +++ b/size_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestMaxSize(t *testing.T) { type TestCase struct { @@ -21,7 +24,7 @@ func TestMaxSize(t *testing.T) { f := MaxSize[int](tc.L) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } @@ -46,7 +49,7 @@ func TestMinSize(t *testing.T) { f := MinSize[int](tc.L) err := f(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } } diff --git a/uuid_test.go b/uuid_test.go index f46af12..7acfc62 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -1,6 +1,9 @@ package validate -import "testing" +import ( + "errors" + "testing" +) func TestUUID(t *testing.T) { type TestCase struct { @@ -23,7 +26,7 @@ func TestUUID(t *testing.T) { err := UUID(tc.Input) - if err != tc.Err { + if !errors.Is(err, tc.Err) { t.Errorf("Expected error %v, got %v", tc.Err, err) } }