Skip to content

Commit 5b97da4

Browse files
author
Dean Karn
authored
Merge pull request #560 from go-playground/cherry-pick-v9
cherry picking v9
2 parents a6a294b + c2546fb commit 5b97da4

File tree

8 files changed

+150
-31
lines changed

8 files changed

+150
-31
lines changed

_examples/simple/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ func validateStruct() {
6868

6969
fmt.Println(err.Namespace())
7070
fmt.Println(err.Field())
71-
fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or
72-
fmt.Println(err.StructField()) // by passing alt name to ReportError like below
71+
fmt.Println(err.StructNamespace())
72+
fmt.Println(err.StructField())
7373
fmt.Println(err.Tag())
7474
fmt.Println(err.ActualTag())
7575
fmt.Println(err.Kind())

_examples/struct-level/main.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"fmt"
5+
"reflect"
6+
"strings"
57

68
"github.com/go-playground/validator/v10"
79
)
@@ -11,7 +13,7 @@ type User struct {
1113
FirstName string `json:"fname"`
1214
LastName string `json:"lname"`
1315
Age uint8 `validate:"gte=0,lte=130"`
14-
Email string `validate:"required,email"`
16+
Email string `json:"e-mail" validate:"required,email"`
1517
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
1618
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
1719
}
@@ -31,6 +33,15 @@ func main() {
3133

3234
validate = validator.New()
3335

36+
// register function to get tag name from json tags.
37+
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
38+
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
39+
if name == "-" {
40+
return ""
41+
}
42+
return name
43+
})
44+
3445
// register validation for 'User'
3546
// NOTE: only have to register a non-pointer type for 'User', validator
3647
// interanlly dereferences during it's type checks.
@@ -48,7 +59,7 @@ func main() {
4859
FirstName: "",
4960
LastName: "",
5061
Age: 45,
51-
Email: "Badger.Smith@gmail.com",
62+
Email: "Badger.Smith@gmail",
5263
FavouriteColor: "#000",
5364
Addresses: []*Address{address},
5465
}
@@ -67,10 +78,10 @@ func main() {
6778

6879
for _, err := range err.(validator.ValidationErrors) {
6980

70-
fmt.Println(err.Namespace())
71-
fmt.Println(err.Field())
72-
fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or
73-
fmt.Println(err.StructField()) // by passing alt name to ReportError like below
81+
fmt.Println(err.Namespace()) // can differ when a custom TagNameFunc is registered or
82+
fmt.Println(err.Field()) // by passing alt name to ReportError like below
83+
fmt.Println(err.StructNamespace())
84+
fmt.Println(err.StructField())
7485
fmt.Println(err.Tag())
7586
fmt.Println(err.ActualTag())
7687
fmt.Println(err.Kind())
@@ -101,8 +112,8 @@ func UserStructLevelValidation(sl validator.StructLevel) {
101112
user := sl.Current().Interface().(User)
102113

103114
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
104-
sl.ReportError(user.FirstName, "FirstName", "fname", "fnameorlname", "")
105-
sl.ReportError(user.LastName, "LastName", "lname", "fnameorlname", "")
115+
sl.ReportError(user.FirstName, "fname", "FirstName", "fnameorlname", "")
116+
sl.ReportError(user.LastName, "lname", "LastName", "fnameorlname", "")
106117
}
107118

108119
// plus can do more, even with different tag than "fnameorlname"

baked_in.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ var (
103103
"rgba": isRGBA,
104104
"hsl": isHSL,
105105
"hsla": isHSLA,
106+
"e164": isE164,
106107
"email": isEmail,
107108
"url": isURL,
108109
"uri": isURI,
@@ -227,14 +228,28 @@ func isOneOf(fl FieldLevel) bool {
227228
func isUnique(fl FieldLevel) bool {
228229

229230
field := fl.Field()
231+
param := fl.Param()
230232
v := reflect.ValueOf(struct{}{})
231233

232234
switch field.Kind() {
233235
case reflect.Slice, reflect.Array:
234-
m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
236+
if param == "" {
237+
m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
238+
239+
for i := 0; i < field.Len(); i++ {
240+
m.SetMapIndex(field.Index(i), v)
241+
}
242+
return field.Len() == m.Len()
243+
}
235244

245+
sf, ok := field.Type().Elem().FieldByName(param)
246+
if !ok {
247+
panic(fmt.Sprintf("Bad field name %s", param))
248+
}
249+
250+
m := reflect.MakeMap(reflect.MapOf(sf.Type, v.Type()))
236251
for i := 0; i < field.Len(); i++ {
237-
m.SetMapIndex(field.Index(i), v)
252+
m.SetMapIndex(field.Index(i).FieldByName(param), v)
238253
}
239254
return field.Len() == m.Len()
240255
case reflect.Map:
@@ -1227,6 +1242,11 @@ func isFile(fl FieldLevel) bool {
12271242
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
12281243
}
12291244

1245+
// IsE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
1246+
func isE164(fl FieldLevel) bool {
1247+
return e164Regex.MatchString(fl.Field().String())
1248+
}
1249+
12301250
// IsEmail is the validation function for validating if the current field's value is a valid email address.
12311251
func isEmail(fl FieldLevel) bool {
12321252
return emailRegex.MatchString(fl.Field().String())

doc.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -587,9 +587,15 @@ Unique
587587
588588
For arrays & slices, unique will ensure that there are no duplicates.
589589
For maps, unique will ensure that there are no duplicate values.
590+
For slices of struct, unique will ensure that there are no duplicate values
591+
in a field of the struct specified via a parameter.
590592
593+
// For arrays, slices, and maps:
591594
Usage: unique
592595
596+
// For slices of struct:
597+
Usage: unique=field
598+
593599
Alpha Only
594600
595601
This validates that a string value contains ASCII alpha characters only
@@ -1060,27 +1066,14 @@ Validator notes:
10601066
And the best reason, you can submit a pull request and we can keep on
10611067
adding to the validation library of this package!
10621068
1063-
Panics
1064-
1065-
This package panics when bad input is provided, this is by design, bad code like
1066-
that should not make it to production.
1067-
1068-
type Test struct {
1069-
TestField string `validate:"nonexistantfunction=1"`
1070-
}
1071-
1072-
t := &Test{
1073-
TestField: "Test"
1074-
}
1075-
1076-
validate.Struct(t) // this will panic
1077-
10781069
Non standard validators
10791070
10801071
A collection of validation rules that are frequently needed but are more
10811072
complex than the ones found in the baked in validators.
1082-
A non standard validator must be registered manually using any tag you like.
1083-
See below examples of registration and use.
1073+
A non standard validator must be registered manually like you would
1074+
with your own custom validation functions.
1075+
1076+
Example of registration and use:
10841077
10851078
type Test struct {
10861079
TestField string `validate:"yourtag"`
@@ -1091,13 +1084,30 @@ See below examples of registration and use.
10911084
}
10921085
10931086
validate := validator.New()
1094-
validate.RegisterValidation("yourtag", validations.ValidatorName)
1087+
validate.RegisterValidation("yourtag", validators.NotBlank)
1088+
1089+
Here is a list of the current non standard validators:
10951090
10961091
NotBlank
10971092
This validates that the value is not blank or with length zero.
10981093
For strings ensures they do not contain only spaces. For channels, maps, slices and arrays
10991094
ensures they don't have zero length. For others, a non empty value is required.
11001095
11011096
Usage: notblank
1097+
1098+
Panics
1099+
1100+
This package panics when bad input is provided, this is by design, bad code like
1101+
that should not make it to production.
1102+
1103+
type Test struct {
1104+
TestField string `validate:"nonexistantfunction=1"`
1105+
}
1106+
1107+
t := &Test{
1108+
TestField: "Test"
1109+
}
1110+
1111+
validate.Struct(t) // this will panic
11021112
*/
11031113
package validator

field_level.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import "reflect"
55
// FieldLevel contains all the information and helper functions
66
// to validate a field
77
type FieldLevel interface {
8-
98
// returns the top level struct, if any
109
Top() reflect.Value
1110

@@ -26,6 +25,9 @@ type FieldLevel interface {
2625
// returns param for validation against current field
2726
Param() string
2827

28+
// GetTag returns the current validations tag name
29+
GetTag() string
30+
2931
// ExtractType gets the actual underlying type of field value.
3032
// It will dive into pointers, customTypes and return you the
3133
// underlying value and it's kind.
@@ -73,6 +75,11 @@ func (v *validate) FieldName() string {
7375
return v.cf.altName
7476
}
7577

78+
// GetTag returns the current validations tag name
79+
func (v *validate) GetTag() string {
80+
return v.ct.tag
81+
}
82+
7683
// StructFieldName returns the struct field's name
7784
func (v *validate) StructFieldName() string {
7885
return v.cf.name

regexes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
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*\\)$"
1717
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*\\)$"
1818
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}])))\\.?$"
19+
e164RegexString = "^\\+[1-9]?[0-9]{7,14}$"
1920
base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
2021
base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$"
2122
iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$"
@@ -62,6 +63,7 @@ var (
6263
rgbaRegex = regexp.MustCompile(rgbaRegexString)
6364
hslRegex = regexp.MustCompile(hslRegexString)
6465
hslaRegex = regexp.MustCompile(hslaRegexString)
66+
e164Regex = regexp.MustCompile(e164RegexString)
6567
emailRegex = regexp.MustCompile(emailRegexString)
6668
base64Regex = regexp.MustCompile(base64RegexString)
6769
base64URLRegex = regexp.MustCompile(base64URLRegexString)

translations/en/en.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
10431043
translation: "{0} must be a valid HSLA color",
10441044
override: false,
10451045
},
1046+
{
1047+
tag: "e164",
1048+
translation: "{0} must be a valid E.164 formatted phone number",
1049+
override: false,
1050+
},
10461051
{
10471052
tag: "email",
10481053
translation: "{0} must be a valid email address",

validator_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4917,6 +4917,7 @@ func TestStructOnlyValidation(t *testing.T) {
49174917
FirstName string `json:"fname"`
49184918
LastName string `json:"lname"`
49194919
Age uint8 `validate:"gte=0,lte=130"`
4920+
Number string `validate:"required,e164"`
49204921
Email string `validate:"required,email"`
49214922
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
49224923
Addresses []*Address `validate:"required"` // a person can have a home and cottage...
@@ -4934,6 +4935,7 @@ func TestStructOnlyValidation(t *testing.T) {
49344935
FirstName: "",
49354936
LastName: "",
49364937
Age: 45,
4938+
Number: "+1123456789",
49374939
49384940
FavouriteColor: "#000",
49394941
Addresses: []*Address{address},
@@ -8183,6 +8185,49 @@ func TestUniqueValidation(t *testing.T) {
81838185
PanicMatches(t, func() { _ = validate.Var(1.0, "unique") }, "Bad field type float64")
81848186
}
81858187

8188+
func TestUniqueValidationStructSlice(t *testing.T) {
8189+
testStructs := []struct {
8190+
A string
8191+
B string
8192+
}{
8193+
{A: "one", B: "two"},
8194+
{A: "one", B: "three"},
8195+
}
8196+
8197+
tests := []struct {
8198+
target interface{}
8199+
param string
8200+
expected bool
8201+
}{
8202+
{testStructs, "unique", true},
8203+
{testStructs, "unique=A", false},
8204+
{testStructs, "unique=B", true},
8205+
}
8206+
8207+
validate := New()
8208+
8209+
for i, test := range tests {
8210+
8211+
errs := validate.Var(test.target, test.param)
8212+
8213+
if test.expected {
8214+
if !IsEqual(errs, nil) {
8215+
t.Fatalf("Index: %d unique failed Error: %v", i, errs)
8216+
}
8217+
} else {
8218+
if IsEqual(errs, nil) {
8219+
t.Fatalf("Index: %d unique failed Error: %v", i, errs)
8220+
} else {
8221+
val := getError(errs, "", "")
8222+
if val.Tag() != "unique" {
8223+
t.Fatalf("Index: %d unique failed Error: %v", i, errs)
8224+
}
8225+
}
8226+
}
8227+
}
8228+
PanicMatches(t, func() { validate.Var(testStructs, "unique=C") }, "Bad field name C")
8229+
}
8230+
81868231
func TestHTMLValidation(t *testing.T) {
81878232
tests := []struct {
81888233
param string
@@ -8939,3 +8984,22 @@ func TestRequiredWithoutAllPointers(t *testing.T) {
89398984
errs = val.Struct(lookup)
89408985
Equal(t, errs, nil)
89418986
}
8987+
8988+
func TestGetTag(t *testing.T) {
8989+
var tag string
8990+
8991+
type Test struct {
8992+
String string `validate:"mytag"`
8993+
}
8994+
8995+
val := New()
8996+
val.RegisterValidation("mytag", func(fl FieldLevel) bool {
8997+
tag = fl.GetTag()
8998+
return true
8999+
})
9000+
9001+
var test Test
9002+
errs := val.Struct(test)
9003+
Equal(t, errs, nil)
9004+
Equal(t, tag, "mytag")
9005+
}

0 commit comments

Comments
 (0)