diff --git a/_examples/simple/main.go b/_examples/simple/main.go index eb9af79..0d3ca51 100644 --- a/_examples/simple/main.go +++ b/_examples/simple/main.go @@ -68,8 +68,8 @@ func validateStruct() { fmt.Println(err.Namespace()) fmt.Println(err.Field()) - fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or - fmt.Println(err.StructField()) // by passing alt name to ReportError like below + fmt.Println(err.StructNamespace()) + fmt.Println(err.StructField()) fmt.Println(err.Tag()) fmt.Println(err.ActualTag()) fmt.Println(err.Kind()) diff --git a/_examples/struct-level/main.go b/_examples/struct-level/main.go index 5e69f6b..3231a81 100644 --- a/_examples/struct-level/main.go +++ b/_examples/struct-level/main.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "reflect" + "strings" "github.com/go-playground/validator/v10" ) @@ -11,7 +13,7 @@ type User struct { FirstName string `json:"fname"` LastName string `json:"lname"` Age uint8 `validate:"gte=0,lte=130"` - Email string `validate:"required,email"` + Email string `json:"e-mail" validate:"required,email"` FavouriteColor string `validate:"hexcolor|rgb|rgba"` Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage... } @@ -31,6 +33,15 @@ func main() { validate = validator.New() + // register function to get tag name from json tags. + validate.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + return name + }) + // register validation for 'User' // NOTE: only have to register a non-pointer type for 'User', validator // interanlly dereferences during it's type checks. @@ -48,7 +59,7 @@ func main() { FirstName: "", LastName: "", Age: 45, - Email: "Badger.Smith@gmail.com", + Email: "Badger.Smith@gmail", FavouriteColor: "#000", Addresses: []*Address{address}, } @@ -67,10 +78,10 @@ func main() { for _, err := range err.(validator.ValidationErrors) { - fmt.Println(err.Namespace()) - fmt.Println(err.Field()) - fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or - fmt.Println(err.StructField()) // by passing alt name to ReportError like below + fmt.Println(err.Namespace()) // can differ when a custom TagNameFunc is registered or + fmt.Println(err.Field()) // by passing alt name to ReportError like below + fmt.Println(err.StructNamespace()) + fmt.Println(err.StructField()) fmt.Println(err.Tag()) fmt.Println(err.ActualTag()) fmt.Println(err.Kind()) @@ -101,8 +112,8 @@ func UserStructLevelValidation(sl validator.StructLevel) { user := sl.Current().Interface().(User) if len(user.FirstName) == 0 && len(user.LastName) == 0 { - sl.ReportError(user.FirstName, "FirstName", "fname", "fnameorlname", "") - sl.ReportError(user.LastName, "LastName", "lname", "fnameorlname", "") + sl.ReportError(user.FirstName, "fname", "FirstName", "fnameorlname", "") + sl.ReportError(user.LastName, "lname", "LastName", "fnameorlname", "") } // plus can do more, even with different tag than "fnameorlname" diff --git a/baked_in.go b/baked_in.go index 04cf57d..9eff0e4 100644 --- a/baked_in.go +++ b/baked_in.go @@ -103,6 +103,7 @@ var ( "rgba": isRGBA, "hsl": isHSL, "hsla": isHSLA, + "e164": isE164, "email": isEmail, "url": isURL, "uri": isURI, @@ -227,14 +228,28 @@ func isOneOf(fl FieldLevel) bool { func isUnique(fl FieldLevel) bool { field := fl.Field() + param := fl.Param() v := reflect.ValueOf(struct{}{}) switch field.Kind() { case reflect.Slice, reflect.Array: - m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type())) + if param == "" { + m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type())) + + for i := 0; i < field.Len(); i++ { + m.SetMapIndex(field.Index(i), v) + } + return field.Len() == m.Len() + } + sf, ok := field.Type().Elem().FieldByName(param) + if !ok { + panic(fmt.Sprintf("Bad field name %s", param)) + } + + m := reflect.MakeMap(reflect.MapOf(sf.Type, v.Type())) for i := 0; i < field.Len(); i++ { - m.SetMapIndex(field.Index(i), v) + m.SetMapIndex(field.Index(i).FieldByName(param), v) } return field.Len() == m.Len() case reflect.Map: @@ -1227,6 +1242,11 @@ func isFile(fl FieldLevel) bool { panic(fmt.Sprintf("Bad field type %T", field.Interface())) } +// IsE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number. +func isE164(fl FieldLevel) bool { + return e164Regex.MatchString(fl.Field().String()) +} + // IsEmail is the validation function for validating if the current field's value is a valid email address. func isEmail(fl FieldLevel) bool { return emailRegex.MatchString(fl.Field().String()) diff --git a/doc.go b/doc.go index cdfb759..799882c 100644 --- a/doc.go +++ b/doc.go @@ -587,9 +587,15 @@ Unique For arrays & slices, unique will ensure that there are no duplicates. For maps, unique will ensure that there are no duplicate values. +For slices of struct, unique will ensure that there are no duplicate values +in a field of the struct specified via a parameter. + // For arrays, slices, and maps: Usage: unique + // For slices of struct: + Usage: unique=field + Alpha Only This validates that a string value contains ASCII alpha characters only @@ -1060,27 +1066,14 @@ Validator notes: And the best reason, you can submit a pull request and we can keep on adding to the validation library of this package! -Panics - -This package panics when bad input is provided, this is by design, bad code like -that should not make it to production. - - type Test struct { - TestField string `validate:"nonexistantfunction=1"` - } - - t := &Test{ - TestField: "Test" - } - - validate.Struct(t) // this will panic - Non standard validators A collection of validation rules that are frequently needed but are more complex than the ones found in the baked in validators. -A non standard validator must be registered manually using any tag you like. -See below examples of registration and use. +A non standard validator must be registered manually like you would +with your own custom validation functions. + +Example of registration and use: type Test struct { TestField string `validate:"yourtag"` @@ -1091,7 +1084,9 @@ See below examples of registration and use. } validate := validator.New() - validate.RegisterValidation("yourtag", validations.ValidatorName) + validate.RegisterValidation("yourtag", validators.NotBlank) + +Here is a list of the current non standard validators: NotBlank This validates that the value is not blank or with length zero. @@ -1099,5 +1094,20 @@ See below examples of registration and use. ensures they don't have zero length. For others, a non empty value is required. Usage: notblank + +Panics + +This package panics when bad input is provided, this is by design, bad code like +that should not make it to production. + + type Test struct { + TestField string `validate:"nonexistantfunction=1"` + } + + t := &Test{ + TestField: "Test" + } + + validate.Struct(t) // this will panic */ package validator diff --git a/field_level.go b/field_level.go index 7a13f33..f0e2a9a 100644 --- a/field_level.go +++ b/field_level.go @@ -5,7 +5,6 @@ import "reflect" // FieldLevel contains all the information and helper functions // to validate a field type FieldLevel interface { - // returns the top level struct, if any Top() reflect.Value @@ -26,6 +25,9 @@ type FieldLevel interface { // returns param for validation against current field Param() string + // GetTag returns the current validations tag name + GetTag() string + // ExtractType gets the actual underlying type of field value. // It will dive into pointers, customTypes and return you the // underlying value and it's kind. @@ -73,6 +75,11 @@ func (v *validate) FieldName() string { return v.cf.altName } +// GetTag returns the current validations tag name +func (v *validate) GetTag() string { + return v.ct.tag +} + // StructFieldName returns the struct field's name func (v *validate) StructFieldName() string { return v.cf.name diff --git a/regexes.go b/regexes.go index 6343abe..63fdc1d 100644 --- a/regexes.go +++ b/regexes.go @@ -16,6 +16,7 @@ const ( hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + e164RegexString = "^\\+[1-9]?[0-9]{7,14}$" base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$" iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" @@ -62,6 +63,7 @@ var ( rgbaRegex = regexp.MustCompile(rgbaRegexString) hslRegex = regexp.MustCompile(hslRegexString) hslaRegex = regexp.MustCompile(hslaRegexString) + e164Regex = regexp.MustCompile(e164RegexString) emailRegex = regexp.MustCompile(emailRegexString) base64Regex = regexp.MustCompile(base64RegexString) base64URLRegex = regexp.MustCompile(base64URLRegexString) diff --git a/translations/en/en.go b/translations/en/en.go index fd43f38..3b1058b 100644 --- a/translations/en/en.go +++ b/translations/en/en.go @@ -1043,6 +1043,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} must be a valid HSLA color", override: false, }, + { + tag: "e164", + translation: "{0} must be a valid E.164 formatted phone number", + override: false, + }, { tag: "email", translation: "{0} must be a valid email address", diff --git a/validator_test.go b/validator_test.go index 968503e..2c8b96b 100644 --- a/validator_test.go +++ b/validator_test.go @@ -4917,6 +4917,7 @@ func TestStructOnlyValidation(t *testing.T) { FirstName string `json:"fname"` LastName string `json:"lname"` Age uint8 `validate:"gte=0,lte=130"` + Number string `validate:"required,e164"` Email string `validate:"required,email"` FavouriteColor string `validate:"hexcolor|rgb|rgba"` Addresses []*Address `validate:"required"` // a person can have a home and cottage... @@ -4934,6 +4935,7 @@ func TestStructOnlyValidation(t *testing.T) { FirstName: "", LastName: "", Age: 45, + Number: "+1123456789", Email: "Badger.Smith@gmail.com", FavouriteColor: "#000", Addresses: []*Address{address}, @@ -8183,6 +8185,49 @@ func TestUniqueValidation(t *testing.T) { PanicMatches(t, func() { _ = validate.Var(1.0, "unique") }, "Bad field type float64") } +func TestUniqueValidationStructSlice(t *testing.T) { + testStructs := []struct { + A string + B string + }{ + {A: "one", B: "two"}, + {A: "one", B: "three"}, + } + + tests := []struct { + target interface{} + param string + expected bool + }{ + {testStructs, "unique", true}, + {testStructs, "unique=A", false}, + {testStructs, "unique=B", true}, + } + + validate := New() + + for i, test := range tests { + + errs := validate.Var(test.target, test.param) + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d unique failed Error: %v", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d unique failed Error: %v", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "unique" { + t.Fatalf("Index: %d unique failed Error: %v", i, errs) + } + } + } + } + PanicMatches(t, func() { validate.Var(testStructs, "unique=C") }, "Bad field name C") +} + func TestHTMLValidation(t *testing.T) { tests := []struct { param string @@ -8939,3 +8984,22 @@ func TestRequiredWithoutAllPointers(t *testing.T) { errs = val.Struct(lookup) Equal(t, errs, nil) } + +func TestGetTag(t *testing.T) { + var tag string + + type Test struct { + String string `validate:"mytag"` + } + + val := New() + val.RegisterValidation("mytag", func(fl FieldLevel) bool { + tag = fl.GetTag() + return true + }) + + var test Test + errs := val.Struct(test) + Equal(t, errs, nil) + Equal(t, tag, "mytag") +}