diff --git a/match.go b/match.go new file mode 100644 index 0000000..4a8ce28 --- /dev/null +++ b/match.go @@ -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 + } +} diff --git a/match_test.go b/match_test.go new file mode 100644 index 0000000..626fbdc --- /dev/null +++ b/match_test.go @@ -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) + } + }) + } +} diff --git a/required.go b/required.go new file mode 100644 index 0000000..7077584 --- /dev/null +++ b/required.go @@ -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 + } +} diff --git a/required_test.go b/required_test.go new file mode 100644 index 0000000..32738a9 --- /dev/null +++ b/required_test.go @@ -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) + } + }) +} diff --git a/string.go b/string.go new file mode 100644 index 0000000..115f74a --- /dev/null +++ b/string.go @@ -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 + } +} diff --git a/string_test.go b/string_test.go new file mode 100644 index 0000000..55a2853 --- /dev/null +++ b/string_test.go @@ -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) + } + }) + } +}