Files
valid/valid.go
Rene Nochebuena ab11fd2ace feat(valid)!: promote to v1.0.0 — JSON tag name resolution, bump xerrors to v1.0.0
Resolve JSON tag name resolution roadmap item: field names in error context
now use the json struct tag when available, falling back to the Go field name.
Commits MessageProvider interface as stable. Bumps xerrors dependency from
v0.9.0 to v1.0.0. API committed as stable.
2026-05-11 18:18:05 -06:00

92 lines
2.3 KiB
Go

package valid
import (
"errors"
"reflect"
"strings"
"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.
// Field names in error context use the json struct tag when available,
// falling back to the Go field name.
func New(opts ...Option) Validator {
cfg := &config{mp: DefaultMessages}
for _, o := range opts {
o(cfg)
}
v := playground.New()
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "" || name == "-" {
return fld.Name
}
return name
})
return &validator{
v: v,
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)
}