From e7c70798c91b2f7aba04c2eab72e24513423208a Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 16 Oct 2019 16:16:27 -0600 Subject: [PATCH 1/7] Add e.164 support --- baked_in.go | 6 ++++++ regexes.go | 2 ++ translations/en/en.go | 5 +++++ validator_test.go | 2 ++ 4 files changed, 15 insertions(+) diff --git a/baked_in.go b/baked_in.go index 04cf57d..f42d8d5 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, @@ -1227,6 +1228,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/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..d0c4278 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}, From 703c0b681a8ec1be5210e0864ceb8e04a7db8dab Mon Sep 17 00:00:00 2001 From: Jean-Philippe Moal Date: Thu, 17 Oct 2019 19:41:40 +0200 Subject: [PATCH 2/7] Rework the non standard validators documentation --- doc.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/doc.go b/doc.go index cdfb759..34dc041 100644 --- a/doc.go +++ b/doc.go @@ -1060,27 +1060,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 +1078,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 +1088,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 From 1effcb06a7ea4a33880612a92bd69b3203e00087 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Moal Date: Thu, 17 Oct 2019 20:02:12 +0200 Subject: [PATCH 3/7] Clarify and complete tag names example --- _examples/simple/main.go | 4 ++-- _examples/struct-level/main.go | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) 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" From 8f604265e0ead955151bc460ee9b123c029d37d1 Mon Sep 17 00:00:00 2001 From: Shi Han NG Date: Fri, 2 Aug 2019 17:11:23 +0900 Subject: [PATCH 4/7] Implement unique=FieldName --- baked_in.go | 18 ++++++++++++++++-- validator_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/baked_in.go b/baked_in.go index f42d8d5..9eff0e4 100644 --- a/baked_in.go +++ b/baked_in.go @@ -228,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: diff --git a/validator_test.go b/validator_test.go index d0c4278..fcad8f9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -8185,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 From e73ec5f5ab3556cc309d3b8cacc9710fb10d4c44 Mon Sep 17 00:00:00 2001 From: Shi Han NG Date: Fri, 2 Aug 2019 20:42:47 +0900 Subject: [PATCH 5/7] Update doc for unique=field --- doc.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc.go b/doc.go index 34dc041..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 From f8a081fd8345e9ca0b9dc00434edde1830bf24a5 Mon Sep 17 00:00:00 2001 From: richi Date: Thu, 7 Nov 2019 21:35:57 +0500 Subject: [PATCH 6/7] Added the function GetTag. The function returns the name of the tag on which the function was called. --- field_level.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/field_level.go b/field_level.go index 7a13f33..c5b62cc 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,8 @@ type FieldLevel interface { // returns param for validation against current field Param() string + 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 +74,11 @@ func (v *validate) FieldName() string { return v.cf.altName } +// GetTag returns the tag name of field +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 From c2546fb355866e60701984fc84e4c929f3f62216 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Tue, 24 Dec 2019 21:13:30 -0800 Subject: [PATCH 7/7] Add test + docs for FieldLevel.GetTag --- field_level.go | 3 ++- validator_test.go | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/field_level.go b/field_level.go index c5b62cc..f0e2a9a 100644 --- a/field_level.go +++ b/field_level.go @@ -25,6 +25,7 @@ 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. @@ -74,7 +75,7 @@ func (v *validate) FieldName() string { return v.cf.altName } -// GetTag returns the tag name of field +// GetTag returns the current validations tag name func (v *validate) GetTag() string { return v.ct.tag } diff --git a/validator_test.go b/validator_test.go index fcad8f9..2c8b96b 100644 --- a/validator_test.go +++ b/validator_test.go @@ -8984,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") +}