Files
valid/valid.go
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

80 lines
2.0 KiB
Go

package valid
import (
"errors"
"code.nochebuena.dev/go/xerrors"
playground "github.com/go-playground/validator/v10"
)
// Validator validates structs using struct tags.
type Validator interface {
// Struct validates v and returns a *xerrors.Err if validation fails.
// Returns nil if v is valid.
// Returns ErrInvalidInput for field constraint failures (first error only).
// Returns ErrInternal if v is not a struct.
Struct(v any) error
}
// Option configures a Validator.
type Option func(*config)
// config holds constructor configuration.
type config struct {
mp MessageProvider
}
// WithMessageProvider sets a custom MessageProvider.
// Default: DefaultMessages (English).
func WithMessageProvider(mp MessageProvider) Option {
return func(c *config) {
c.mp = mp
}
}
// New returns a Validator. Without options, DefaultMessages (English) is used.
func New(opts ...Option) Validator {
cfg := &config{mp: DefaultMessages}
for _, o := range opts {
o(cfg)
}
return &validator{
v: playground.New(),
mp: cfg.mp,
}
}
// validator is the concrete implementation of Validator.
type validator struct {
v *playground.Validate
mp MessageProvider
}
// Struct implements Validator.
//
// Only the first validation error is surfaced. Apps needing all failures can
// cast errors.Unwrap(err) to validator.ValidationErrors themselves.
func (val *validator) Struct(v any) error {
err := val.v.Struct(v)
if err == nil {
return nil
}
var invalidErr *playground.InvalidValidationError
if errors.As(err, &invalidErr) {
return xerrors.Wrap(xerrors.ErrInternal, "internal validation error", err)
}
var validationErrs playground.ValidationErrors
if errors.As(err, &validationErrs) {
first := validationErrs[0]
msg := val.mp.Message(first.Field(), first.Tag(), first.Param())
return xerrors.New(xerrors.ErrInvalidInput, msg).
WithContext("field", first.Field()).
WithContext("tag", first.Tag()).
WithError(err)
}
return xerrors.Wrap(xerrors.ErrInternal, "internal validation error", err)
}