Files
valid/README.md

144 lines
4.0 KiB
Markdown
Raw Permalink Normal View History

# `valid`
> Struct validation backed by `go-playground/validator` with structured error output.
**Module:** `code.nochebuena.dev/go/valid`
**Tier:** 1 — depends on `xerrors` (Tier 0) and `go-playground/validator/v10`
**Go:** 1.25+
**Dependencies:** `code.nochebuena.dev/go/xerrors`, `github.com/go-playground/validator/v10`
---
## Overview
`valid` wraps [`go-playground/validator/v10`](https://github.com/go-playground/validator) and maps validation failures to structured [`*xerrors.Err`](../xerrors) values. It replaces the old `check` package, adding a plain constructor, a swappable message provider, and English messages by default.
## Installation
```sh
go get code.nochebuena.dev/go/valid
```
## Quick start
```go
import "code.nochebuena.dev/go/valid"
type CreateUserRequest struct {
Name string `validate:"required"`
Email string `validate:"required,email"`
Age int `validate:"min=18,max=120"`
}
v := valid.New()
err := v.Struct(req)
if err != nil {
// err is a *xerrors.Err with code ErrInvalidInput.
var xe *xerrors.Err
if errors.As(err, &xe) {
fmt.Println(xe.Code()) // INVALID_ARGUMENT
fmt.Println(xe.Message()) // "field 'Email' must be a valid email address"
fmt.Println(xe.Fields()) // map[field:Email tag:email]
}
}
```
## Usage
### Creating a validator
```go
// English messages (default).
v := valid.New()
// Spanish messages.
v := valid.New(valid.WithMessageProvider(valid.SpanishMessages))
// Custom message provider.
v := valid.New(valid.WithMessageProvider(myProvider))
```
### Validating structs
```go
err := v.Struct(myStruct)
```
| Outcome | Error returned |
|---------|---------------|
| All fields pass | `nil` |
| Field constraint failure | `*xerrors.Err` with `ErrInvalidInput`, first error only |
| Not a struct | `*xerrors.Err` with `ErrInternal` |
The returned `*xerrors.Err` for field failures carries:
- `Fields()["field"]` — the failing struct field name
- `Fields()["tag"]` — the failing validation rule (e.g. `"email"`, `"required"`)
- `Unwrap()` — the underlying `validator.ValidationErrors`
### Message providers
`MessageProvider` maps a validation failure to a human-readable message:
```go
type MessageProvider interface {
Message(field, tag, param string) string
}
```
Built-in presets:
| Variable | Language | Usage |
|----------|----------|-------|
| `DefaultMessages` | English | automatic (no option needed) |
| `SpanishMessages` | Spanish | `WithMessageProvider(valid.SpanishMessages)` |
Custom provider example:
```go
type myMessages struct{}
func (m myMessages) Message(field, tag, param string) string {
switch tag {
case "required":
return field + " is mandatory"
default:
return field + " failed: " + tag
}
}
v := valid.New(valid.WithMessageProvider(myMessages{}))
```
## Design decisions
**No singleton** — `valid.New(opts...)` returns a plain value. Multiple validators with different configurations can coexist. Tests create isolated instances without global state.
**Only the first validation error is surfaced** — `go-playground/validator` returns all field errors at once; we surface only the first for API simplicity. Apps needing all failures can cast `errors.Unwrap(err)` to `validator.ValidationErrors`:
```go
var xe *xerrors.Err
errors.As(err, &xe)
var ve validator.ValidationErrors
errors.As(errors.Unwrap(xe), &ve) // all field errors
```
**`valid` imports `xerrors` directly** — `valid` is Tier 1 and `xerrors` is Tier 0. The import is intentional; `valid` constructs `*xerrors.Err` values. Duck-typing is reserved for cases where the import would create a circular or cross-tier dependency.
**Spanish is bundled, not a separate module** — the Spanish preset is a small, zero-dep addition. Splitting it into a separate module would add publish overhead for negligible gain.
## Ecosystem
```
Tier 0: xerrors
↑ (direct import — constructs *xerrors.Err)
Tier 1: valid ← you are here
Tier 2: httputil (injects valid.Validator into generic handler)
```
## License
MIT