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:
143
README.md
Normal file
143
README.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# `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
|
||||
Reference in New Issue
Block a user