From 9ff11ae808be91c24d3f10d7219c813dbdb73f10 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 10 Jun 2015 22:03:57 -0400 Subject: [PATCH 01/20] break out benchmarks into separate file create examples file for better godocs --- benchmarks_test.go | 163 +++++++++++++++++++++++++++++++++++++++++++++ examples_test.go | 95 ++++++++++++++++++++++++++ validator.go | 2 +- validator_test.go | 160 -------------------------------------------- 4 files changed, 259 insertions(+), 161 deletions(-) create mode 100644 benchmarks_test.go create mode 100644 examples_test.go diff --git a/benchmarks_test.go b/benchmarks_test.go new file mode 100644 index 0000000..2517209 --- /dev/null +++ b/benchmarks_test.go @@ -0,0 +1,163 @@ +package validator + +import "testing" + +func BenchmarkValidateField(b *testing.B) { + for n := 0; n < b.N; n++ { + validate.Field("1", "len=1") + } +} + +func BenchmarkValidateStructSimple(b *testing.B) { + + type Foo struct { + StringValue string `validate:"min=5,max=10"` + IntValue int `validate:"min=5,max=10"` + } + + validFoo := &Foo{StringValue: "Foobar", IntValue: 7} + invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} + + for n := 0; n < b.N; n++ { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } +} + +// func BenchmarkTemplateParallelSimple(b *testing.B) { + +// type Foo struct { +// StringValue string `validate:"min=5,max=10"` +// IntValue int `validate:"min=5,max=10"` +// } + +// validFoo := &Foo{StringValue: "Foobar", IntValue: 7} +// invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} + +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(validFoo) +// validate.Struct(invalidFoo) +// } +// }) +// } + +func BenchmarkValidateStructLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + for n := 0; n < b.N; n++ { + validate.Struct(tSuccess) + validate.Struct(tFail) + } +} + +// func BenchmarkTemplateParallelLarge(b *testing.B) { + +// tFail := &TestString{ +// Required: "", +// Len: "", +// Min: "", +// Max: "12345678901", +// MinMax: "", +// Lt: "0123456789", +// Lte: "01234567890", +// Gt: "1", +// Gte: "1", +// OmitEmpty: "12345678901", +// Sub: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "", +// }, +// Iface: &Impl{ +// F: "12", +// }, +// } + +// tSuccess := &TestString{ +// Required: "Required", +// Len: "length==10", +// Min: "min=1", +// Max: "1234567890", +// MinMax: "12345", +// Lt: "012345678", +// Lte: "0123456789", +// Gt: "01234567890", +// Gte: "0123456789", +// OmitEmpty: "", +// Sub: &SubTest{ +// Test: "1", +// }, +// SubIgnore: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "1", +// }, +// Iface: &Impl{ +// F: "123", +// }, +// } + +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(tSuccess) +// validate.Struct(tFail) +// } +// }) +// } diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..c5dd351 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,95 @@ +package validator_test + +import ( + "fmt" + + "../validator" +) + +func ExampleValidate_new() { + validator.New("validate", validator.BakedInValidators) +} + +func ExampleValidate_addFunction() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + fn := func(top interface{}, current interface{}, field interface{}, param string) bool { + return field.(string) == "hello" + } + + validate.AddFunction("valueishello", fn) + + message := "hello" + err := validate.Field(message, "valueishello") + fmt.Println(err) + //Output: + // +} + +func ExampleValidate_field() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + i := 0 + err := validate.Field(i, "gt=1,lte=10") + fmt.Println(err.Field) + fmt.Println(err.Tag) + fmt.Println(err.Kind) // NOTE: Kind and Type can be different i.e. time Kind=struct and Type=time.Time + fmt.Println(err.Type) + fmt.Println(err.Param) + fmt.Println(err.Value) + //Output: + // + //gt + //int + //int + //1 + //0 +} + +func ExampleValidate_struct() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + type ContactInformation struct { + Phone string `validate:"required"` + Street string `validate:"required"` + City string `validate:"required"` + } + + type User struct { + Name string `validate:"required,excludesall=!@#$%^&*()_+-=:;?/0x2C"` // 0x2C = comma (,) + Age int8 `validate:"required,gt=0,lt=150"` + Email string `validate:"email"` + ContactInformation []*ContactInformation + } + + contactInfo := &ContactInformation{ + Street: "26 Here Blvd.", + City: "Paradeso", + } + + user := &User{ + Name: "Joey Bloggs", + Age: 31, + Email: "joeybloggs@gmail.com", + ContactInformation: []*ContactInformation{contactInfo}, + } + + structError := validate.Struct(user) + for _, fieldError := range structError.Errors { + fmt.Println(fieldError.Field) // Phone + fmt.Println(fieldError.Tag) // required + //... and so forth + //Output: + //Phone + //required + } +} diff --git a/validator.go b/validator.go index 33110f6..5f0c18a 100644 --- a/validator.go +++ b/validator.go @@ -246,7 +246,7 @@ func (v *Validate) SetTag(tagName string) { v.tagName = tagName } -// SetStructPoolMax sets the struct pools max size. this may be usefull for fine grained +// SetMaxStructPoolSize sets the struct pools max size. this may be usefull for fine grained // performance tuning towards your application, however, the default should be fine for // nearly all cases. only increase if you have a deeply nested struct structure. // NOTE: this method is not thread-safe diff --git a/validator_test.go b/validator_test.go index aa8c749..5828c2e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2335,163 +2335,3 @@ func TestInvalidValidatorFunction(t *testing.T) { PanicMatches(t, func() { validate.Field(s.Test, "zzxxBadFunction") }, fmt.Sprintf("Undefined validation function on field %s", "")) } - -func BenchmarkValidateField(b *testing.B) { - for n := 0; n < b.N; n++ { - validate.Field("1", "len=1") - } -} - -func BenchmarkValidateStructSimple(b *testing.B) { - - type Foo struct { - StringValue string `validate:"min=5,max=10"` - IntValue int `validate:"min=5,max=10"` - } - - validFoo := &Foo{StringValue: "Foobar", IntValue: 7} - invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} - - for n := 0; n < b.N; n++ { - validate.Struct(validFoo) - validate.Struct(invalidFoo) - } -} - -// func BenchmarkTemplateParallelSimple(b *testing.B) { - -// type Foo struct { -// StringValue string `validate:"min=5,max=10"` -// IntValue int `validate:"min=5,max=10"` -// } - -// validFoo := &Foo{StringValue: "Foobar", IntValue: 7} -// invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} - -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(validFoo) -// validate.Struct(invalidFoo) -// } -// }) -// } - -func BenchmarkValidateStructLarge(b *testing.B) { - - tFail := &TestString{ - Required: "", - Len: "", - Min: "", - Max: "12345678901", - MinMax: "", - Lt: "0123456789", - Lte: "01234567890", - Gt: "1", - Gte: "1", - OmitEmpty: "12345678901", - Sub: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "", - }, - Iface: &Impl{ - F: "12", - }, - } - - tSuccess := &TestString{ - Required: "Required", - Len: "length==10", - Min: "min=1", - Max: "1234567890", - MinMax: "12345", - Lt: "012345678", - Lte: "0123456789", - Gt: "01234567890", - Gte: "0123456789", - OmitEmpty: "", - Sub: &SubTest{ - Test: "1", - }, - SubIgnore: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "1", - }, - Iface: &Impl{ - F: "123", - }, - } - - for n := 0; n < b.N; n++ { - validate.Struct(tSuccess) - validate.Struct(tFail) - } -} - -// func BenchmarkTemplateParallelLarge(b *testing.B) { - -// tFail := &TestString{ -// Required: "", -// Len: "", -// Min: "", -// Max: "12345678901", -// MinMax: "", -// Lt: "0123456789", -// Lte: "01234567890", -// Gt: "1", -// Gte: "1", -// OmitEmpty: "12345678901", -// Sub: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "", -// }, -// Iface: &Impl{ -// F: "12", -// }, -// } - -// tSuccess := &TestString{ -// Required: "Required", -// Len: "length==10", -// Min: "min=1", -// Max: "1234567890", -// MinMax: "12345", -// Lt: "012345678", -// Lte: "0123456789", -// Gt: "01234567890", -// Gte: "0123456789", -// OmitEmpty: "", -// Sub: &SubTest{ -// Test: "1", -// }, -// SubIgnore: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "1", -// }, -// Iface: &Impl{ -// F: "123", -// }, -// } - -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(tSuccess) -// validate.Struct(tFail) -// } -// }) -// } From 5db5165e2c8b9b564f41ae9b55b853b9c8c2f5c4 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 22:07:30 -0400 Subject: [PATCH 02/20] update rub and reba regex rejects now properly test for RGB 255 and RGB using percentages, before it allowed mixing of percentages or numbers between 0-255 but it's either or now. --- regexes.go | 4 ++-- validator_test.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/regexes.go b/regexes.go index e5ed0fa..5ca45e9 100644 --- a/regexes.go +++ b/regexes.go @@ -9,8 +9,8 @@ const ( numberRegexString = "^[0-9]+$" hexadecimalRegexString = "^[0-9a-fA-F]+$" hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" - rgbRegexString = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$" - rgbaRegexString = "^rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" + rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" + rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" 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}])))\\.?$" diff --git a/validator_test.go b/validator_test.go index 5828c2e..7536527 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1736,10 +1736,19 @@ func TestRgba(t *testing.T) { err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55%,100%,0.12)" + err = validate.Field(s, "rgba") + Equal(t, err, nil) + s = "rgba( 0, 31, 255, 0.5)" err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55,100%,0.12)" + err = validate.Field(s, "rgba") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgba") + s = "rgb(0, 31, 255)" err = validate.Field(s, "rgba") NotEqual(t, err, nil) @@ -1769,6 +1778,15 @@ func TestRgb(t *testing.T) { err = validate.Field(s, "rgb") Equal(t, err, nil) + s = "rgb(10%, 50%, 100%)" + err = validate.Field(s, "rgb") + Equal(t, err, nil) + + s = "rgb(10%, 50%, 55)" + err = validate.Field(s, "rgb") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgb") + s = "rgb(1,349,275)" err = validate.Field(s, "rgb") NotEqual(t, err, nil) From 05e0fe1f85065bbfbee4af1d0a8f966328246882 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 22:12:36 -0400 Subject: [PATCH 03/20] updated various regex's to remove any capturing groups --- regexes.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regexes.go b/regexes.go index 5ca45e9..31c1c68 100644 --- a/regexes.go +++ b/regexes.go @@ -11,9 +11,9 @@ const ( hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" - 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}])))\\.?$" + 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}])))\\.?$" base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" ) From 20d9b7909fbb2b144b958358b55d768492f38a14 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 23:22:36 -0400 Subject: [PATCH 04/20] add isbn, isbn10 and isbn validators + tests + documentation --- baked_in.go | 59 ++++++++++++++++++++++++++++ doc.go | 12 ++++++ regexes.go | 4 ++ validator_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) diff --git a/baked_in.go b/baked_in.go index ed69eb3..50c0c19 100644 --- a/baked_in.go +++ b/baked_in.go @@ -50,6 +50,65 @@ var BakedInValidators = map[string]Func{ "excludes": excludes, "excludesall": excludesAll, "excludesrune": excludesRune, + "isbn": isISBN, + "isbn10": isISBN10, + "isbn13": isISBN13, +} + +func isISBN(top interface{}, current interface{}, field interface{}, param string) bool { + return isISBN10(top, current, field, param) || isISBN13(top, current, field, param) +} + +func isISBN13(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 4), " ", "", 4) + + if !matchesRegex(iSBN13Regex, s) { + return false + } + + var checksum int32 + var i int32 + + factor := []int32{1, 3} + + for i = 0; i < 12; i++ { + checksum += factor[i%2] * int32(s[i]-'0') + } + + if (int32(s[12]-'0'))-((10-(checksum%10))%10) == 0 { + return true + } + + return false +} + +func isISBN10(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 3), " ", "", 3) + + if !matchesRegex(iSBN10Regex, s) { + return false + } + + var checksum int32 + var i int32 + + for i = 0; i < 9; i++ { + checksum += (i + 1) * int32(s[i]-'0') + } + + if s[9] == 'X' { + checksum += 10 * 10 + } else { + checksum += 10 * int32(s[9]-'0') + } + + if checksum%11 == 0 { + return true + } + + return false } func excludesRune(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 5282cf5..55d325d 100644 --- a/doc.go +++ b/doc.go @@ -362,6 +362,18 @@ Here is a list of the current built in validators: This validates that a string value does not contain the supplied rune value. (Usage: excludesrune=@) + isbn + This validates that a string value contains a valid isbn10 or isbn13 value. + (Usage: isbn) + + isbn10 + This validates that a string value contains a valid isbn10 value. + (Usage: isbn10) + + isbn13 + This validates that a string value contains a valid isbn13 value. + (Usage: isbn13) + Validator notes: regex diff --git a/regexes.go b/regexes.go index 31c1c68..984ffbd 100644 --- a/regexes.go +++ b/regexes.go @@ -15,6 +15,8 @@ const ( 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}])))\\.?$" base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" + iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" + iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" ) var ( @@ -30,6 +32,8 @@ var ( hslaRegex = regexp.MustCompile(hslaRegexString) emailRegex = regexp.MustCompile(emailRegexString) base64Regex = regexp.MustCompile(base64RegexString) + iSBN10Regex = regexp.MustCompile(iSBN10RegexString) + iSBN13Regex = regexp.MustCompile(iSBN13RegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index 7536527..dc0a1e3 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,104 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestISBNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn") { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) + } + } + } +} + +func TestISBN13Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3-8362-2119-5", false}, + {"01234567890ab", false}, + {"978 3 8362 2119 0", false}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn13") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn13") { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } + } +} + +func TestISBN10Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3423214121", false}, + {"978-3836221191", false}, + {"3-423-21412-1", false}, + {"3 423 21412 1", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn10") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn10") { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } + } +} + func TestExcludesRuneValidation(t *testing.T) { tests := []struct { From 7aa70841bcfc0a4b9ba8f0a7c182c72bef89878e Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 08:03:31 -0400 Subject: [PATCH 05/20] add uuid, uuid3, uuid4 and uuid5 validators + tests + documentation --- baked_in.go | 20 ++++++++ doc.go | 16 +++++++ regexes.go | 8 ++++ validator_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) diff --git a/baked_in.go b/baked_in.go index 50c0c19..da5933e 100644 --- a/baked_in.go +++ b/baked_in.go @@ -53,6 +53,26 @@ var BakedInValidators = map[string]Func{ "isbn": isISBN, "isbn10": isISBN10, "isbn13": isISBN13, + "uuid": isUUID, + "uuid3": isUUID3, + "uuid4": isUUID4, + "uuid5": isUUID5, +} + +func isUUID5(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID5Regex, field) +} + +func isUUID4(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID4Regex, field) +} + +func isUUID3(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID3Regex, field) +} + +func isUUID(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUIDRegex, field) } func isISBN(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 55d325d..5c8b44b 100644 --- a/doc.go +++ b/doc.go @@ -374,6 +374,22 @@ Here is a list of the current built in validators: This validates that a string value contains a valid isbn13 value. (Usage: isbn13) + uuid + This validates that a string value contains a valid UUID. + (Usage: uuid) + + uuid3 + This validates that a string value contains a valid version 3 UUID. + (Usage: uuid3) + + uuid4 + This validates that a string value contains a valid version 4 UUID. + (Usage: uuid4) + + uuid5 + This validates that a string value contains a valid version 5 UUID. + (Usage: uuid5) + Validator notes: regex diff --git a/regexes.go b/regexes.go index 984ffbd..d8c22cc 100644 --- a/regexes.go +++ b/regexes.go @@ -17,6 +17,10 @@ const ( base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" + uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" + uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" ) var ( @@ -34,6 +38,10 @@ var ( base64Regex = regexp.MustCompile(base64RegexString) iSBN10Regex = regexp.MustCompile(iSBN10RegexString) iSBN13Regex = regexp.MustCompile(iSBN13RegexString) + uUID3Regex = regexp.MustCompile(uUID3RegexString) + uUID4Regex = regexp.MustCompile(uUID4RegexString) + uUID5Regex = regexp.MustCompile(uUID5RegexString) + uUIDRegex = regexp.MustCompile(uUIDRegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index dc0a1e3..ba7eb5c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,124 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestUUID5Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"9c858901-8a57-4791-81fe-4c455b099bc9", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"987fbc97-4bed-5078-af07-9141ba07c9f3", true}, + {"987fbc97-4bed-5078-9f07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid5") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid5") { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } + } + } +} + +func TestUUID4Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-5078-af07-9141ba07c9f3", false}, + {"934859", false}, + {"57b73598-8764-4ad0-a76a-679bb6640eb1", true}, + {"625e63f3-58f5-40b7-83a1-a72ad31acffb", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid4") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid4") { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } + } +} + +func TestUUID3Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"412452646", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-4078-8f07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid3") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid3") { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } + } +} + +func TestUUIDValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3xxx", false}, + {"a987fbc94bed3078cf079141ba07c9f3", false}, + {"934859", false}, + {"987fbc9-4bed-3078-cf07a-9141ba07c9f3", false}, + {"aaaaaaaa-1111-1111-aaag-111111111111", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid") { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } + } +} + func TestISBNValidation(t *testing.T) { tests := []struct { param string From 35aff710e438d75e5667968d892818f0bae165db Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 09:02:26 -0400 Subject: [PATCH 06/20] added many new validator + tests + documentation: ascii printascii multibyte datauri latitude longitude ssn --- baked_in.go | 56 ++++++++++++ doc.go | 34 ++++++- regexes.go | 86 ++++++++++-------- validator_test.go | 220 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 37 deletions(-) diff --git a/baked_in.go b/baked_in.go index da5933e..22746ad 100644 --- a/baked_in.go +++ b/baked_in.go @@ -57,6 +57,62 @@ var BakedInValidators = map[string]Func{ "uuid3": isUUID3, "uuid4": isUUID4, "uuid5": isUUID5, + "ascii": isASCII, + "printascii": isPrintableASCII, + "multibyte": hasMultiByteCharacter, + "datauri": isDataURI, + "latitude": isLatitude, + "longitude": isLongitude, + "ssn": isSSN, +} + +func isSSN(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) != 11 { + return false + } + + return matchesRegex(sSNRegex, field) +} + +func isLongitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(longitudeRegex, field) +} + +func isLatitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(latitudeRegex, field) +} + +func isDataURI(top interface{}, current interface{}, field interface{}, param string) bool { + + uri := strings.SplitN(field.(string), ",", 2) + + if len(uri) != 2 { + return false + } + + if !matchesRegex(dataURIRegex, uri[0]) { + return false + } + + return isBase64(top, current, uri[1], param) +} + +func hasMultiByteCharacter(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) == 0 { + return true + } + + return matchesRegex(multibyteRegex, field) +} + +func isPrintableASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(printableASCIIRegex, field) +} + +func isASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(aSCIIRegex, field) } func isUUID5(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 5c8b44b..89142e0 100644 --- a/doc.go +++ b/doc.go @@ -168,7 +168,7 @@ Here is a list of the current built in validators: verify it has been assigned. omitempty - Allows conitional validation, for example if a field is not set with + Allows conditional validation, for example if a field is not set with a value (Determined by the required validator) then other validation such as min or max won't run, but if a value is set validation will run. (Usage: omitempty) @@ -390,6 +390,38 @@ Here is a list of the current built in validators: This validates that a string value contains a valid version 5 UUID. (Usage: uuid5) + ascii + This validates that a string value contains only ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: ascii) + + asciiprint + This validates that a string value contains only printable ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: asciiprint) + + multibyte + This validates that a string value contains one or more multibyte characters. + NOTE: if the string is blank, this validates as true. + (Usage: multibyte) + + datauri + This validates that a string value contains a valid DataURI. + NOTE: this will also validate that the data portion is valid base64 + (Usage: datauri) + + latitude + This validates that a string value contains a valid latitude. + (Usage: latitude) + + longitude + This validates that a string value contains a valid longitude. + (Usage: longitude) + + ssn + This validates that a string value contains a valid U.S. Social Security Number. + (Usage: ssn) + Validator notes: regex diff --git a/regexes.go b/regexes.go index d8c22cc..d3e8d80 100644 --- a/regexes.go +++ b/regexes.go @@ -3,45 +3,59 @@ package validator import "regexp" const ( - alphaRegexString = "^[a-zA-Z]+$" - alphaNumericRegexString = "^[a-zA-Z0-9]+$" - numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" - numberRegexString = "^[0-9]+$" - hexadecimalRegexString = "^[0-9a-fA-F]+$" - hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" - rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" - rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" - 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}])))\\.?$" - base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" - iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" - iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" - uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" - uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + alphaRegexString = "^[a-zA-Z]+$" + alphaNumericRegexString = "^[a-zA-Z0-9]+$" + numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" + numberRegexString = "^[0-9]+$" + hexadecimalRegexString = "^[0-9a-fA-F]+$" + hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" + rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + 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}])))\\.?$" + base64RegexString = "^(?:[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})$" + iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" + uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" + uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + aSCIIRegexString = "^[\x00-\x7F]*$" + printableASCIIRegexString = "^[\x20-\x7E]*$" + multibyteRegexString = "[^\x00-\x7F]" + dataURIRegexString = "^data:.+\\/(.+);base64$" + latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$" + longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$" + sSNRegexString = `^\d{3}[- ]?\d{2}[- ]?\d{4}$` ) var ( - alphaRegex = regexp.MustCompile(alphaRegexString) - alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) - numericRegex = regexp.MustCompile(numericRegexString) - numberRegex = regexp.MustCompile(numberRegexString) - hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) - hexcolorRegex = regexp.MustCompile(hexcolorRegexString) - rgbRegex = regexp.MustCompile(rgbRegexString) - rgbaRegex = regexp.MustCompile(rgbaRegexString) - hslRegex = regexp.MustCompile(hslRegexString) - hslaRegex = regexp.MustCompile(hslaRegexString) - emailRegex = regexp.MustCompile(emailRegexString) - base64Regex = regexp.MustCompile(base64RegexString) - iSBN10Regex = regexp.MustCompile(iSBN10RegexString) - iSBN13Regex = regexp.MustCompile(iSBN13RegexString) - uUID3Regex = regexp.MustCompile(uUID3RegexString) - uUID4Regex = regexp.MustCompile(uUID4RegexString) - uUID5Regex = regexp.MustCompile(uUID5RegexString) - uUIDRegex = regexp.MustCompile(uUIDRegexString) + alphaRegex = regexp.MustCompile(alphaRegexString) + alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) + numericRegex = regexp.MustCompile(numericRegexString) + numberRegex = regexp.MustCompile(numberRegexString) + hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) + hexcolorRegex = regexp.MustCompile(hexcolorRegexString) + rgbRegex = regexp.MustCompile(rgbRegexString) + rgbaRegex = regexp.MustCompile(rgbaRegexString) + hslRegex = regexp.MustCompile(hslRegexString) + hslaRegex = regexp.MustCompile(hslaRegexString) + emailRegex = regexp.MustCompile(emailRegexString) + base64Regex = regexp.MustCompile(base64RegexString) + iSBN10Regex = regexp.MustCompile(iSBN10RegexString) + iSBN13Regex = regexp.MustCompile(iSBN13RegexString) + uUID3Regex = regexp.MustCompile(uUID3RegexString) + uUID4Regex = regexp.MustCompile(uUID4RegexString) + uUID5Regex = regexp.MustCompile(uUID5RegexString) + uUIDRegex = regexp.MustCompile(uUIDRegexString) + aSCIIRegex = regexp.MustCompile(aSCIIRegexString) + printableASCIIRegex = regexp.MustCompile(printableASCIIRegexString) + multibyteRegex = regexp.MustCompile(multibyteRegexString) + dataURIRegex = regexp.MustCompile(dataURIRegexString) + latitudeRegex = regexp.MustCompile(latitudeRegexString) + longitudeRegex = regexp.MustCompile(longitudeRegexString) + sSNRegex = regexp.MustCompile(sSNRegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index ba7eb5c..c9da1e4 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,226 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestSSNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"00-90-8787", false}, + {"66690-76", false}, + {"191 60 2869", true}, + {"191-60-2869", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "ssn") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ssn") { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } + } +} + +func TestLongitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-180.000", true}, + {"180.1", false}, + {"+73.234", true}, + {"+382.3811", false}, + {"23.11111111", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "longitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "longitude") { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } + } +} + +func TestLatitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-90.000", true}, + {"+90", true}, + {"47.1231231", true}, + {"+99.9", false}, + {"108", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "latitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "latitude") { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } + } +} + +func TestDataURIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true}, + {"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true}, + {"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + {"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" + + "UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" + + "rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" + + "FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" + + "QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" + + "Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true}, + {"data:image/png;base64,12345", false}, + {"", false}, + {"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "datauri") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "datauri") { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } + } +} + +func TestMultibyteValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"abc", false}, + {"123", false}, + {"<>@;.-=", false}, + {"ひらがな・カタカナ、.漢字", true}, + {"あいうえお foobar", true}, + {"test@example.com", true}, + {"test@example.com", true}, + {"1234abcDExyz", true}, + {"カタカナ", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "multibyte") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "multibyte") { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } + } +} + +func TestPrintableASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"newline\n", false}, + {"\x19test\x7F", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "printascii") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "printascii") { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } + } +} + +func TestASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "ascii") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ascii") { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } + } +} + func TestUUID5Validation(t *testing.T) { tests := []struct { param string From 63a3b7e6ad8e6057986caebbc78f6a7439a9d9cf Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 09:38:48 -0400 Subject: [PATCH 07/20] add multibyte test case for blank string to maintain 100% test coverage --- validator_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/validator_test.go b/validator_test.go index c9da1e4..a2d5394 100644 --- a/validator_test.go +++ b/validator_test.go @@ -352,6 +352,7 @@ func TestMultibyteValidation(t *testing.T) { param string expected bool }{ + {"", true}, {"abc", false}, {"123", false}, {"<>@;.-=", false}, From e0bfa17b226425fcca3d08709cf3f4ab6059de60 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 20 Jun 2015 10:04:55 -0400 Subject: [PATCH 08/20] add initial dive logic --- validator.go | 68 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/validator.go b/validator.go index 5f0c18a..5e2196c 100644 --- a/validator.go +++ b/validator.go @@ -28,6 +28,8 @@ const ( structOnlyTag = "structonly" omitempty = "omitempty" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" + mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" ) @@ -64,8 +66,6 @@ func (p *pool) Borrow() *StructErrors { // Return returns a StructErrors to the pool. func (p *pool) Return(c *StructErrors) { - // c.Struct = "" - select { case p.pool <- c: default: @@ -135,15 +135,62 @@ func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} +// // SliceError contains a fields error for a single index within an array or slice +// // NOTE: library only checks the first dimension of the array so if you have a multidimensional +// // array [][]string that validations after the "dive" tag are applied to []string not each +// // string within it. It is not a dificulty with traversing the chain, but how to add validations +// // to what dimension of an array and even how to report on them in any meaningful fashion. +// type SliceError struct { +// Index uint64 +// Field string +// Tag string +// Kind reflect.Kind +// Type reflect.Type +// Param string +// Value interface{} +// } + +// // This is intended for use in development + debugging and not intended to be a production error message. +// // it also allows SliceError to be used as an Error interface +// func (e *SliceError) Error() string { +// return fmt.Sprintf(sliceErrMsg, e.Field, e.Index, e.Tag) +// } + +// // MapError contains a fields error for a single key within a map +// // NOTE: library only checks the first dimension of the array so if you have a multidimensional +// // array [][]string that validations after the "dive" tag are applied to []string not each +// // string within it. It is not a dificulty with traversing the chain, but how to add validations +// // to what dimension of an array and even how to report on them in any meaningful fashion. +// type MapError struct { +// Key interface{} +// Field string +// Tag string +// Kind reflect.Kind +// Type reflect.Type +// Param string +// Value interface{} +// } + +// // This is intended for use in development + debugging and not intended to be a production error message. +// // it also allows MapError to be used as an Error interface +// func (e *MapError) Error() string { +// return fmt.Sprintf(mapErrMsg, e.Field, e.Key, e.Tag) +// } + // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { - Field string - Tag string - Kind reflect.Kind - Type reflect.Type - Param string - Value interface{} + Field string + Tag string + Kind reflect.Kind + Type reflect.Type + Param string + Value interface{} + IsSliceOrArrayError bool + IsMapError bool + Key interface{} + Index uint64 + DiveErrors []*error // counld be FieldError, StructErrors } // This is intended for use in development + debugging and not intended to be a production error message. @@ -162,6 +209,11 @@ type StructErrors struct { // Struct Fields of type struct and their errors // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors + + IsSliceOrArrayError bool + IsMapError bool + Key interface{} + Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. From e0e86490bf9755a3870d5b80f19707471017bf3b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 20 Jun 2015 11:56:16 -0400 Subject: [PATCH 09/20] Fix Issue with nested struct as pointer being nil for #79 --- validator.go | 19 +++++++++ validator_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/validator.go b/validator.go index 5f0c18a..278dda9 100644 --- a/validator.go +++ b/validator.go @@ -27,6 +27,7 @@ const ( tagKeySeparator = "=" structOnlyTag = "structonly" omitempty = "omitempty" + required = "required" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" ) @@ -390,6 +391,24 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } + if valueField.Kind() == reflect.Ptr && valueField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + validationErrors.Errors[cField.name] = &FieldError{ + Field: cField.name, + Tag: required, + Value: valueField.Interface(), + } + + continue + } + } + if structErrors := v.structRecursive(top, valueField.Interface(), valueField.Interface()); structErrors != nil { validationErrors.StructErrors[cField.name] = structErrors // free up memory map no longer needed diff --git a/validator_test.go b/validator_test.go index a2d5394..84c4785 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,98 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestNilStructPointerValidation(t *testing.T) { + type Inner struct { + Data string + } + + type Outer struct { + Inner *Inner `validate:"omitempty"` + } + + inner := &Inner{ + Data: "test", + } + + outer := &Outer{ + Inner: inner, + } + + errs := validate.Struct(outer) + Equal(t, errs, nil) + + outer = &Outer{ + Inner: nil, + } + + errs = validate.Struct(outer) + Equal(t, errs, nil) + + type Inner2 struct { + Data string + } + + type Outer2 struct { + Inner2 *Inner2 `validate:"required"` + } + + inner2 := &Inner2{ + Data: "test", + } + + outer2 := &Outer2{ + Inner2: inner2, + } + + errs = validate.Struct(outer2) + Equal(t, errs, nil) + + outer2 = &Outer2{ + Inner2: nil, + } + + errs = validate.Struct(outer2) + NotEqual(t, errs, nil) + + type Inner3 struct { + Data string + } + + type Outer3 struct { + Inner3 *Inner3 + } + + inner3 := &Inner3{ + Data: "test", + } + + outer3 := &Outer3{ + Inner3: inner3, + } + + errs = validate.Struct(outer3) + Equal(t, errs, nil) + + type Inner4 struct { + Data string + } + + type Outer4 struct { + Inner4 *Inner4 `validate:"-"` + } + + inner4 := &Inner4{ + Data: "test", + } + + outer4 := &Outer4{ + Inner4: inner4, + } + + errs = validate.Struct(outer4) + Equal(t, errs, nil) +} + func TestSSNValidation(t *testing.T) { tests := []struct { param string @@ -1100,7 +1192,7 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: nil, } - errs := validate.Struct(outer).Flatten() + errs := validate.Struct(outer) NotEqual(t, errs, nil) inner := &Inner{ @@ -1111,9 +1203,8 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: inner, } - errs = validate.Struct(outer).Flatten() - NotEqual(t, errs, nil) - Equal(t, len(errs), 0) + errs = validate.Struct(outer) + Equal(t, errs, nil) } func TestGtField(t *testing.T) { From 4afdc19aef655b5c36a3089d548ac25e9a9b6b64 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 25 Jun 2015 09:15:46 -0400 Subject: [PATCH 10/20] Finish initial array traversal logic for #78 --- validator.go | 309 +++++++++++++++++++++++++++++++++++++++++----- validator_test.go | 32 +++++ 2 files changed, 313 insertions(+), 28 deletions(-) diff --git a/validator.go b/validator.go index ba293f0..049be60 100644 --- a/validator.go +++ b/validator.go @@ -32,6 +32,8 @@ const ( sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" + diveTag = "dive" + diveSplit = "," + diveTag ) var structPool *pool @@ -80,13 +82,23 @@ type cachedTags struct { } type cachedField struct { - index int - name string - tags []*cachedTags - tag string - kind reflect.Kind - typ reflect.Type - isTime bool + index int + name string + tags []*cachedTags + tag string + kind reflect.Kind + typ reflect.Type + isTime bool + isSliceOrArray bool + isMap bool + isTimeSubtype bool + sliceSubtype reflect.Type + mapSubtype reflect.Type + sliceSubKind reflect.Kind + mapSubKind reflect.Kind + // DiveMaxDepth uint64 // zero means no depth + // DiveTags map[uint64]string // map of dive depth and associated tag as string] + diveTag string } type cachedStruct struct { @@ -181,17 +193,19 @@ var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { - Field string - Tag string - Kind reflect.Kind - Type reflect.Type - Param string - Value interface{} - IsSliceOrArrayError bool - IsMapError bool - Key interface{} - Index uint64 - DiveErrors []*error // counld be FieldError, StructErrors + Field string + Tag string + Kind reflect.Kind + Type reflect.Type + Param string + Value interface{} + isPlaceholderErr bool + IsSliceOrArray bool + IsMap bool + // Key interface{} + // Index int + SliceOrArrayErrs []error // counld be FieldError, StructErrors + MapErrs map[interface{}]error // counld be FieldError, StructErrors } // This is intended for use in development + debugging and not intended to be a production error message. @@ -211,10 +225,12 @@ type StructErrors struct { // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors - IsSliceOrArrayError bool - IsMapError bool - Key interface{} - Index uint64 + // Index int + // Key interface{} + // IsSliceOrArrayError bool + // IsMapError bool + // Key interface{} + // Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. @@ -393,7 +409,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter typeField = structType.Field(i) - cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName)} + cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: valueField.Type() == reflect.TypeOf(time.Time{})} if cField.tag == noValidationTag { cs.children-- @@ -426,9 +442,9 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } - if cField.isTime || valueField.Type() == reflect.TypeOf(time.Time{}) { + if cField.isTime { - cField.isTime = true + // cField.isTime = true if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError @@ -468,8 +484,31 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } } - default: + case reflect.Slice, reflect.Array: + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.sliceSubKind = cField.sliceSubtype.Kind() + + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + + case reflect.Map: + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.mapSubKind = cField.mapSubtype.Kind() + + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + default: if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference @@ -506,6 +545,8 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f var cField *cachedField var isCached bool + // var isInDive bool + var valueField reflect.Value // This is a double check if coming from validate.Struct but need to be here in case function is called directly if tag == noValidationTag { @@ -516,8 +557,10 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } + valueField = reflect.ValueOf(f) + if cacheField == nil { - valueField := reflect.ValueOf(f) + // valueField = reflect.ValueOf(f) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { valueField = valueField.Elem() @@ -525,6 +568,19 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } cField = &cachedField{name: name, kind: valueField.Kind(), tag: tag, typ: valueField.Type()} + + switch cField.kind { + case reflect.Slice, reflect.Array: + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.sliceSubKind = cField.sliceSubtype.Kind() + case reflect.Map: + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.mapSubKind = cField.mapSubtype.Kind() + } } else { cField = cacheField } @@ -546,11 +602,37 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if !isCached { - for _, t := range strings.Split(tag, tagSeparator) { + for k, t := range strings.Split(tag, tagSeparator) { + + if t == diveTag { + + if k == 0 { + cField.diveTag = tag[4:] + } else { + cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] + } + + break + } orVals := strings.Split(t, orSeparator) cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} + + // if isInDive { + + // s, ok := cField.DiveTags[cField.DiveMaxDepth] + + // if ok { + // cField.DiveTags[cField.DiveMaxDepth] = cField.DiveTags[cField.DiveMaxDepth] + tagSeparator + tag + // } else { + // cField.DiveTags[cField.DiveMaxDepth] = tag + // } + + // continue + + // } else { cField.tags = append(cField.tags, cTag) + // } for i, val := range orVals { vals := strings.SplitN(val, tagKeySeparator, 2) @@ -614,9 +696,180 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } + if len(cField.diveTag) > 0 { + + if cField.isSliceOrArray { + + if errs := v.traverseSliceOrArray(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + isPlaceholderErr: true, + IsSliceOrArray: true, + // Index: i, + SliceOrArrayErrs: errs, + } + } + // return if error here + } else if cField.isMap { + // return if error here + } else { + // throw error, if not a slice or map then should not have gotten here + } + + // dive tags need to be passed to traverse + // traverse needs to call a SliceOrArray recursive function to meet depth requirements + + // for depth, diveTag := range cField.DiveTags { + + // // error returned should be added to SliceOrArrayErrs + // if errs := v.traverseSliceOrArrayField(val, current, depth, currentDepth+1, diveTag, cField, valueField); len(errs) > 0 { + // // result := &FieldError{ + // // Field: cField.name, + // // Kind: cField.kind, + // // Type: cField.typ, + // // Value: valueField.Index(i).Interface(), + // // isPlaceholderErr: true, + // // IsSliceOrArray:true, + // // Index:i, + // // SliceOrArrayErrs: + // // } + // } + + // for _, tag := range diveTag { + + // fmt.Println("Depth:", depth, " Tag:", tag, " SliceType:", cField.SliceSubtype, " MapType:", cField.MapSubtype, " Kind:", cField.kind) + + // } + // } + } + return nil } +func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) []error { + + errs := make([]error, 0) + + for i := 0; i < valueField.Len(); i++ { + + idxField := valueField.Index(i) + + switch cField.sliceSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + errs = append(errs, fieldError) + } + + continue + } + + if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + errs = append(errs, &FieldError{ + Field: cField.name, + Tag: required, + Value: idxField.Interface(), + Kind: reflect.Ptr, + Type: cField.sliceSubtype, + }) + + continue + } + } + + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs = append(errs, structErrors) + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + errs = append(errs, fieldError) + } + } + } + + return errs +} + +// func (v *Validate) traverseSliceOrArrayField(val interface{}, current interface{}, depth uint64, currentDepth uint64, diveTags []*cachedTags, cField *cachedField, valueField reflect.Value) []error { + +// for i := 0; i < valueField.Len(); i++ { + +// if depth != currentDepth { + +// switch cField.SliceSubKind { +// case reflect.Slice, reflect.Array: +// return v.fieldWithNameAndValue(val, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField, currentDepth) + +// // type FieldError struct { +// // Field string +// // Tag string +// // Kind reflect.Kind +// // Type reflect.Type +// // Param string +// // Value interface{} +// // HasErr bool +// // IsSliceOrArray bool +// // IsMap bool +// // Key interface{} +// // Index uint64 +// // SliceOrArrayErrs []*error // counld be FieldError, StructErrors +// // MapErrs map[interface{}]*error // counld be FieldError, StructErrors +// // } + +// // result := &FieldError{ +// // Field: cField.name, +// // Kind: cField.kind, +// // Type: cField.typ, +// // Value: valueField.Index(i).Interface(), +// // isPlaceholderErr: true, +// // IsSliceOrArray:true, +// // Index:i, +// // SliceOrArrayErrs: +// // } +// // validationErrors.Errors[fieldError.Field] = fieldError +// // // free up memory reference +// // fieldError = nil +// // } +// default: +// panic("attempting to dive deeper, but Kind is not a Slice nor Array") +// } +// } + +// // switch cField.SliceSubKind { +// // case reflect.Struct, reflect.Interface: +// // // need to check if required tag and or omitempty just like in struct recirsive +// // if cField.isTimeSubtype || valueField.Type() == reflect.TypeOf(time.Time{}) { + +// // if fieldError := v.fieldWithNameAndValue(top, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField); fieldError != nil { +// // validationErrors.Errors[fieldError.Field] = fieldError +// // // free up memory reference +// // fieldError = nil +// // } +// // } +// // } +// fmt.Println(valueField.Index(i)) +// } +// // fmt.Println(v) +// // for _, item := range arr { + +// // } +// return nil +// } + func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { // OK to continue because we checked it's existance before getting into this loop diff --git a/validator_test.go b/validator_test.go index 84c4785..804babf 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,38 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestArrayDiveValidation(t *testing.T) { + + type Test struct { + Errs []string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: []string{"ok", "", "ok"}, + } + + errs := validate.Struct(test) + + fmt.Println(errs) + + // type TestMap struct { + // Errs *map[int]string `validate:"gt=0,dive,required"` + // } + + // m := map[int]string{} + // m[1] = "ok" + // m[2] = "" + // m[3] = "ok" + + // testMap := &TestMap{ + // Errs: &m, + // } + + // errs = validate.Struct(testMap) + + // fmt.Println(errs) +} + func TestNilStructPointerValidation(t *testing.T) { type Inner struct { Data string From d019d02290b692285b4104a5f43daead30c57f8b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 25 Jun 2015 17:24:25 -0400 Subject: [PATCH 11/20] Add some initial validation change slice errors variable type to map[int]error to allow tracking of index of the error i the array for #78 --- validator.go | 21 +++++++++++---------- validator_test.go | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/validator.go b/validator.go index 049be60..e6e0cb9 100644 --- a/validator.go +++ b/validator.go @@ -199,12 +199,13 @@ type FieldError struct { Type reflect.Type Param string Value interface{} - isPlaceholderErr bool + IsPlaceholderErr bool IsSliceOrArray bool IsMap bool // Key interface{} // Index int - SliceOrArrayErrs []error // counld be FieldError, StructErrors + // SliceOrArrayErrs []error // counld be FieldError, StructErrors + SliceOrArrayErrs map[int]error // counld be FieldError, StructErrors MapErrs map[interface{}]error // counld be FieldError, StructErrors } @@ -707,7 +708,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f Kind: cField.kind, Type: cField.typ, Value: f, - isPlaceholderErr: true, + IsPlaceholderErr: true, IsSliceOrArray: true, // Index: i, SliceOrArrayErrs: errs, @@ -750,9 +751,9 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } -func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) []error { +func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { - errs := make([]error, 0) + errs := make(map[int]error, 0) for i := 0; i < valueField.Len(); i++ { @@ -764,7 +765,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { - errs = append(errs, fieldError) + errs[i] = fieldError } continue @@ -778,25 +779,25 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if strings.Contains(cField.tag, required) { - errs = append(errs, &FieldError{ + errs[i] = &FieldError{ Field: cField.name, Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, Type: cField.sliceSubtype, - }) + } continue } } if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { - errs = append(errs, structErrors) + errs[i] = structErrors } default: if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { - errs = append(errs, fieldError) + errs[i] = fieldError } } } diff --git a/validator_test.go b/validator_test.go index 804babf..a32d99e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -237,8 +237,42 @@ func TestArrayDiveValidation(t *testing.T) { } errs := validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs") + + test = &Test{ + Errs: []string{"ok", "ok", ""}, + } + + errs = validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs") - fmt.Println(errs) + fmt.Println(errs.Errors["Errs"].IsPlaceholderErr) // type TestMap struct { // Errs *map[int]string `validate:"gt=0,dive,required"` From 6eded1f81732b10f5aa8c1249e7561925b1df2f1 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:28:15 -0400 Subject: [PATCH 12/20] correct error output and index out of order error for #78 --- validator.go | 228 +++++++++++----------------------------------- validator_test.go | 129 ++++++++++++++++---------- 2 files changed, 135 insertions(+), 222 deletions(-) diff --git a/validator.go b/validator.go index e6e0cb9..3f32f4d 100644 --- a/validator.go +++ b/validator.go @@ -29,11 +29,12 @@ const ( omitempty = "omitempty" required = "required" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" - mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" failed with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed with key \"%v\" failed with error(s): %s" structErrMsg = "Struct:%s\n" diveTag = "dive" diveSplit = "," + diveTag + indexFieldName = "%s[%d]" ) var structPool *pool @@ -96,9 +97,7 @@ type cachedField struct { mapSubtype reflect.Type sliceSubKind reflect.Kind mapSubKind reflect.Kind - // DiveMaxDepth uint64 // zero means no depth - // DiveTags map[uint64]string // map of dive depth and associated tag as string] - diveTag string + diveTag string } type cachedStruct struct { @@ -148,48 +147,6 @@ func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} -// // SliceError contains a fields error for a single index within an array or slice -// // NOTE: library only checks the first dimension of the array so if you have a multidimensional -// // array [][]string that validations after the "dive" tag are applied to []string not each -// // string within it. It is not a dificulty with traversing the chain, but how to add validations -// // to what dimension of an array and even how to report on them in any meaningful fashion. -// type SliceError struct { -// Index uint64 -// Field string -// Tag string -// Kind reflect.Kind -// Type reflect.Type -// Param string -// Value interface{} -// } - -// // This is intended for use in development + debugging and not intended to be a production error message. -// // it also allows SliceError to be used as an Error interface -// func (e *SliceError) Error() string { -// return fmt.Sprintf(sliceErrMsg, e.Field, e.Index, e.Tag) -// } - -// // MapError contains a fields error for a single key within a map -// // NOTE: library only checks the first dimension of the array so if you have a multidimensional -// // array [][]string that validations after the "dive" tag are applied to []string not each -// // string within it. It is not a dificulty with traversing the chain, but how to add validations -// // to what dimension of an array and even how to report on them in any meaningful fashion. -// type MapError struct { -// Key interface{} -// Field string -// Tag string -// Kind reflect.Kind -// Type reflect.Type -// Param string -// Value interface{} -// } - -// // This is intended for use in development + debugging and not intended to be a production error message. -// // it also allows MapError to be used as an Error interface -// func (e *MapError) Error() string { -// return fmt.Sprintf(mapErrMsg, e.Field, e.Key, e.Tag) -// } - // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { @@ -202,9 +159,6 @@ type FieldError struct { IsPlaceholderErr bool IsSliceOrArray bool IsMap bool - // Key interface{} - // Index int - // SliceOrArrayErrs []error // counld be FieldError, StructErrors SliceOrArrayErrs map[int]error // counld be FieldError, StructErrors MapErrs map[interface{}]error // counld be FieldError, StructErrors } @@ -212,6 +166,40 @@ type FieldError struct { // This is intended for use in development + debugging and not intended to be a production error message. // it also allows FieldError to be used as an Error interface func (e *FieldError) Error() string { + + if e.IsPlaceholderErr { + + buff := bytes.NewBufferString("") + + if e.IsSliceOrArray { + + for i, err := range e.SliceOrArrayErrs { + + if i != 0 { + buff.WriteString("\n") + } + + buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, i, err)) + } + + } else if e.IsMap { + + var i uint64 + + for key, err := range e.MapErrs { + + if i != 0 { + buff.WriteString("\n") + } + + buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, err)) + i++ + } + } + + return buff.String() + } + return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } @@ -225,13 +213,6 @@ type StructErrors struct { // Struct Fields of type struct and their errors // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors - - // Index int - // Key interface{} - // IsSliceOrArrayError bool - // IsMapError bool - // Key interface{} - // Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. @@ -241,11 +222,19 @@ func (e *StructErrors) Error() string { for _, err := range e.Errors { buff.WriteString(err.Error()) - buff.WriteString("\n") } + var i uint64 + for _, err := range e.StructErrors { + + if i != 0 { + buff.WriteString("\n") + } + buff.WriteString(err.Error()) + + i++ } return buff.String() @@ -445,8 +434,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter if cField.isTime { - // cField.isTime = true - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference @@ -532,13 +519,11 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter // Field allows validation of a single field, still using tag style validation to check multiple errors func (v *Validate) Field(f interface{}, tag string) *FieldError { - return v.FieldWithValue(nil, f, tag) } // FieldWithValue allows validation of a single field, possibly even against another fields value, still using tag style validation to check multiple errors func (v *Validate) FieldWithValue(val interface{}, f interface{}, tag string) *FieldError { - return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil) } @@ -546,7 +531,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f var cField *cachedField var isCached bool - // var isInDive bool var valueField reflect.Value // This is a double check if coming from validate.Struct but need to be here in case function is called directly @@ -561,7 +545,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f valueField = reflect.ValueOf(f) if cacheField == nil { - // valueField = reflect.ValueOf(f) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { valueField = valueField.Elem() @@ -608,7 +591,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if t == diveTag { if k == 0 { - cField.diveTag = tag[4:] + cField.diveTag = tag[5:] } else { cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] } @@ -618,22 +601,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f orVals := strings.Split(t, orSeparator) cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} - - // if isInDive { - - // s, ok := cField.DiveTags[cField.DiveMaxDepth] - - // if ok { - // cField.DiveTags[cField.DiveMaxDepth] = cField.DiveTags[cField.DiveMaxDepth] + tagSeparator + tag - // } else { - // cField.DiveTags[cField.DiveMaxDepth] = tag - // } - - // continue - - // } else { cField.tags = append(cField.tags, cTag) - // } for i, val := range orVals { vals := strings.SplitN(val, tagKeySeparator, 2) @@ -710,42 +678,16 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f Value: f, IsPlaceholderErr: true, IsSliceOrArray: true, - // Index: i, SliceOrArrayErrs: errs, } } - // return if error here + } else if cField.isMap { // return if error here } else { // throw error, if not a slice or map then should not have gotten here + panic("dive error! can't dive on a non slice or map") } - - // dive tags need to be passed to traverse - // traverse needs to call a SliceOrArray recursive function to meet depth requirements - - // for depth, diveTag := range cField.DiveTags { - - // // error returned should be added to SliceOrArrayErrs - // if errs := v.traverseSliceOrArrayField(val, current, depth, currentDepth+1, diveTag, cField, valueField); len(errs) > 0 { - // // result := &FieldError{ - // // Field: cField.name, - // // Kind: cField.kind, - // // Type: cField.typ, - // // Value: valueField.Index(i).Interface(), - // // isPlaceholderErr: true, - // // IsSliceOrArray:true, - // // Index:i, - // // SliceOrArrayErrs: - // // } - // } - - // for _, tag := range diveTag { - - // fmt.Println("Depth:", depth, " Tag:", tag, " SliceType:", cField.SliceSubtype, " MapType:", cField.MapSubtype, " Kind:", cField.kind) - - // } - // } } return nil @@ -753,7 +695,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { - errs := make(map[int]error, 0) + errs := map[int]error{} for i := 0; i < valueField.Len(); i++ { @@ -796,7 +738,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va } default: - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(indexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } } @@ -805,72 +747,6 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va return errs } -// func (v *Validate) traverseSliceOrArrayField(val interface{}, current interface{}, depth uint64, currentDepth uint64, diveTags []*cachedTags, cField *cachedField, valueField reflect.Value) []error { - -// for i := 0; i < valueField.Len(); i++ { - -// if depth != currentDepth { - -// switch cField.SliceSubKind { -// case reflect.Slice, reflect.Array: -// return v.fieldWithNameAndValue(val, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField, currentDepth) - -// // type FieldError struct { -// // Field string -// // Tag string -// // Kind reflect.Kind -// // Type reflect.Type -// // Param string -// // Value interface{} -// // HasErr bool -// // IsSliceOrArray bool -// // IsMap bool -// // Key interface{} -// // Index uint64 -// // SliceOrArrayErrs []*error // counld be FieldError, StructErrors -// // MapErrs map[interface{}]*error // counld be FieldError, StructErrors -// // } - -// // result := &FieldError{ -// // Field: cField.name, -// // Kind: cField.kind, -// // Type: cField.typ, -// // Value: valueField.Index(i).Interface(), -// // isPlaceholderErr: true, -// // IsSliceOrArray:true, -// // Index:i, -// // SliceOrArrayErrs: -// // } -// // validationErrors.Errors[fieldError.Field] = fieldError -// // // free up memory reference -// // fieldError = nil -// // } -// default: -// panic("attempting to dive deeper, but Kind is not a Slice nor Array") -// } -// } - -// // switch cField.SliceSubKind { -// // case reflect.Struct, reflect.Interface: -// // // need to check if required tag and or omitempty just like in struct recirsive -// // if cField.isTimeSubtype || valueField.Type() == reflect.TypeOf(time.Time{}) { - -// // if fieldError := v.fieldWithNameAndValue(top, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField); fieldError != nil { -// // validationErrors.Errors[fieldError.Field] = fieldError -// // // free up memory reference -// // fieldError = nil -// // } -// // } -// // } -// fmt.Println(valueField.Index(i)) -// } -// // fmt.Println(v) -// // for _, item := range arr { - -// // } -// return nil -// } - func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { // OK to continue because we checked it's existance before getting into this loop diff --git a/validator_test.go b/validator_test.go index a32d99e..ca60acc 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,70 +226,107 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestMapDiveValidation(t *testing.T) { +} + func TestArrayDiveValidation(t *testing.T) { - type Test struct { - Errs []string `validate:"gt=0,dive,required"` - } + // type Test struct { + // Errs []string `validate:"gt=0,dive,required"` + // } - test := &Test{ - Errs: []string{"ok", "", "ok"}, - } + // test := &Test{ + // Errs: []string{"ok", "", "ok"}, + // } - errs := validate.Struct(test) - NotEqual(t, errs, nil) - Equal(t, len(errs.Errors), 1) + // errs := validate.Struct(test) + // NotEqual(t, errs, nil) + // Equal(t, len(errs.Errors), 1) - fieldErr, ok := errs.Errors["Errs"] - Equal(t, ok, true) - Equal(t, fieldErr.IsPlaceholderErr, true) - Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + // fieldErr, ok := errs.Errors["Errs"] + // Equal(t, ok, true) + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) - Equal(t, ok, true) - Equal(t, innerErr.Tag, required) - Equal(t, innerErr.IsPlaceholderErr, false) - Equal(t, innerErr.Field, "Errs") + // innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + // Equal(t, ok, true) + // Equal(t, innerErr.Tag, required) + // Equal(t, innerErr.IsPlaceholderErr, false) + // Equal(t, innerErr.Field, "Errs") + + // test = &Test{ + // Errs: []string{"ok", "ok", ""}, + // } + + // errs = validate.Struct(test) + // NotEqual(t, errs, nil) + // Equal(t, len(errs.Errors), 1) - test = &Test{ - Errs: []string{"ok", "ok", ""}, + // fieldErr, ok = errs.Errors["Errs"] + // Equal(t, ok, true) + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + // innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + // Equal(t, ok, true) + // Equal(t, innerErr.Tag, required) + // Equal(t, innerErr.IsPlaceholderErr, false) + // Equal(t, innerErr.Field, "Errs") + + type TestMultiDimensional struct { + Errs [][]string `validate:"gt=0,dive,dive,required"` } - errs = validate.Struct(test) + var errArray [][]string + + errArray = append(errArray, []string{"ok", "", ""}) + errArray = append(errArray, []string{"ok", "", ""}) + // fmt.Println(len(errArray)) + // errArray = append(errArray, []string{"", "ok", "ok"}) + // errArray = append(errArray, []string{"", "", "ok"}) + // errArray = append(errArray, []string{"", "", "ok"}) + + tm := &TestMultiDimensional{ + Errs: errArray, + } + + errs := validate.Struct(tm) + fmt.Println(errs) + // validate.Struct(tm) + + // fmt.Printf("%#v\n", errs.Errors["Errs"].SliceOrArrayErrs) + NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) - fieldErr, ok = errs.Errors["Errs"] + fieldErr, ok := errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) - innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + sliceError1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) Equal(t, ok, true) - Equal(t, innerErr.Tag, required) - Equal(t, innerErr.IsPlaceholderErr, false) - Equal(t, innerErr.Field, "Errs") + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) - fmt.Println(errs.Errors["Errs"].IsPlaceholderErr) - - // type TestMap struct { - // Errs *map[int]string `validate:"gt=0,dive,required"` - // } - - // m := map[int]string{} - // m[1] = "ok" - // m[2] = "" - // m[3] = "ok" - - // testMap := &TestMap{ - // Errs: &m, - // } - - // errs = validate.Struct(testMap) - - // fmt.Println(errs) + innerSliceError1, ok := sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.Tag, required) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + // fmt.Println(fieldErr.SliceOrArrayErrs) + + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + // fmt.Println(fieldErr.SliceOrArrayErrs) + // fmt.Println(len(fieldErr.SliceOrArrayErrs)) } func TestNilStructPointerValidation(t *testing.T) { From 1ba858eec1230a307d29c328460d2e567f6a9a08 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:38:28 -0400 Subject: [PATCH 13/20] correct FieldError error printing idea issue for #78 --- validator.go | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/validator.go b/validator.go index 3f32f4d..74b634c 100644 --- a/validator.go +++ b/validator.go @@ -173,27 +173,14 @@ func (e *FieldError) Error() string { if e.IsSliceOrArray { - for i, err := range e.SliceOrArrayErrs { - - if i != 0 { - buff.WriteString("\n") - } - - buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, i, err)) + for j, err := range e.SliceOrArrayErrs { + buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, j, "\n"+err.Error())) } } else if e.IsMap { - var i uint64 - for key, err := range e.MapErrs { - - if i != 0 { - buff.WriteString("\n") - } - - buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, err)) - i++ + buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, "\n"+err.Error())) } } @@ -233,7 +220,6 @@ func (e *StructErrors) Error() string { } buff.WriteString(err.Error()) - i++ } From 689d3e9989e4598fa6a159a145a9d5c9dd9f65fe Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:47:12 -0400 Subject: [PATCH 14/20] finalized array error handling for #78 --- validator.go | 11 +---- validator_test.go | 106 ++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/validator.go b/validator.go index 74b634c..36918dc 100644 --- a/validator.go +++ b/validator.go @@ -209,21 +209,14 @@ func (e *StructErrors) Error() string { for _, err := range e.Errors { buff.WriteString(err.Error()) + buff.WriteString("\n") } - var i uint64 - for _, err := range e.StructErrors { - - if i != 0 { - buff.WriteString("\n") - } - buff.WriteString(err.Error()) - i++ } - return buff.String() + return strings.TrimSpace(buff.String()) } // Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name diff --git a/validator_test.go b/validator_test.go index ca60acc..970d5dd 100644 --- a/validator_test.go +++ b/validator_test.go @@ -231,49 +231,49 @@ func TestMapDiveValidation(t *testing.T) { func TestArrayDiveValidation(t *testing.T) { - // type Test struct { - // Errs []string `validate:"gt=0,dive,required"` - // } - - // test := &Test{ - // Errs: []string{"ok", "", "ok"}, - // } - - // errs := validate.Struct(test) - // NotEqual(t, errs, nil) - // Equal(t, len(errs.Errors), 1) - - // fieldErr, ok := errs.Errors["Errs"] - // Equal(t, ok, true) - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - - // innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) - // Equal(t, ok, true) - // Equal(t, innerErr.Tag, required) - // Equal(t, innerErr.IsPlaceholderErr, false) - // Equal(t, innerErr.Field, "Errs") - - // test = &Test{ - // Errs: []string{"ok", "ok", ""}, - // } - - // errs = validate.Struct(test) - // NotEqual(t, errs, nil) - // Equal(t, len(errs.Errors), 1) - - // fieldErr, ok = errs.Errors["Errs"] - // Equal(t, ok, true) - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - - // innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) - // Equal(t, ok, true) - // Equal(t, innerErr.Tag, required) - // Equal(t, innerErr.IsPlaceholderErr, false) - // Equal(t, innerErr.Field, "Errs") + type Test struct { + Errs []string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: []string{"ok", "", "ok"}, + } + + errs := validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[1]") + + test = &Test{ + Errs: []string{"ok", "ok", ""}, + } + + errs = validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[2]") type TestMultiDimensional struct { Errs [][]string `validate:"gt=0,dive,dive,required"` @@ -283,25 +283,17 @@ func TestArrayDiveValidation(t *testing.T) { errArray = append(errArray, []string{"ok", "", ""}) errArray = append(errArray, []string{"ok", "", ""}) - // fmt.Println(len(errArray)) - // errArray = append(errArray, []string{"", "ok", "ok"}) - // errArray = append(errArray, []string{"", "", "ok"}) - // errArray = append(errArray, []string{"", "", "ok"}) tm := &TestMultiDimensional{ Errs: errArray, } - errs := validate.Struct(tm) - fmt.Println(errs) - // validate.Struct(tm) - - // fmt.Printf("%#v\n", errs.Errors["Errs"].SliceOrArrayErrs) + errs = validate.Struct(tm) NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) - fieldErr, ok := errs.Errors["Errs"] + fieldErr, ok = errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) @@ -319,14 +311,6 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.Tag, required) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - // fmt.Println(fieldErr.SliceOrArrayErrs) - - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 3) - - // fmt.Println(fieldErr.SliceOrArrayErrs) - // fmt.Println(len(fieldErr.SliceOrArrayErrs)) } func TestNilStructPointerValidation(t *testing.T) { From a0f6d14ada749dca7b7cb5c807b69dc9d3a704b5 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 08:41:37 -0400 Subject: [PATCH 15/20] add more tests correct pointer issue is traverseArray for #78 --- validator.go | 23 +++++++++++--- validator_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/validator.go b/validator.go index 36918dc..9b1c3ea 100644 --- a/validator.go +++ b/validator.go @@ -97,6 +97,7 @@ type cachedField struct { mapSubtype reflect.Type sliceSubKind reflect.Kind mapSubKind reflect.Kind + dive bool diveTag string } @@ -174,6 +175,7 @@ func (e *FieldError) Error() string { if e.IsSliceOrArray { for j, err := range e.SliceOrArrayErrs { + buff.WriteString("\n") buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, j, "\n"+err.Error())) } @@ -184,7 +186,7 @@ func (e *FieldError) Error() string { } } - return buff.String() + return strings.TrimSpace(buff.String()) } return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) @@ -553,6 +555,8 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { + + fmt.Println(cField.typ) panic("Invalid field passed to ValidateFieldWithTag") } } @@ -569,8 +573,14 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if t == diveTag { + cField.dive = true + if k == 0 { - cField.diveTag = tag[5:] + if len(tag) == 4 { + cField.diveTag = "" + } else { + cField.diveTag = tag[5:] + } } else { cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] } @@ -644,7 +654,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } - if len(cField.diveTag) > 0 { + if cField.dive { if cField.isSliceOrArray { @@ -680,12 +690,17 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va idxField := valueField.Index(i) + if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.sliceSubKind = idxField.Kind() + } + switch cField.sliceSubKind { case reflect.Struct, reflect.Interface: if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { errs[i] = fieldError } diff --git a/validator_test.go b/validator_test.go index 970d5dd..2e98403 100644 --- a/validator_test.go +++ b/validator_test.go @@ -289,7 +289,6 @@ func TestArrayDiveValidation(t *testing.T) { } errs = validate.Struct(tm) - NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) @@ -311,6 +310,86 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.Tag, required) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + + type Inner struct { + Name string `validate:"required"` + } + + type TestMultiDimensionalStructs struct { + Errs [][]Inner `validate:"gt=0,dive,dive"` + } + + var errStructArray [][]Inner + + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + + tms := &TestMultiDimensionalStructs{ + Errs: errStructArray, + } + + errs = validate.Struct(tms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok := sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 := innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr struct { + Errs [][]*Inner `validate:"gt=0,dive,dive"` + } + + var errStructPtrArray [][]*Inner + + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + + tmsp := &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs = validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) } func TestNilStructPointerValidation(t *testing.T) { From 98f4165fae5722c9cd1621bb55c1e6f52f85e194 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 09:57:02 -0400 Subject: [PATCH 16/20] added time test fix issue with time.Time data type validation --- validator.go | 14 ++-- validator_test.go | 165 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 9 deletions(-) diff --git a/validator.go b/validator.go index 9b1c3ea..36fd3ff 100644 --- a/validator.go +++ b/validator.go @@ -380,7 +380,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter typeField = structType.Field(i) - cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: valueField.Type() == reflect.TypeOf(time.Time{})} + cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: (valueField.Type() == reflect.TypeOf(time.Time{}) || valueField.Type() == reflect.TypeOf(&time.Time{}))} if cField.tag == noValidationTag { cs.children-- @@ -538,12 +538,12 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Slice, reflect.Array: cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() case reflect.Map: cField.isMap = true cField.mapSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) cField.mapSubKind = cField.mapSubtype.Kind() } } else { @@ -555,8 +555,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { - - fmt.Println(cField.typ) panic("Invalid field passed to ValidateFieldWithTag") } } @@ -698,7 +696,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va switch cField.sliceSubKind { case reflect.Struct, reflect.Interface: - if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { + if cField.isTimeSubtype { if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { errs[i] = fieldError @@ -722,9 +720,9 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va Kind: reflect.Ptr, Type: cField.sliceSubtype, } - - continue } + + continue } if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { diff --git a/validator_test.go b/validator_test.go index 2e98403..a1086ab 100644 --- a/validator_test.go +++ b/validator_test.go @@ -361,6 +361,7 @@ func TestArrayDiveValidation(t *testing.T) { errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) tmsp := &TestMultiDimensionalStructsPtr{ Errs: errStructPtrArray, @@ -374,7 +375,46 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr2 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,required"` + } + + var errStructPtr2Array [][]*Inner + + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp2 := &TestMultiDimensionalStructsPtr2{ + Errs: errStructPtr2Array, + } + + errs = validate.Struct(tmsp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) Equal(t, ok, true) @@ -390,6 +430,129 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerInnersliceError1.IsPlaceholderErr, false) Equal(t, innerInnersliceError1.IsSliceOrArray, false) Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr3 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,omitempty"` + } + + var errStructPtr3Array [][]*Inner + + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp3 := &TestMultiDimensionalStructsPtr3{ + Errs: errStructPtr3Array, + } + + errs = validate.Struct(tmsp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalTimeTime struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimePtr3Array [][]*time.Time + + t1 := time.Now().UTC() + t2 := time.Now().UTC() + t3 := time.Now().UTC().Add(time.Hour * 24) + + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, &t3}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, nil}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, nil, nil}) + + tmtp3 := &TestMultiDimensionalTimeTime{ + Errs: errTimePtr3Array, + } + + errs = validate.Struct(tmtp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Tag, required) + + type TestMultiDimensionalTimeTime2 struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimeArray [][]*time.Time + + t1 = time.Now().UTC() + t2 = time.Now().UTC() + t3 = time.Now().UTC().Add(time.Hour * 24) + + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, &t3}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, nil}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, nil, nil}) + + tmtp := &TestMultiDimensionalTimeTime2{ + Errs: errTimeArray, + } + + errs = validate.Struct(tmtp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Tag, required) } func TestNilStructPointerValidation(t *testing.T) { From 14f176e8ac9d7c4393fb26ee8101cf6d8a5bf522 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 10:18:17 -0400 Subject: [PATCH 17/20] add traverseMap for #78 --- validator.go | 107 ++++++++++++++++++++++++++++++++++++++-------- validator_test.go | 16 ++++++- 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/validator.go b/validator.go index 36fd3ff..a77cbb5 100644 --- a/validator.go +++ b/validator.go @@ -20,21 +20,22 @@ import ( ) const ( - utf8HexComma = "0x2C" - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - required = "required" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" failed with error(s): %s" - mapErrMsg = "Field validation for \"%s\" failed with key \"%v\" failed with error(s): %s" - structErrMsg = "Struct:%s\n" - diveTag = "dive" - diveSplit = "," + diveTag - indexFieldName = "%s[%d]" + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" + diveSplit = "," + diveTag + arrayIndexFieldName = "%s[%d]" + mapIndexFieldName = "%s[%v]" ) var structPool *pool @@ -670,7 +671,18 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } else if cField.isMap { - // return if error here + if errs := v.traverseMap(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + IsPlaceholderErr: true, + IsMap: true, + MapErrs: errs, + } + } } else { // throw error, if not a slice or map then should not have gotten here panic("dive error! can't dive on a non slice or map") @@ -680,6 +692,65 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } +func (v *Validate) traverseMap(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[interface{}]error { + + errs := map[interface{}]error{} + + for _, key := range valueField.MapKeys() { + + idxField := valueField.MapIndex(key) + + if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.sliceSubKind = idxField.Kind() + } + + switch cField.sliceSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + + continue + } + + if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + errs[key.Interface()] = &FieldError{ + Field: cField.name, + Tag: required, + Value: idxField.Interface(), + Kind: reflect.Ptr, + Type: cField.sliceSubtype, + } + } + + continue + } + + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs[key.Interface()] = structErrors + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + } + } + + return errs +} + func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { errs := map[int]error{} @@ -698,7 +769,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if cField.isTimeSubtype { - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } @@ -730,7 +801,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va } default: - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(indexFieldName, cField.name, i), false, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } } diff --git a/validator_test.go b/validator_test.go index a1086ab..207c948 100644 --- a/validator_test.go +++ b/validator_test.go @@ -227,6 +227,18 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e } func TestMapDiveValidation(t *testing.T) { + + type Test struct { + Errs map[int]string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + } + + errs := validate.Struct(test) + + fmt.Println(errs) } func TestArrayDiveValidation(t *testing.T) { @@ -509,7 +521,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.IsPlaceholderErr, false) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Field, "Errs[2][1]") Equal(t, innerSliceError1.Tag, required) type TestMultiDimensionalTimeTime2 struct { @@ -551,7 +563,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.IsPlaceholderErr, false) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Field, "Errs[2][1]") Equal(t, innerSliceError1.Tag, required) } From 8bf793acde6d57df6e685975933c9761632b38fa Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 08:22:37 -0400 Subject: [PATCH 18/20] correct map references pointing to slice after copy/paste for#78 --- validator.go | 60 +++++++++++++++++++----------------------- validator_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/validator.go b/validator.go index a77cbb5..b083a4d 100644 --- a/validator.go +++ b/validator.go @@ -20,20 +20,20 @@ import ( ) const ( - utf8HexComma = "0x2C" - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - required = "required" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" - mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" - structErrMsg = "Struct:%s\n" - diveTag = "dive" - diveSplit = "," + diveTag + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" + // diveSplit = "," + diveTag arrayIndexFieldName = "%s[%d]" mapIndexFieldName = "%s[%v]" ) @@ -457,7 +457,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter case reflect.Slice, reflect.Array: cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { @@ -469,7 +469,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter case reflect.Map: cField.isMap = true cField.mapSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) cField.mapSubKind = cField.mapSubtype.Kind() if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { @@ -537,11 +537,13 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f switch cField.kind { case reflect.Slice, reflect.Array: + isSingleField = false // cached tags mean nothing because it will be split up while diving cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() case reflect.Map: + isSingleField = false // cached tags mean nothing because it will be split up while diving cField.isMap = true cField.mapSubtype = cField.typ.Elem() cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) @@ -556,7 +558,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { - panic("Invalid field passed to ValidateFieldWithTag") + panic("Invalid field passed to fieldWithNameAndValue") } } @@ -568,22 +570,12 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if !isCached { - for k, t := range strings.Split(tag, tagSeparator) { + for _, t := range strings.Split(tag, tagSeparator) { if t == diveTag { cField.dive = true - - if k == 0 { - if len(tag) == 4 { - cField.diveTag = "" - } else { - cField.diveTag = tag[5:] - } - } else { - cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] - } - + cField.diveTag = strings.TrimLeft(strings.SplitN(tag, diveTag, 2)[1], ",") break } @@ -700,12 +692,14 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField idxField := valueField.MapIndex(key) - if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + if cField.mapSubKind == reflect.Ptr && !idxField.IsNil() { idxField = idxField.Elem() - cField.sliceSubKind = idxField.Kind() + cField.mapSubKind = idxField.Kind() } - switch cField.sliceSubKind { + // fmt.Println(cField.sliceSubKind) + + switch cField.mapSubKind { case reflect.Struct, reflect.Interface: if cField.isTimeSubtype { @@ -730,7 +724,7 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, - Type: cField.sliceSubtype, + Type: cField.mapSubtype, } } diff --git a/validator_test.go b/validator_test.go index 207c948..90ee5ee 100644 --- a/validator_test.go +++ b/validator_test.go @@ -228,21 +228,76 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e func TestMapDiveValidation(t *testing.T) { - type Test struct { - Errs map[int]string `validate:"gt=0,dive,required"` + m := map[int]string{0: "ok", 3: "", 4: "ok"} + + err := validate.Field(m, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsMap, true) + Equal(t, len(err.MapErrs), 1) + + err = validate.Field(m, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsMap, false) + Equal(t, len(err.MapErrs), 0) + + type Inner struct { + Name string `validate:"required"` } - test := &Test{ - Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + type TestMapStruct struct { + Errs map[int]Inner `validate:"gt=0,dive"` } - errs := validate.Struct(test) + mi := map[int]Inner{0: Inner{"ok"}, 3: Inner{""}, 4: Inner{"ok"}} + + ms := &TestMapStruct{ + Errs: mi, + } + + errs := validate.Struct(ms) fmt.Println(errs) + + // type Test struct { + // Errs map[int]string `validate:"gt=0,dive,required"` + // } + + // test := &Test{ + // Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + // } + + // errs := validate.Struct(test) + // NotEqual(t, errs, nil) } func TestArrayDiveValidation(t *testing.T) { + arr := []string{"ok", "", "ok"} + + err := validate.Field(arr, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsSliceOrArray, true) + Equal(t, len(err.SliceOrArrayErrs), 1) + + err = validate.Field(arr, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsSliceOrArray, false) + Equal(t, len(err.SliceOrArrayErrs), 0) + + type BadDive struct { + Name string `validate:"dive"` + } + + bd := &BadDive{ + Name: "TEST", + } + + PanicMatches(t, func() { validate.Struct(bd) }, "dive error! can't dive on a non slice or map") + type Test struct { Errs []string `validate:"gt=0,dive,required"` } @@ -3204,7 +3259,7 @@ func TestInvalidField(t *testing.T) { Test: "1", } - PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to ValidateFieldWithTag") + PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to fieldWithNameAndValue") } func TestInvalidTagField(t *testing.T) { From 200a5b4aad158afb5980788aae5ffde87009f133 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 13:41:33 -0400 Subject: [PATCH 19/20] finish map error handling & complete test coverage for #78 --- validator.go | 6 +-- validator_test.go | 105 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/validator.go b/validator.go index b083a4d..f421c87 100644 --- a/validator.go +++ b/validator.go @@ -697,8 +697,6 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField cField.mapSubKind = idxField.Kind() } - // fmt.Println(cField.sliceSubKind) - switch cField.mapSubKind { case reflect.Struct, reflect.Interface: @@ -720,7 +718,7 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField if strings.Contains(cField.tag, required) { errs[key.Interface()] = &FieldError{ - Field: cField.name, + Field: fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, @@ -779,7 +777,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if strings.Contains(cField.tag, required) { errs[i] = &FieldError{ - Field: cField.name, + Field: fmt.Sprintf(arrayIndexFieldName, cField.name, i), Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, diff --git a/validator_test.go b/validator_test.go index 90ee5ee..77e4e15 100644 --- a/validator_test.go +++ b/validator_test.go @@ -257,19 +257,95 @@ func TestMapDiveValidation(t *testing.T) { } errs := validate.Struct(ms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + structErr, ok := fieldError.MapErrs[3].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(structErr.Errors), 1) + + innerErr := structErr.Errors["Name"] + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Name") + Equal(t, innerErr.Tag, "required") + + type TestMapTimeStruct struct { + Errs map[int]*time.Time `validate:"gt=0,dive,required"` + } + + t1 := time.Now().UTC() + + mta := map[int]*time.Time{0: &t1, 3: nil, 4: nil} + + mt := &TestMapTimeStruct{ + Errs: mta, + } + + errs = validate.Struct(mt) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) - fmt.Println(errs) + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 2) - // type Test struct { - // Errs map[int]string `validate:"gt=0,dive,required"` - // } + innerErr, ok = fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Errs[3]") + Equal(t, innerErr.Tag, "required") - // test := &Test{ - // Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, - // } + type TestMapStructPtr struct { + Errs map[int]*Inner `validate:"gt=0,dive,required"` + } - // errs := validate.Struct(test) - // NotEqual(t, errs, nil) + mip := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsMap, false) + Equal(t, len(innerFieldError.MapErrs), 0) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "required") + + type TestMapStructPtr2 struct { + Errs map[int]*Inner `validate:"gt=0,dive,omitempty,required"` + } + + mip2 := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp2 := &TestMapStructPtr2{ + Errs: mip2, + } + + errs = validate.Struct(msp2) + Equal(t, errs, nil) } func TestArrayDiveValidation(t *testing.T) { @@ -437,6 +513,8 @@ func TestArrayDiveValidation(t *testing.T) { errs = validate.Struct(tmsp) NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) fieldErr, ok = errs.Errors["Errs"] Equal(t, ok, true) @@ -483,7 +561,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, fieldErr.IsSliceOrArray, true) Equal(t, len(fieldErr.SliceOrArrayErrs), 3) - sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) Equal(t, ok, true) Equal(t, sliceError1.IsPlaceholderErr, true) Equal(t, sliceError1.IsSliceOrArray, true) @@ -493,6 +571,13 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, ok, true) Equal(t, len(innerSliceStructError1.Errors), 1) + innerSliceStructError2, ok := sliceError1.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceStructError2.IsPlaceholderErr, false) + Equal(t, innerSliceStructError2.IsSliceOrArray, false) + Equal(t, len(innerSliceStructError2.SliceOrArrayErrs), 0) + Equal(t, innerSliceStructError2.Field, "Errs[2][2]") + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] Equal(t, innerInnersliceError1.IsPlaceholderErr, false) Equal(t, innerInnersliceError1.IsSliceOrArray, false) From 22aaa55c7c788576a93bccd26018bb8b9efef55d Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 14:08:07 -0400 Subject: [PATCH 20/20] add dive documentation for #78 --- doc.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc.go b/doc.go index 89142e0..06c940b 100644 --- a/doc.go +++ b/doc.go @@ -173,6 +173,25 @@ Here is a list of the current built in validators: such as min or max won't run, but if a value is set validation will run. (Usage: omitempty) + dive + This tells the validator to dive into a slice, array or map and validate that + level of the slice, array or map with the validation tags that follow. + Multidimensional nesting is also supported, each level you with to dive will + require another dive tag. (Usage: dive) + Example: [][]string with validation tag "gt=0,dive,len=1,dive,required" + gt=0 will be applied to [] + len=1 will be applied to []string + required will be applied to string + Example2: [][]string with validation tag "gt=0,dive,dive,required" + gt=0 will be applied to [] + []string will be spared validation + required will be applied to string + NOTE: in Example2 if the required validation failed, but all others passed + the hierarchy of FieldError's in the middle with have their IsPlaceHolder field + set to true. If a FieldError has IsSliceOrMap=true or IsMap=true then the + FieldError is a Slice or Map field and if IsPlaceHolder=true then contains errors + within its SliceOrArrayErrs or MapErrs fields. + required This validates that the value is not the data types default value. For numbers ensures value is not zero. For strings ensures value is