Skip to content

Commit 7664702

Browse files
committed
Merge pull request #123 from annismckenzie/custom_validation_with_context
[BC break] Custom validation with context + smaller fixes
2 parents 37d5f82 + c5b5a56 commit 7664702

12 files changed

Lines changed: 257 additions & 48 deletions

README.md

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,71 @@ Add following line in your `*.go` file:
1919
```go
2020
import "github.com/asaskevich/govalidator"
2121
```
22-
If you unhappy to use long `govalidator`, you can do something like this:
22+
If you are unhappy to use long `govalidator`, you can do something like this:
2323
```go
2424
import (
25-
valid "github.com/asaskevich/govalidator"
25+
valid "github.com/asaskevich/govalidator"
2626
)
2727
```
2828

29+
#### Activate behavior to require all fields have a validation tag by default
30+
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
31+
32+
```go
33+
import "github.com/asaskevich/govalidator"
34+
35+
func init() {
36+
govalidator.SetFieldsRequiredByDefault(true)
37+
}
38+
```
39+
40+
Here's some code to explain it:
41+
```go
42+
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
43+
type exampleStruct struct {
44+
Name string ``
45+
Email string `valid:"email"`
46+
47+
// this, however, will only fail when Email is empty or an invalid email address:
48+
type exampleStruct2 struct {
49+
Name string `valid:"-"`
50+
Email string `valid:"email"`
51+
52+
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
53+
type exampleStruct2 struct {
54+
Name string `valid:"-"`
55+
Email string `valid:"email,optional"`
56+
```
57+
58+
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
59+
##### Custom validator function signature
60+
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
61+
```go
62+
import "github.com/asaskevich/govalidator"
63+
64+
// old signature
65+
func(i interface{}) bool
66+
67+
// new signature
68+
func(i interface{}, o interface{}) bool
69+
```
70+
71+
##### Adding a custom validator
72+
This was changed to prevent data races when accessing custom validators.
73+
```go
74+
import "github.com/asaskevich/govalidator"
75+
76+
// before
77+
govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool {
78+
// ...
79+
})
80+
81+
// after
82+
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool {
83+
// ...
84+
}))
85+
```
86+
2987
#### List of functions:
3088
```go
3189
func Abs(value float64) float64
@@ -184,6 +242,8 @@ govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
184242
return str == "duck"
185243
})
186244
```
245+
For completely custom validators (interface-based), see below.
246+
187247
Here is a list of available validators for struct fields (validator - used function):
188248
```go
189249
"alpha": IsAlpha,
@@ -272,6 +332,49 @@ println(result)
272332
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
273333
```
274334
335+
###### Custom validation functions
336+
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
337+
```go
338+
import "github.com/asaskevich/govalidator"
339+
340+
type CustomByteArray [6]byte // custom types are supported and can be validated
341+
342+
type StructWithCustomByteArray struct {
343+
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
344+
Email string `valid:"email"`
345+
CustomMinLength int `valid:"-"`
346+
}
347+
348+
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
349+
switch v := context.(type) { // you can type switch on the context interface being validated
350+
case StructWithCustomByteArray:
351+
// you can check and validate against some other field in the context,
352+
// return early or not validate against the context at all – your choice
353+
case SomeOtherType:
354+
// ...
355+
default:
356+
// expecting some other type? Throw/panic here or continue
357+
}
358+
359+
switch v := i.(type) { // type switch on the struct field being validated
360+
case CustomByteArray:
361+
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
362+
if e != 0 {
363+
return true
364+
}
365+
}
366+
}
367+
return false
368+
}))
369+
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
370+
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
371+
case StructWithCustomByteArray:
372+
return len(v.ID) >= v.CustomMinLength
373+
}
374+
return false
375+
}))
376+
```
377+
275378
#### Notes
276379
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
277380
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).

