Struct validation backed by go-playground/validator/v10 with xerrors integration and pluggable i18n message providers. What's included: - Validator interface with Struct(v any) error method - New(...Option) constructor with WithMessageProvider functional option - MessageProvider interface for i18n; DefaultMessages (EN) and SpanishMessages (ES) built in - ValidationErrors mapped to xerrors.ErrInvalidInput with field and tag context keys - InvalidValidationError (non-struct input) mapped to xerrors.ErrInternal - Full playground.ValidationErrors attached via WithError for callers needing all failures Tested-via: todo-api POC integration Reviewed-against: docs/adr/
144 lines
4.0 KiB
Markdown
144 lines
4.0 KiB
Markdown
# `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
|