package validation import ( "errors" "fmt" "reflect" "strconv" "strings" "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" valid "github.com/go-playground/validator/v10" en_translations "github.com/go-playground/validator/v10/translations/en" ) type Translation struct { Tag string RegisterFn valid.RegisterTranslationsFunc TranslationFn valid.TranslationFunc } type validation struct { Validator *valid.Validate Translator ut.Translator } type validationOption func(*validation) error func New(opts ...validationOption) (*validation, error) { validator := valid.New() validator.RegisterValidation("possible-values", isPossibleValues) enTranslator := en.New() universalTranslator := ut.New(enTranslator, enTranslator) translator, found := universalTranslator.GetTranslator("en") if !found { return nil, errors.New("translator for en locale is not found") } validation := &validation{ Validator: validator, Translator: translator, } for _, opt := range opts { if err := opt(validation); err != nil { return nil, err } } return validation, nil } func WithJSONNamesForStructFields() validationOption { return func(v *validation) error { v.Validator.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] return name }) return nil } } func WithPredefinedErrorMessages() validationOption { translations := []Translation{ { Tag: "possible-values", RegisterFn: func(ut ut.Translator) error { return ut.Add("possible-values", "The {0} must use the following values: {1}", true) }, TranslationFn: func(ut ut.Translator, fe valid.FieldError) string { t, _ := ut.T("possible-values", fe.Field(), fe.Param()) return t }, }, { Tag: "required_if", RegisterFn: func(ut ut.Translator) error { // TODO: Improve the message for condition required_if for several fields return ut.Add("required_if", "The {0} is required since the {1} is {2}", true) }, TranslationFn: func(ut ut.Translator, fe valid.FieldError) string { params := []string{fe.Field()} params = append(params, strings.Split(fe.Param(), " ")...) t, _ := ut.T("required_if", params...) return t }, }, } return func(v *validation) error { if err := registerTranslations(translations, v.Validator, v.Translator); err != nil { return err } return nil } } func WithCustomErrorMessages(translations []Translation) validationOption { return func(v *validation) error { if err := registerTranslations(translations, v.Validator, v.Translator); err != nil { return err } return nil } } func (v *validation) ValidateStruct(s interface{}) error { var errStr string errs := v.Validator.Struct(s) if errs != nil { if err, ok := errs.(*valid.InvalidValidationError); ok { return err } for _, err := range errs.(valid.ValidationErrors) { errStr += err.Translate(v.Translator) + ". " } return errors.New(errStr) } return nil } func registerTranslations(translations []Translation, validator *valid.Validate, translator ut.Translator) error { if err := en_translations.RegisterDefaultTranslations(validator, translator); err != nil { return err } for _, t := range translations { if err := validator.RegisterTranslation(t.Tag, translator, t.RegisterFn, t.TranslationFn); err != nil { return err } } return nil } func isPossibleValues(fl valid.FieldLevel) bool { vals := strings.Split(strings.TrimSpace(fl.Param()), " ") field := fl.Field() switch field.Kind() { case reflect.String: val := field.String() // Empty value can be used vals = append(vals, "") return contains(vals, val) case reflect.Int: val := strconv.FormatInt(field.Int(), 10) return contains(vals, val) case reflect.Slice: slice, ok := field.Interface().([]string) if !ok { panic("Only []string can be used as slice type") } for _, val := range slice { if !contains(vals, val) { return false } } return true default: panic(fmt.Sprintf("Bad field type %T", field.Interface())) } } func contains(slice []string, str string) bool { for _, v := range slice { if v == str { return true } } return false }