package valid import ( "errors" "reflect" "strings" "code.nochebuena.dev/einherjar/core/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 } var _ Validator = (*validator)(nil) // 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 }) for _, cv := range cfg.customValidators { if err := v.RegisterValidation(cv.tag, func(fl playground.FieldLevel) bool { return cv.fn(fieldLevelAdapter{fl}) }); err != nil { panic("valid: WithCustomValidator tag " + cv.tag + ": " + err.Error()) } } return &validator{v: v, mp: cfg.mp} } type fieldLevelAdapter struct{ fl playground.FieldLevel } func (a fieldLevelAdapter) Field() reflect.Value { return a.fl.Field() } func (a fieldLevelAdapter) Param() string { return a.fl.Param() } func (a fieldLevelAdapter) FieldName() string { return a.fl.FieldName() } 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 playground.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) }