Skip to content

Commit

Permalink
Merge pull request #123 from annismckenzie/custom_validation_with_con…
Browse files Browse the repository at this point in the history
…text

[BC break] Custom validation with context + smaller fixes
  • Loading branch information
asaskevich committed May 18, 2016
2 parents 37d5f82 + c5b5a56 commit 7664702
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 48 deletions.
107 changes: 105 additions & 2 deletions README.md
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://github.com/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
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
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
@@ -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
@@ -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
@@ -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
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
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

0 comments on commit 7664702

Please sign in to comment.