Files
valid/CLAUDE.md
Rene Nochebuena 328b80c060 feat(valid): initial stable release v0.9.0
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/
2026-03-18 21:02:26 +00:00

79 lines
3.3 KiB
Markdown

# valid
Struct validation via go-playground/validator, returning xerrors-typed errors and supporting pluggable i18n messages.
## Purpose
Wraps `github.com/go-playground/validator/v10` behind a minimal `Validator` interface. Translates playground error types into `*xerrors.Err` values with stable `Code` values (`ErrInvalidInput`, `ErrInternal`), so HTTP middleware can map validation failures to HTTP status codes without knowing anything about the underlying library.
## Tier & Dependencies
**Tier 1** — depends on:
- `code.nochebuena.dev/go/xerrors` (Tier 0, error types)
- `github.com/go-playground/validator/v10` (external, hidden behind the interface)
## Key Design Decisions
- **Playground validator as hidden backend** (ADR-001): `*playground.Validate` is never exposed in the public API. Callers interact only with the `Validator` interface and `*xerrors.Err` errors.
- **xerrors integration** (ADR-002): `ValidationErrors``ErrInvalidInput`; `InvalidValidationError` (non-struct arg) → `ErrInternal`. Only the first failing field is surfaced; the full `playground.ValidationErrors` is attached via `WithError` for callers that need all failures.
- **MessageProvider for i18n** (ADR-003): Human-readable messages are delegated to a `MessageProvider` interface. `DefaultMessages` (English) is used automatically. `SpanishMessages` is opt-in. Custom providers are accepted via `WithMessageProvider`.
## Patterns
Default (English):
```go
v := valid.New()
if err := v.Struct(req); err != nil {
// err is *xerrors.Err with Code() == xerrors.ErrInvalidInput
}
```
Spanish messages:
```go
v := valid.New(valid.WithMessageProvider(valid.SpanishMessages))
```
Custom message provider:
```go
type myMessages struct{}
func (m myMessages) Message(field, tag, param string) string { ... }
v := valid.New(valid.WithMessageProvider(myMessages{}))
```
Accessing structured context from the error:
```go
var xe *xerrors.Err
if errors.As(err, &xe) {
field := xe.Fields()["field"] // e.g. "Email"
tag := xe.Fields()["tag"] // e.g. "email"
}
```
Accessing all validation errors:
```go
var xe *xerrors.Err
errors.As(err, &xe)
var ve playground.ValidationErrors
errors.As(errors.Unwrap(xe), &ve)
// iterate ve for all failing fields
```
## What to Avoid
- Do not expose `*playground.Validate` or `playground.ValidationErrors` from any new public function. Keep the backend hidden.
- Do not add generics to the `Validator` interface. The `Struct(v any) error` signature is intentionally non-generic; the playground library uses reflection internally.
- Do not register global custom validators on the shared `*playground.Validate` instance — that would break isolation between callers that share a process but expect different validation behaviour.
- Do not construct a new `*playground.Validate` per call; `New()` creates one instance per `Validator`.
## Testing Notes
- `valid_test.go` covers: `New()` defaults, custom `MessageProvider` injection, valid struct (nil error), `required`/`email`/`min`/`max` failures, non-struct input (`ErrInternal`), `Fields()` context keys, `Unwrap` to `playground.ValidationErrors`, Spanish messages, and all built-in message tags.
- `compliance_test.go` checks at compile time that `valid.New()` satisfies `Validator`, and that `DefaultMessages`/`SpanishMessages` satisfy `MessageProvider`.
- No network or database access — all tests are pure in-process.