Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BC break] Custom validation with context + smaller fixes #123

Merged
107 changes: 105 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,71 @@ Add following line in your `*.go` file:
```go
import "github.com/asaskevich/govalidator"
```
If you unhappy to use long `govalidator`, you can do something like this:
If you are unhappy to use long `govalidator`, you can do something like this:
```go
import (
valid "github.com/asaskevich/govalidator"
valid "github.com/asaskevich/govalidator"
)
```

#### Activate behavior to require all fields have a validation tag by default
`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.

```go
import "github.com/asaskevich/govalidator"

func init() {
govalidator.SetFieldsRequiredByDefault(true)
}
```

Here's some code to explain it:
```go
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
type exampleStruct struct {
Name string ``
Email string `valid:"email"`

// this, however, will only fail when Email is empty or an invalid email address:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email"`

// lastly, this will only fail when Email is an invalid email address but not when it's empty:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email,optional"`
```

#### Recent breaking changes (see [#123](https:/asaskevich/govalidator/pull/123))
##### Custom validator function signature
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
```go
import "github.com/asaskevich/govalidator"

// old signature
func(i interface{}) bool

// new signature
func(i interface{}, o interface{}) bool
```

##### Adding a custom validator
This was changed to prevent data races when accessing custom validators.
```go
import "github.com/asaskevich/govalidator"

// before
govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool {
// ...
})

// after
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool {
// ...
}))
```

#### List of functions:
```go
func Abs(value float64) float64
Expand Down Expand Up @@ -184,6 +242,8 @@ govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
return str == "duck"
})
```
For completely custom validators (interface-based), see below.

Here is a list of available validators for struct fields (validator - used function):
```go
"alpha": IsAlpha,
Expand Down Expand Up @@ -272,6 +332,49 @@ println(result)
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
```

###### Custom validation functions
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
```go
import "github.com/asaskevich/govalidator"

type CustomByteArray [6]byte // custom types are supported and can be validated

type StructWithCustomByteArray struct {
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
Email string `valid:"email"`
CustomMinLength int `valid:"-"`
}

govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
switch v := context.(type) { // you can type switch on the context interface being validated
case StructWithCustomByteArray:
// you can check and validate against some other field in the context,
// return early or not validate against the context at all – your choice
case SomeOtherType:
// ...
default:
// expecting some other type? Throw/panic here or continue
}

