From 4483105a7818664913ecef048197c7b3f59a93ed Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 7 Sep 2026 14:01:03 +0100 Subject: [PATCH] docs: positioning README, package doc, usage examples 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 --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++-- doc.go | 41 +++++++++++++++++++++++++ example_test.go | 66 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 doc.go create mode 100644 example_test.go diff --git a/README.md b/README.md index f4070e8..b3cd0c3 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..3834495 --- /dev/null +++ b/doc.go @@ -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 diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..acee601 --- /dev/null +++ b/example_test.go @@ -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: + // + // 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 +}