* feat: generate fields validation and use it when issuing requests to API with soft (default) or hard modes * chore: apply linter fixes * feat: enable field validation on int fields * feat: add validation for ^[\w]+$ fields * feat: add validation for MAC address fields * fix: trim wrappers for all comments * feat: add validation for IPv4, IPv6 and IP(IPv4/IPv6) fields * feat: add validation for numeric, non-zero based fields * fix: one of validation can contain dot (.) sign in values * feat: add second notation of MAC address validation * fix: one of validation can start with ^( and end with )$ * feat: add option to disable validation and use soft validation by default * chore: fix test * docs: add readme about client-side validation
155 lines
4.4 KiB
Go
155 lines
4.4 KiB
Go
package unifi
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/go-playground/locales/en"
|
|
ut "github.com/go-playground/universal-translator"
|
|
vd "github.com/go-playground/validator/v10"
|
|
en_translations "github.com/go-playground/validator/v10/translations/en"
|
|
)
|
|
|
|
// ValidationError is a custom error type for validation errors.
|
|
type ValidationError struct {
|
|
Root error
|
|
Messages map[string]string
|
|
}
|
|
|
|
// Error returns the error message with combined all validation error messages.
|
|
func (v *ValidationError) Error() string {
|
|
err := "validation failed: \n"
|
|
for field, message := range v.Messages {
|
|
err += fmt.Sprintf("%s: %s\n", field, message)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Validator is the interface for the validator. Use it to validate structs. You can register structure-level validations
|
|
// with RegisterStructValidation.
|
|
type Validator interface {
|
|
// Validate validates the given struct and returns an error if the struct is not valid.
|
|
Validate(i interface{}) error
|
|
// RegisterStructValidation registers a structure-level validation function for a given struct type.
|
|
RegisterStructValidation(fn vd.StructLevelFunc, i interface{})
|
|
// RegisterTranslation registers a custom translation for a given tag.
|
|
RegisterTranslation(tag string, registerFn vd.RegisterTranslationsFunc, translationFn vd.TranslationFunc) (err error)
|
|
// RegisterCustomValidator registers a custom validator function with own tag and error message.
|
|
RegisterCustomValidator(cv CustomValidator) error
|
|
}
|
|
|
|
type validator struct {
|
|
validate *vd.Validate
|
|
trans ut.Translator
|
|
}
|
|
|
|
func (v *validator) Validate(i interface{}) error {
|
|
if err := v.validate.Struct(i); err != nil {
|
|
var errs vd.ValidationErrors
|
|
errors.As(err, &errs)
|
|
messages := errs.Translate(v.trans)
|
|
|
|
return &ValidationError{Root: err, Messages: messages}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *validator) RegisterStructValidation(f vd.StructLevelFunc, s interface{}) {
|
|
v.validate.RegisterStructValidation(f, s)
|
|
}
|
|
|
|
func (v *validator) RegisterTranslation(tag string, registerFn vd.RegisterTranslationsFunc, translationFn vd.TranslationFunc) error {
|
|
return v.validate.RegisterTranslation(tag, v.trans, registerFn, translationFn)
|
|
}
|
|
|
|
func (v *validator) RegisterCustomValidator(cv CustomValidator) error {
|
|
var err error
|
|
if err = v.validate.RegisterValidation(cv.tag, cv.fn, false); err != nil {
|
|
return fmt.Errorf("failed to register custom validation '%s': %w", cv.tag, err)
|
|
}
|
|
err = v.RegisterTranslation(cv.tag, func(ut ut.Translator) error {
|
|
return ut.Add(cv.tag, cv.messageText, true)
|
|
}, func(ut ut.Translator, fe vd.FieldError) string {
|
|
t, _ := ut.T(cv.tag, append([]string{fe.Field()}, cv.params...)...)
|
|
return t
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to register custom validation '%s' translation: %w", cv.tag, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newValidator() (*validator, error) {
|
|
validate := vd.New(vd.WithRequiredStructEnabled())
|
|
enLocale := en.New()
|
|
uni := ut.New(enLocale, enLocale)
|
|
trans, _ := uni.GetTranslator(enLocale.Locale())
|
|
err := en_translations.RegisterDefaultTranslations(validate, trans)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v := &validator{
|
|
validate: validate,
|
|
trans: trans,
|
|
}
|
|
|
|
for _, customValidator := range customValidators {
|
|
if err = v.RegisterCustomValidator(customValidator); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
type CustomValidator struct {
|
|
tag string
|
|
fn vd.Func
|
|
messageText string
|
|
params []string
|
|
}
|
|
|
|
func NewCustomRegexValidator(tag string, regex string) CustomValidator {
|
|
cv := &CustomValidator{
|
|
tag: tag,
|
|
messageText: regexValidatorMessage,
|
|
params: []string{regex},
|
|
}
|
|
crv := CustomRegexValidator{
|
|
CustomValidator: cv,
|
|
regex: lazyRegexCompile(regex),
|
|
}
|
|
crv.fn = func(fl vd.FieldLevel) bool {
|
|
return crv.regex().MatchString(fl.Field().String())
|
|
}
|
|
return *crv.CustomValidator
|
|
}
|
|
|
|
type CustomRegexValidator struct {
|
|
*CustomValidator
|
|
regex func() *regexp.Regexp
|
|
}
|
|
|
|
var customValidators = []CustomValidator{
|
|
NewCustomRegexValidator("w_regex", wRegexString),
|
|
NewCustomRegexValidator("numeric_nonzero", `^[1-9][0-9]*$`),
|
|
}
|
|
|
|
func lazyRegexCompile(str string) func() *regexp.Regexp {
|
|
var regex *regexp.Regexp
|
|
var once sync.Once
|
|
return func() *regexp.Regexp {
|
|
once.Do(func() {
|
|
regex = regexp.MustCompile(str)
|
|
})
|
|
return regex
|
|
}
|
|
}
|
|
|
|
const (
|
|
regexValidatorMessage = "{0} must comply with the regular expression pattern '{1}'"
|
|
wRegexString = `^[\w]+$`
|
|
)
|