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/
This commit is contained in:
27
docs/adr/ADR-001-playground-validator-backend.md
Normal file
27
docs/adr/ADR-001-playground-validator-backend.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# ADR-001: go-playground/validator as Hidden Backend
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Struct validation via struct tags is a well-understood pattern in Go. `github.com/go-playground/validator/v10` is the de-facto standard library for this, supporting a large rule set (`required`, `email`, `min`, `max`, `url`, `uuid`, and hundreds more) and nested struct traversal.
|
||||
|
||||
However, directly using the playground `*validate.Validate` type throughout application code creates a hard dependency: its error types (`ValidationErrors`, `InvalidValidationError`) must be imported wherever errors are inspected, and its configuration API leaks into all call sites.
|
||||
|
||||
## Decision
|
||||
|
||||
`github.com/go-playground/validator/v10` is used as the sole validation backend but is not exposed in the public API.
|
||||
|
||||
The public API is the `Validator` interface, which has one method: `Struct(v any) error`. The concrete type `validator` (unexported) holds a `*playground.Validate` instance. The playground package is imported under the alias `playground` to make it unambiguous at a glance which types originate from it.
|
||||
|
||||
Callers never see `playground.ValidationErrors` or `*playground.InvalidValidationError` directly. The concrete error types are translated to `*xerrors.Err` inside `Struct()` before being returned.
|
||||
|
||||
The playground `*Validate` instance is created once inside `New()` with default options. No custom validators, tag name functions, or struct-level validators are registered. This keeps the API surface small and the behaviour predictable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive**: The playground library can be upgraded or replaced without changing any call-site code — only the `valid` package internals change.
|
||||
- **Positive**: Application code only needs to import `code.nochebuena.dev/go/valid` and `code.nochebuena.dev/go/xerrors`; no direct dependency on `go-playground/validator`.
|
||||
- **Negative**: Advanced playground features (custom validators, `RegisterTagNameFunc`, cross-field validation) are not accessible without extending the `valid` package. If a project needs them, it should add `Option` functions or methods to `Validator`.
|
||||
- **Note**: The playground alias `playground "github.com/go-playground/validator/v10"` is retained in the source as a readability aid, not a requirement. It prevents confusion with the package name `valid` used in the surrounding code.
|
||||
39
docs/adr/ADR-002-xerrors-integration.md
Normal file
39
docs/adr/ADR-002-xerrors-integration.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# ADR-002: xerrors Integration for Validation Errors
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Validation failures need to be distinguishable from other error categories (not found, internal, already exists) so that HTTP handlers can map them to the correct status codes without inspecting error messages. The `xerrors` module provides a typed error system with stable `Code` values for this purpose.
|
||||
|
||||
The `go-playground/validator` library returns two distinct error types for failures:
|
||||
1. `*playground.InvalidValidationError` — the argument passed to `Struct()` is not a struct (programmer error).
|
||||
2. `playground.ValidationErrors` — one or more field constraints failed (expected user input error).
|
||||
|
||||
These two cases must be mapped to different xerrors codes: the first is an internal bug, the second is a bad-input condition.
|
||||
|
||||
## Decision
|
||||
|
||||
`Struct(v any) error` applies the following mapping:
|
||||
|
||||
| playground error type | xerrors code | rationale |
|
||||
|------------------------------|-------------------|----------------------------------------------|
|
||||
| `*InvalidValidationError` | `ErrInternal` | Passing a non-struct is a programming error |
|
||||
| `ValidationErrors` (first) | `ErrInvalidInput` | Field constraint failures are user errors |
|
||||
| any other non-nil error | `ErrInternal` | Unexpected; treated as internal |
|
||||
|
||||
Only the **first** `ValidationErrors` entry is surfaced. The full `playground.ValidationErrors` slice is attached as the wrapped error via `WithError(err)`, so callers who need all failures can use `errors.Unwrap` to reach `playground.ValidationErrors` and iterate themselves.
|
||||
|
||||
The `*xerrors.Err` for `ErrInvalidInput` includes two context fields set via `WithContext`:
|
||||
- `"field"`: the struct field name (e.g. `"Email"`)
|
||||
- `"tag"`: the failing rule (e.g. `"email"`, `"required"`)
|
||||
|
||||
These allow structured logging and API response builders to attach field-level detail without parsing the error message string.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive**: HTTP middleware can use `errors.As(err, &xe)` and switch on `xe.Code()` to produce `400 Bad Request` for `ErrInvalidInput` without any knowledge of the validation library.
|
||||
- **Positive**: Structured context fields (`field`, `tag`) are available for logging and response serialization without string parsing.
|
||||
- **Positive**: The full `ValidationErrors` slice is recoverable for callers that must report all failing fields (e.g., form validation responses).
|
||||
- **Negative**: Only the first error is surfaced by default. Callers expecting a list of all failures in the top-level error must unwrap manually. This is a deliberate trade-off: the common case (return the first problem to the user) is easy; the less common case (return all problems) is possible but requires extra code.
|
||||
38
docs/adr/ADR-003-message-provider-i18n.md
Normal file
38
docs/adr/ADR-003-message-provider-i18n.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# ADR-003: MessageProvider Pattern for i18n
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Validation error messages shown to end users must be human-readable. Applications targeting different locales need messages in different languages. Hardcoding English messages inside the `Validator` implementation would make internationalization impossible without forking the package.
|
||||
|
||||
At the same time, the majority of applications use a single language throughout their lifetime. Requiring every caller to configure a message provider would be boilerplate-heavy for the common case.
|
||||
|
||||
## Decision
|
||||
|
||||
A `MessageProvider` interface is defined:
|
||||
|
||||
```go
|
||||
type MessageProvider interface {
|
||||
Message(field, tag, param string) string
|
||||
}
|
||||
```
|
||||
|
||||
`Message` receives the failing field name, the rule tag, and the rule parameter (e.g., `"5"` for `min=5`, or `""` if none), and returns a human-readable string.
|
||||
|
||||
Two built-in implementations are provided as package-level variables:
|
||||
- `DefaultMessages` — English, used automatically when no option is passed to `New()`.
|
||||
- `SpanishMessages` — Spanish, available as an opt-in via `WithMessageProvider(valid.SpanishMessages)`.
|
||||
|
||||
Custom implementations are supported by passing any value that satisfies `MessageProvider` to `WithMessageProvider`.
|
||||
|
||||
The `New()` constructor defaults to `DefaultMessages` and applies options via a `config` struct, following the functional options pattern. This means zero boilerplate for the common (English) case and a single option call for overrides.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive**: English is the zero-configuration default — `valid.New()` requires no arguments.
|
||||
- **Positive**: Spanish is available without any external dependency — just `valid.SpanishMessages`.
|
||||
- **Positive**: Applications can supply their own `MessageProvider` for any other language or for message formats that include the failing value, link to docs, etc.
|
||||
- **Negative**: The built-in providers handle only four tags (`required`, `email`, `min`, `max`) explicitly; all others fall through to a generic fallback message. Applications using many custom tags should supply a custom provider.
|
||||
- **Note**: Message formatting uses the struct field name as returned by `go-playground/validator` (the Go field name, e.g. `"Email"`), not a JSON tag. If user-facing messages must show the JSON key name, a custom `MessageProvider` combined with a registered tag name function on the playground validator would be needed.
|
||||
Reference in New Issue
Block a user