add Required, Prefix, Suffix, Contains, Match validators
Broadens the built-in set with the string/presence checks that came up most often when reaching for this package: - Required[T comparable] non-zero value - Prefix / Suffix / Contains strings.HasPrefix/HasSuffix/Contains - Match(*regexp.Regexp) arbitrary pattern All follow the existing shape: a constructor returning func(T) error, a sentinel error matched with errors.Is, table-driven tests and an Example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoMatch = NewError("invalid format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match validates that a string matches a regular expression. Compile the
|
||||||
|
// expression once with regexp.MustCompile and reuse the returned validator.
|
||||||
|
func Match(re *regexp.Regexp) func(string) error {
|
||||||
|
return func(value string) error {
|
||||||
|
if !re.MatchString(value) {
|
||||||
|
return ErrNoMatch
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleMatch() {
|
||||||
|
slug := Match(regexp.MustCompile(`^[a-z0-9-]+$`))
|
||||||
|
fmt.Println(slug("Not A Slug"))
|
||||||
|
// Output: invalid format
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
v := Match(regexp.MustCompile(`^[a-z0-9-]+$`))
|
||||||
|
|
||||||
|
testCases := map[string]error{
|
||||||
|
"hello-world-123": nil,
|
||||||
|
"abc": nil,
|
||||||
|
|
||||||
|
"Hello": ErrNoMatch,
|
||||||
|
"has space": ErrNoMatch,
|
||||||
|
"": ErrNoMatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
for input, want := range testCases {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got := v(input); !errors.Is(got, want) {
|
||||||
|
t.Error("got", got, "want", want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRequired = NewError("is required")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Required validates that a value is not the zero value for its type: an
|
||||||
|
// empty string, 0, a nil pointer, and so on.
|
||||||
|
//
|
||||||
|
// It is defined for comparable types only. For slices and maps, use
|
||||||
|
// MinSize(1); for "non-whitespace", combine with a trimming step.
|
||||||
|
func Required[T comparable]() func(T) error {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
return func(value T) error {
|
||||||
|
if value == zero {
|
||||||
|
return ErrRequired
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleRequired() {
|
||||||
|
notBlank := Required[string]()
|
||||||
|
fmt.Println(notBlank(""))
|
||||||
|
// Output: is required
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequired(t *testing.T) {
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
v := Required[string]()
|
||||||
|
if err := v(""); !errors.Is(err, ErrRequired) {
|
||||||
|
t.Error("empty string should be required, got", err)
|
||||||
|
}
|
||||||
|
if err := v("x"); err != nil {
|
||||||
|
t.Error("non-empty string should pass, got", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int", func(t *testing.T) {
|
||||||
|
v := Required[int]()
|
||||||
|
if err := v(0); !errors.Is(err, ErrRequired) {
|
||||||
|
t.Error("zero should be required, got", err)
|
||||||
|
}
|
||||||
|
if err := v(1); err != nil {
|
||||||
|
t.Error("non-zero should pass, got", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pointer", func(t *testing.T) {
|
||||||
|
v := Required[*int]()
|
||||||
|
if err := v(nil); !errors.Is(err, ErrRequired) {
|
||||||
|
t.Error("nil pointer should be required, got", err)
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
if err := v(&n); err != nil {
|
||||||
|
t.Error("non-nil pointer should pass, got", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingPrefix = NewError("must start with %q")
|
||||||
|
ErrMissingSuffix = NewError("must end with %q")
|
||||||
|
ErrMissingSubstring = NewError("must contain %q")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prefix validates that a string begins with a given prefix.
|
||||||
|
func Prefix(prefix string) func(string) error {
|
||||||
|
return func(value string) error {
|
||||||
|
if !strings.HasPrefix(value, prefix) {
|
||||||
|
return ErrMissingPrefix.With(prefix)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix validates that a string ends with a given suffix.
|
||||||
|
func Suffix(suffix string) func(string) error {
|
||||||
|
return func(value string) error {
|
||||||
|
if !strings.HasSuffix(value, suffix) {
|
||||||
|
return ErrMissingSuffix.With(suffix)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains validates that a string contains a given substring.
|
||||||
|
func Contains(substr string) func(string) error {
|
||||||
|
return func(value string) error {
|
||||||
|
if !strings.Contains(value, substr) {
|
||||||
|
return ErrMissingSubstring.With(substr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExamplePrefix() {
|
||||||
|
fmt.Println(Prefix("https://")("http://example.com"))
|
||||||
|
// Output: must start with "https://"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrefix(t *testing.T) {
|
||||||
|
testCases := map[string]error{
|
||||||
|
"https://example.com": nil,
|
||||||
|
"https://": nil,
|
||||||
|
|
||||||
|
"http://example.com": ErrMissingPrefix.With("https://"),
|
||||||
|
"": ErrMissingPrefix.With("https://"),
|
||||||
|
}
|
||||||
|
|
||||||
|
v := Prefix("https://")
|
||||||
|
for input, want := range testCases {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got := v(input); !errors.Is(got, want) {
|
||||||
|
t.Error("got", got, "want", want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuffix(t *testing.T) {
|
||||||
|
testCases := map[string]error{
|
||||||
|
"report.pdf": nil,
|
||||||
|
".pdf": nil,
|
||||||
|
|
||||||
|
"report.txt": ErrMissingSuffix.With(".pdf"),
|
||||||
|
"pdf": ErrMissingSuffix.With(".pdf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
v := Suffix(".pdf")
|
||||||
|
for input, want := range testCases {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got := v(input); !errors.Is(got, want) {
|
||||||
|
t.Error("got", got, "want", want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
testCases := map[string]error{
|
||||||
|
"a b c": nil,
|
||||||
|
" ": nil,
|
||||||
|
|
||||||
|
"abc": ErrMissingSubstring.With(" "),
|
||||||
|
"": ErrMissingSubstring.With(" "),
|
||||||
|
}
|
||||||
|
|
||||||
|
v := Contains(" ")
|
||||||
|
for input, want := range testCases {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got := v(input); !errors.Is(got, want) {
|
||||||
|
t.Error("got", got, "want", want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user