arrays.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func Each(array []interface{}, iterator Iterator) {
1818

1919
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
2020
func Map(array []interface{}, iterator ResultIterator) []interface{} {
21-
var result []interface{} = make([]interface{}, len(array))
21+
var result = make([]interface{}, len(array))
2222
for index, data := range array {
2323
result[index] = iterator(data, index)
2424
}
@@ -37,7 +37,7 @@ func Find(array []interface{}, iterator ConditionIterator) interface{} {
3737

3838
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
3939
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
40-
var result []interface{} = make([]interface{}, 0)
40+
var result = make([]interface{}, 0)
4141
for index, data := range array {
4242
if iterator(data, index) {
4343
result = append(result, data)

arrays_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestFilter(t *testing.T) {
7878
return value.(int)%2 == 0
7979
}
8080
result := Filter(data, fn)
81-
for i, _ := range result {
81+
for i := range result {
8282
if result[i] != answer[i] {
8383
t.Errorf("Expected Filter(..) to be %v, got %v", answer[i], result[i])
8484
}

converter_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package govalidator
22

3-
import "testing"
3+
import (
4+
"fmt"
5+
"testing"
6+
)
47

58
func TestToInt(t *testing.T) {
69
tests := []string{"1000", "-123", "abcdef", "100000000000000000000000000000000000000000000"}
@@ -55,3 +58,21 @@ func TestToFloat(t *testing.T) {
5558
}
5659
}
5760
}
61+
62+
func TestToJSON(t *testing.T) {
63+
tests := []interface{}{"test", map[string]string{"a": "b", "b": "c"}, func() error { return fmt.Errorf("Error") }}
64+
expected := [][]string{
65+
[]string{"\"test\"", "<nil>"},
66+
[]string{"{\"a\":\"b\",\"b\":\"c\"}", "<nil>"},
67+
[]string{"", "json: unsupported type: func() error"},
68+
}
69+
for i, test := range tests {
70+
actual, err := ToJSON(test)
71+
if actual != expected[i][0] {
72+
t.Errorf("Expected toJSON(%v) to return '%v', got '%v'", test, expected[i][0], actual)
73+
}
74+
if fmt.Sprintf("%v", err) != expected[i][1] {
75+
t.Errorf("Expected error returned from toJSON(%v) to return '%v', got '%v'", test, expected[i][1], fmt.Sprintf("%v", err))
76+
}
77+
}
78+
}

error.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package govalidator
22

3+
// Errors is an array of multiple errors and conforms to the error interface.
34
type Errors []error
45

6+
// Errors returns itself.
57
func (es Errors) Errors() []error {
68
return es
79
}
@@ -14,6 +16,7 @@ func (es Errors) Error() string {
1416
return err
1517
}
1618

19+
// Error encapsulates a name, an error and whether there's a custom error message or not.
1720
type Error struct {
1821
Name string
1922
Err error
@@ -23,7 +26,6 @@ type Error struct {
2326
func (e Error) Error() string {
2427
if e.CustomErrorMessageExists {
2528
return e.Err.Error()
26-
} else {
27-
return e.Name + ": " + e.Err.Error()
2829
}
30+
return e.Name + ": " + e.Err.Error()
2931
}

error_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package govalidator
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestErrorsToString(t *testing.T) {
9+
t.Parallel()
10+
customErr := &Error{Name: "Custom Error Name", Err: fmt.Errorf("stdlib error")}
11+
customErrWithCustomErrorMessage := &Error{Name: "Custom Error Name 2", Err: fmt.Errorf("Bad stuff happened"), CustomErrorMessageExists: true}
12+
13+
var tests = []struct {
14+
param1 Errors
15+
expected string
16+
}{
17+
{Errors{}, ""},
18+
{Errors{fmt.Errorf("Error 1")}, "Error 1;"},
19+
{Errors{fmt.Errorf("Error 1"), fmt.Errorf("Error 2")}, "Error 1;Error 2;"},
20+
{Errors{customErr, fmt.Errorf("Error 2")}, "Custom Error Name: stdlib error;Error 2;"},
21+
{Errors{fmt.Errorf("Error 123"), customErrWithCustomErrorMessage}, "Error 123;Bad stuff happened;"},
22+
}
23+
for _, test := range tests {
24+
actual := test.param1.Error()
25+
if actual != test.expected {
26+
t.Errorf("Expected Error() to return '%v', got '%v'", test.expected, actual)
27+
}
28+
}
29+
}

numerics_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestAbs(t *testing.T) {
1919
for _, test := range tests {
2020
actual := Abs(test.param)
2121
if actual != test.expected {
22-
t.Errorf("Expected Abs(%q) to be %v, got %v", test.param, test.expected, actual)
22+
t.Errorf("Expected Abs(%v) to be %v, got %v", test.param, test.expected, actual)
2323
}
2424
}
2525
}
@@ -41,7 +41,7 @@ func TestSign(t *testing.T) {
4141
for _, test := range tests {
4242
actual := Sign(test.param)
4343
if actual != test.expected {
44-
t.Errorf("Expected Sign(%q) to be %v, got %v", test.param, test.expected, actual)
44+
t.Errorf("Expected Sign(%v) to be %v, got %v", test.param, test.expected, actual)
4545
}
4646
}
4747
}
@@ -63,7 +63,7 @@ func TestIsNegative(t *testing.T) {
6363
for _, test := range tests {
6464
actual := IsNegative(test.param)
6565
if actual != test.expected {
66-
t.Errorf("Expected IsNegative(%q) to be %v, got %v", test.param, test.expected, actual)
66+
t.Errorf("Expected IsNegative(%v) to be %v, got %v", test.param, test.expected, actual)
6767
}
6868
}
6969
}
@@ -85,7 +85,7 @@ func TestIsNonNegative(t *testing.T) {
8585
for _, test := range tests {
8686
actual := IsNonNegative(test.param)
8787
if actual != test.expected {
88-
t.Errorf("Expected IsNonNegative(%q) to be %v, got %v", test.param, test.expected, actual)
88+
t.Errorf("Expected IsNonNegative(%v) to be %v, got %v", test.param, test.expected, actual)
8989
}
9090
}
9191
}
@@ -107,7 +107,7 @@ func TestIsPositive(t *testing.T) {
107107
for _, test := range tests {
108108
actual := IsPositive(test.param)
109109
if actual != test.expected {
110-
t.Errorf("Expected IsPositive(%q) to be %v, got %v", test.param, test.expected, actual)
110+
t.Errorf("Expected IsPositive(%v) to be %v, got %v", test.param, test.expected, actual)
111111
}
112112
}
113113
}
@@ -129,7 +129,7 @@ func TestIsNonPositive(t *testing.T) {
129129
for _, test := range tests {
130130
actual := IsNonPositive(test.param)
131131
if actual != test.expected {
132-
t.Errorf("Expected IsNonPositive(%q) to be %v, got %v", test.param, test.expected, actual)
132+
t.Errorf("Expected IsNonPositive(%v) to be %v, got %v", test.param, test.expected, actual)
133133
}
134134
}
135135
}
@@ -151,7 +151,7 @@ func TestIsWhole(t *testing.T) {
151151
for _, test := range tests {
152152
actual := IsWhole(test.param)
153153
if actual != test.expected {
154-
t.Errorf("Expected IsWhole(%q) to be %v, got %v", test.param, test.expected, actual)
154+
t.Errorf("Expected IsWhole(%v) to be %v, got %v", test.param, test.expected, actual)
155155
}
156156
}
157157
}
@@ -173,7 +173,7 @@ func TestIsNatural(t *testing.T) {
173173
for _, test := range tests {
174174
actual := IsNatural(test.param)
175175
if actual != test.expected {
176-
t.Errorf("Expected IsNatural(%q) to be %v, got %v", test.param, test.expected, actual)
176+
t.Errorf("Expected IsNatural(%v) to be %v, got %v", test.param, test.expected, actual)
177177
}
178178
}
179179
}
@@ -198,7 +198,7 @@ func TestInRange(t *testing.T) {
198198
for _, test := range tests {
199199
actual := InRange(test.param, test.left, test.right)
200200
if actual != test.expected {
201-
t.Errorf("Expected InRange(%q, %q, %q) to be %v, got %v", test.param, test.left, test.right, test.expected, actual)
201+
t.Errorf("Expected InRange(%v, %v, %v) to be %v, got %v", test.param, test.left, test.right, test.expected, actual)
202202
}
203203
}
204204
}

types.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package govalidator
33
import (
44
"reflect"
55
"regexp"
6+
"sync"
67
)
78

89
// Validator is a wrapper for a validator function that returns bool and accepts string.
910
type Validator func(str string) bool
1011

1112
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
12-
type CustomTypeValidator func(i interface{}) bool
13+
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
14+
type CustomTypeValidator func(i interface{}, o interface{}) bool
1315

1416
// ParamValidator is a wrapper for validator functions that accepts additional parameters.
1517
type ParamValidator func(str string, params ...string) bool
@@ -31,16 +33,36 @@ var ParamTagMap = map[string]ParamValidator{
3133
"matches": StringMatches,
3234
}
3335

36+
// ParamTagRegexMap maps param tags to their respective regexes.
3437
var ParamTagRegexMap = map[string]*regexp.Regexp{
3538
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
3639
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
3740
"matches": regexp.MustCompile(`matches\(([^)]+)\)`),
3841
}
3942

43+
type customTypeTagMap struct {
44+
validators map[string]CustomTypeValidator
45+
46+
sync.RWMutex
47+
}
48+
49+
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
50+
tm.RLock()
51+
defer tm.RUnlock()
52+
v, ok := tm.validators[name]
53+
return v, ok
54+
}
55+
56+
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
57+
tm.Lock()
58+
defer tm.Unlock()
59+
tm.validators[name] = ctv
60+
}
61+
4062
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
4163
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
4264
// `type UUID [16]byte` (this would be handled as an array of bytes).
43-
var CustomTypeTagMap = map[string]CustomTypeValidator{}
65+
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
4466

4567
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
4668
var TagMap = map[string]Validator{

0 commit comments

Comments
 (0)