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.
92 lines
2.3 KiB
Go
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)
|
|
}
|