Closes #3. - doc.go: package overview -- what a validator is, All vs Collect, errors are sentinels, how this compares to validator/v10 (tags+reflection), ozzo-validation (Rule interface) and govalidator (IsX helpers), and when to pick something else. - README.md: rewritten from three lines to a positioning intro, a "why this and not X" table, a building-blocks table, and a "not a fit if" note. - example_test.go: runnable Example (compose built-ins + a closure), Example_collectAll (errors.Is over a joined error), Example_errorsIs (sentinel matching, Err as match-anything). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,85 @@
|
||||
# Go Validate
|
||||
|
||||
A suite of straightforward validation functions. You put something in, you get back `nil` or an error.
|
||||
Small, composable value validators for Go. A validator is just a
|
||||
`func(T) error` — it returns `nil` or an error. You compose them with
|
||||
`All` or `Collect` and drop in plain closures wherever you need something
|
||||
custom. No struct tags, no reflection, no dependencies outside the
|
||||
standard library.
|
||||
|
||||
```go
|
||||
username := validate.All(
|
||||
validate.MinLength(3),
|
||||
validate.MaxLength(16),
|
||||
validate.Chars("abcdefghijklmnopqrstuvwxyz0123456789_"),
|
||||
func(s string) error {
|
||||
if strings.HasPrefix(s, "_") {
|
||||
return errors.New("must not start with an underscore")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err := username(input); err != nil {
|
||||
// errors.Is(err, validate.ErrDisallowedChars) still works
|
||||
}
|
||||
```
|
||||
|
||||
## Why this and not one of the established packages
|
||||
|
||||
| Package | Style |
|
||||
|---|---|
|
||||
| [`go-playground/validator`](https://github.com/go-playground/validator) | struct tags (`validate:"required,email"`) driven by reflection |
|
||||
| [`go-ozzo/ozzo-validation`](https://github.com/go-ozzo/ozzo-validation) | rules composed in code, through a `Rule` interface over `interface{}` |
|
||||
| [`asaskevich/govalidator`](https://github.com/asaskevich/govalidator) | a bag of `IsEmail` / `IsURL` string helpers |
|
||||
|
||||
This package is the code-composition style (closest to ozzo-validation)
|
||||
with two constraints held on purpose:
|
||||
|
||||
- **Validators are ordinary generic functions.** `In`, `Equal` and the
|
||||
rest are type-checked by the compiler — pass the wrong type and it does
|
||||
not build. The tag/reflection libraries can't do this; they predate
|
||||
generics.
|
||||
- **Errors are plain sentinels.** `errors.Is(err, ErrInvalidEmail)`
|
||||
composes with the standard `errors` package. There is no bespoke
|
||||
`ValidationErrors` type to learn. `Err` matches any error from the
|
||||
package.
|
||||
|
||||
Everything is value-level: there is no struct walker. You wire fields
|
||||
together yourself (a few lines) and decide how to present the result.
|
||||
|
||||
## Fail fast or collect everything
|
||||
|
||||
```go
|
||||
// stops at the first failure
|
||||
validate.All(rules...)
|
||||
|
||||
// runs every rule, joins the failures with errors.Join;
|
||||
// errors.Is still matches each one
|
||||
validate.Collect(rules...)
|
||||
```
|
||||
|
||||
## Building blocks
|
||||
|
||||
| Group | Functions |
|
||||
|---|---|
|
||||
| Compose | `All`, `Collect` |
|
||||
| Presence / equality | `Required`, `Equal`, `In`, `NotIn` |
|
||||
| String length & content | `MinLength`, `MaxLength`, `Chars`, `ExceptChars`, `Prefix`, `Suffix`, `Contains`, `Match` |
|
||||
| Formats | `Email`, `URL`, `UUID` |
|
||||
| Numbers | `Min`, `Max`, `MinFloat32`, `MaxFloat32`, `MinFloat64`, `MaxFloat64` |
|
||||
| Slices | `MinSize`, `MaxSize` |
|
||||
| Errors | `Error`, `NewError`, `Err` |
|
||||
|
||||
Each returns (or is) a `func(T) error`, so anything you write with the
|
||||
same shape composes with them.
|
||||
|
||||
## Not a fit if…
|
||||
|
||||
You want **struct-tag validation**, **translated / i18n messages**, or a
|
||||
**large catalogue** of built-in checks (credit cards, ISO codes, CIDRs, …).
|
||||
Use [`go-playground/validator`](https://github.com/go-playground/validator)
|
||||
for that. This package deliberately stays small.
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE.md](./LICENSE.md)
|
||||
MIT. See [LICENSE.md](./LICENSE.md).
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Package validate provides small, composable value validators.
|
||||
//
|
||||
// A validator is just a func(T) error: it returns nil when the value is
|
||||
// acceptable, or an error describing the problem. Nothing has to implement
|
||||
// an interface, so a plain closure works anywhere a validator is expected.
|
||||
// Combine validators with [All] (stop at the first failure) or [Collect]
|
||||
// (run them all and join every failure).
|
||||
//
|
||||
// username := validate.All(
|
||||
// validate.MinLength(3),
|
||||
// validate.MaxLength(16),
|
||||
// validate.Chars("abcdefghijklmnopqrstuvwxyz0123456789_"),
|
||||
// )
|
||||
// if err := username(input); err != nil {
|
||||
// // errors.Is(err, validate.ErrDisallowedChars) still works here
|
||||
// }
|
||||
//
|
||||
// Errors are plain sentinel values (see [Error]); match them with
|
||||
// errors.Is. [Err] matches any error produced by this package.
|
||||
//
|
||||
// # Comparison with other packages
|
||||
//
|
||||
// - go-playground/validator drives validation from struct tags and
|
||||
// reflection.
|
||||
// - go-ozzo/ozzo-validation composes rules in code, but through a Rule
|
||||
// interface over interface{}.
|
||||
// - asaskevich/govalidator is a bag of IsX string helpers.
|
||||
//
|
||||
// This package is the code-composition style with two constraints kept
|
||||
// deliberately: validators are ordinary generic functions, checked by the
|
||||
// compiler with no reflection ([In], [Equal] and the rest will not accept
|
||||
// the wrong type), and there are no dependencies outside the standard
|
||||
// library.
|
||||
//
|
||||
// # When to use something else
|
||||
//
|
||||
// Reach for go-playground/validator if you want struct-tag validation,
|
||||
// translated messages, or a large catalogue of built-in checks. This
|
||||
// package stays small and leaves struct/field wiring and error
|
||||
// presentation to the caller.
|
||||
package validate
|
||||
@@ -0,0 +1,66 @@
|
||||
package validate_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.aneur.in/go/validate"
|
||||
)
|
||||
|
||||
// A validator is any func(T) error. Compose the built-ins with your own
|
||||
// closures using All (stop at the first failure) or Collect (report all).
|
||||
func Example() {
|
||||
username := validate.All(
|
||||
validate.MinLength(3),
|
||||
validate.MaxLength(16),
|
||||
validate.Chars("abcdefghijklmnopqrstuvwxyz0123456789_"),
|
||||
func(s string) error {
|
||||
if strings.HasPrefix(s, "_") {
|
||||
return errors.New("must not start with an underscore")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
fmt.Println(username("ok_name"))
|
||||
fmt.Println(username("_nope"))
|
||||
fmt.Println(username("Nope"))
|
||||
// Output:
|
||||
// <nil>
|
||||
// must not start with an underscore
|
||||
// contains disallowed characters
|
||||
}
|
||||
|
||||
// Collect runs every validator and joins the failures; errors.Is still
|
||||
// matches each one.
|
||||
func Example_collectAll() {
|
||||
password := validate.Collect(
|
||||
validate.MinLength(8),
|
||||
validate.Chars("abcdefghijklmnopqrstuvwxyz"),
|
||||
)
|
||||
|
||||
err := password("Ab1")
|
||||
fmt.Println(err)
|
||||
fmt.Println(errors.Is(err, validate.ErrMustBeLonger))
|
||||
fmt.Println(errors.Is(err, validate.ErrDisallowedChars))
|
||||
// Output:
|
||||
// must contain at least 8 characters
|
||||
// contains disallowed characters
|
||||
// true
|
||||
// true
|
||||
}
|
||||
|
||||
// Errors are plain sentinels, so errors.Is works with no library-specific
|
||||
// machinery. Err matches any error from this package.
|
||||
func Example_errorsIs() {
|
||||
err := validate.Email("not-an-email")
|
||||
|
||||
fmt.Println(err)
|
||||
fmt.Println(errors.Is(err, validate.ErrInvalidEmail))
|
||||
fmt.Println(errors.Is(err, validate.Err))
|
||||
// Output:
|
||||
// invalid email address
|
||||
// true
|
||||
// true
|
||||
}
|
||||
Reference in New Issue
Block a user