switch v := i.(type) { // type switch on the struct field being validated
case CustomByteArray:
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
if e != 0 {
return true
}
}
}
return false
}))
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
case StructWithCustomByteArray:
return len(v.ID) >= v.CustomMinLength
}
return false
}))
```

#### Notes
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
Expand Down
4 changes: 2 additions & 2 deletions arrays.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func Each(array []interface{}, iterator Iterator) {

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

// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
var result []interface{} = make([]interface{}, 0)
var result = make([]interface{}, 0)
for index, data := range array {
if iterator(data, index) {
result = append(result, data)
Expand Down
2 changes: 1 addition & 1 deletion arrays_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestFilter(t *testing.T) {
return value.(int)%2 == 0
}
result := Filter(data, fn)
for i, _ := range result {
for i := range result {
if result[i] != answer[i] {
t.Errorf("Expected Filter(..) to be %v, got %v", answer[i], result[i])
}
Expand Down
23 changes: 22 additions & 1 deletion converter_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package govalidator

import "testing"
import (
"fmt"
"testing"
)

func TestToInt(t *testing.T) {
tests := []string{"1000", "-123", "abcdef", "100000000000000000000000000000000000000000000"}
Expand Down Expand Up @@ -55,3 +58,21 @@ func TestToFloat(t *testing.T) {
}
}
}

func TestToJSON(t *testing.T) {
tests := []interface{}{"test", map[string]string{"a": "b", "b": "c"}, func() error { return fmt.Errorf("Error") }}
expected := [][]string{
[]string{"\"test\"", "<nil>"},
[]string{"{\"a\":\"b\",\"b\":\"c\"}", "<nil>"},
[]string{"", "json: unsupported type: func() error"},
}
for i, test := range tests {
actual, err := ToJSON(test)
if actual != expected[i][0] {
t.Errorf("Expected toJSON(%v) to return '%v', got '%v'", test, expected[i][0], actual)
}
if fmt.Sprintf("%v", err) != expected[i][1] {
t.Errorf("Expected error returned from toJSON(%v) to return '%v', got '%v'", test, expected[i][1], fmt.Sprintf("%v", err))
}
}
}
6 changes: 4 additions & 2 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package govalidator

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

// Errors returns itself.
func (es Errors) Errors() []error {
return es
}
Expand All @@ -14,6 +16,7 @@ func (es Errors) Error() string {
return err
}

// Error encapsulates a name, an error and whether there's a custom error message or not.
type Error struct {
Name string
Err error
Expand All @@ -23,7 +26,6 @@ type Error struct {
func (e Error) Error() string {
if e.CustomErrorMessageExists {
return e.Err.Error()
} else {
return e.Name + ": " + e.Err.Error()
}
return e.Name + ": " + e.Err.Error()
}
29 changes: 29 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package govalidator

import (
"fmt"
"testing"
)

func TestErrorsToString(t *testing.T) {
t.Parallel()
customErr := &Error{Name: "Custom Error Name", Err: fmt.Errorf("stdlib error")}
customErrWithCustomErrorMessage := &Error{Name: "Custom Error Name 2", Err: fmt.Errorf("Bad stuff happened"), CustomErrorMessageExists: true}

var tests = []struct {
param1 Errors
expected string
}{
{Errors{}, ""},
{Errors{fmt.Errorf("Error 1")}, "Error 1;"},
{Errors{fmt.Errorf("Error 1"), fmt.Errorf("Error 2")}, "Error 1;Error 2;"},
{Errors{customErr, fmt.Errorf("Error 2")}, "Custom Error Name: stdlib error;Error 2;"},
{Errors{fmt.Errorf("Error 123"), customErrWithCustomErrorMessage}, "Error 123;Bad stuff happened;"},
}
for _, test := range tests {
actual := test.param1.Error()
if actual != test.expected {
t.Errorf("Expected Error() to return '%v', got '%v'", test.expected, actual)
}
}
}
18 changes: 9 additions & 9 deletions numerics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestAbs(t *testing.T) {
for _, test := range tests {
actual := Abs(test.param)
if actual != test.expected {
t.Errorf("Expected Abs(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected Abs(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -41,7 +41,7 @@ func TestSign(t *testing.T) {
for _, test := range tests {
actual := Sign(test.param)
if actual != test.expected {
t.Errorf("Expected Sign(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected Sign(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -63,7 +63,7 @@ func TestIsNegative(t *testing.T) {
for _, test := range tests {
actual := IsNegative(test.param)
if actual != test.expected {
t.Errorf("Expected IsNegative(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsNegative(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -85,7 +85,7 @@ func TestIsNonNegative(t *testing.T) {
for _, test := range tests {
actual := IsNonNegative(test.param)
if actual != test.expected {
t.Errorf("Expected IsNonNegative(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsNonNegative(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -107,7 +107,7 @@ func TestIsPositive(t *testing.T) {
for _, test := range tests {
actual := IsPositive(test.param)
if actual != test.expected {
t.Errorf("Expected IsPositive(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsPositive(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -129,7 +129,7 @@ func TestIsNonPositive(t *testing.T) {
for _, test := range tests {
actual := IsNonPositive(test.param)
if actual != test.expected {
t.Errorf("Expected IsNonPositive(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsNonPositive(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -151,7 +151,7 @@ func TestIsWhole(t *testing.T) {
for _, test := range tests {
actual := IsWhole(test.param)
if actual != test.expected {
t.Errorf("Expected IsWhole(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsWhole(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -173,7 +173,7 @@ func TestIsNatural(t *testing.T) {
for _, test := range tests {
actual := IsNatural(test.param)
if actual != test.expected {
t.Errorf("Expected IsNatural(%q) to be %v, got %v", test.param, test.expected, actual)
t.Errorf("Expected IsNatural(%v) to be %v, got %v", test.param, test.expected, actual)
}
}
}
Expand All @@ -198,7 +198,7 @@ func TestInRange(t *testing.T) {
for _, test := range tests {
actual := InRange(test.param, test.left, test.right)
if actual != test.expected {
t.Errorf("Expected InRange(%q, %q, %q) to be %v, got %v", test.param, test.left, test.right, test.expected, actual)
t.Errorf("Expected InRange(%v, %v, %v) to be %v, got %v", test.param, test.left, test.right, test.expected, actual)
}
}
}
26 changes: 24 additions & 2 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package govalidator
import (
"reflect"
"regexp"
"sync"
)

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

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

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

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

type customTypeTagMap struct {
validators map[string]CustomTypeValidator

sync.RWMutex
}

func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
tm.RLock()
defer tm.RUnlock()
v, ok := tm.validators[name]
return v, ok
}

func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
tm.Lock()
defer tm.Unlock()
tm.validators[name] = ctv
}

// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
// `type UUID [16]byte` (this would be handled as an array of bytes).
var CustomTypeTagMap = map[string]CustomTypeValidator{}
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}

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