├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── check.go ├── check_test.go ├── datetime.go ├── datetime_test.go ├── e2e_test.go ├── example_custom_validator_test.go ├── example_test.go ├── number.go ├── number_test.go ├── slice.go ├── string.go └── string_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | *.test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - master 6 | 7 | script: 8 | - go test -v 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # check - Go package for data validation 3 | 4 | [![Build Status](https://travis-ci.org/pengux/check.svg?branch=master)](https://travis-ci.org/pengux/check) [![GoDoc](https://godoc.org/github.com/pengux/check?status.svg)](https://godoc.org/github.com/pengux/check) 5 | 6 | ## Design goals 7 | - Composite pattern 8 | - Multiple constraints on the same value by applying multiple validators 9 | - Easy to create custom validators 10 | - Easy to customize error messages 11 | 12 | ## Usage 13 | ```bash 14 | go get github.com/pengux/check 15 | ``` 16 | 17 | 18 | To run tests: 19 | ```bash 20 | cd $GOPATH/src/github.com/pengux/check && go test 21 | ``` 22 | 23 | 24 | To validate your data, create a new Struct and add validators to it: 25 | 26 | ```go 27 | type User struct { 28 | Username string 29 | } 30 | 31 | func main() { 32 | u := &User{ 33 | Username: "invalid*", 34 | } 35 | 36 | s := check.Struct{ 37 | "Username": check.Composite{ 38 | check.NonEmpty{}, 39 | check.Regex{`^[a-zA-Z0-9]+$`}, 40 | check.MinChar{10}, 41 | }, 42 | } 43 | 44 | e := s.Validate(u) 45 | 46 | if e.HasErrors() { 47 | err, ok := e.GetErrorsByKey("Username") 48 | if !ok { 49 | panic("key 'Username' does not exists") 50 | } 51 | fmt.Println(err) 52 | } 53 | } 54 | ``` 55 | 56 | To use your own custom validator, just implement the Validator interface: 57 | 58 | ```go 59 | type CustomStringContainValidator struct { 60 | Constraint string 61 | } 62 | 63 | func (validator CustomStringContainValidator) Validate(v interface{}) check.Error { 64 | if !strings.Contains(v.(string), validator.Constraint) { 65 | return check.NewValidationError("customStringContainValidator", v, validator.Constraint) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func main() { 72 | username := "invalid*" 73 | validator := CustomStringContainValidator{"admin"} 74 | e := validator.Validate(username) 75 | fmt.Println(check.ErrorMessages[e.Error()]) 76 | } 77 | ``` 78 | 79 | To use custom error messages, either overwrite the package variable `ErrorMessages` or create your own `map[string]string`: 80 | 81 | ```go 82 | check.ErrorMessages["minChar"] := "the string must be minimum %v characters long" 83 | errMessages := errs.ToMessages() 84 | fmt.Println(errMessages) 85 | ``` 86 | 87 | For more example code check the file [`e2e_test.go`](https://github.com/pengux/check/blob/master/e2e_test.go). 88 | 89 | -------------------------------------------------------------------------------- /check.go: -------------------------------------------------------------------------------- 1 | // Package check allows data validation of values in different types 2 | package check 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // ErrorMessages contains default error messages 12 | ErrorMessages = map[string]string{ 13 | "nonZero": "value cannot be empty", 14 | "before": "%v is not before %v", 15 | "after": "%v is not after %v", 16 | "lowerThan": "%v is not lower than %v", 17 | "greaterThan": "%v is not greater than %v", 18 | "minChar": "too short, minimum %v characters", 19 | "maxChar": "too long, minimum %v characters", 20 | "email": "'%v' is an invalid email address", 21 | "regex": "'%v' does not match '%v'", 22 | "uuid": "'%v' is an invalid uuid", 23 | } 24 | ) 25 | 26 | // Validator is an interface for constraint types with a method of validate() 27 | type Validator interface { 28 | // Validate check value against constraints 29 | Validate(v interface{}) Error 30 | } 31 | 32 | // Struct allows validation of structs 33 | type Struct map[string]Validator 34 | 35 | // Validate execute validation using the validators. 36 | func (s Struct) Validate(v interface{}) StructError { 37 | val := reflect.ValueOf(v) 38 | 39 | if val.Kind() == reflect.Ptr { 40 | val = val.Elem() 41 | } 42 | 43 | if val.Kind() != reflect.Struct { 44 | panic("not a struct") 45 | } 46 | 47 | e := StructError{} 48 | for fieldname, validator := range s { 49 | field := val.FieldByName(fieldname) 50 | 51 | if field.Kind() == reflect.Invalid { 52 | panic("invalid struct field name \"" + fieldname + "\".") 53 | } 54 | 55 | if err := validator.Validate(field.Interface()); err != nil { 56 | if _, ok := e[fieldname]; !ok { 57 | e[fieldname] = make([]Error, 0) 58 | } 59 | 60 | e[fieldname] = append(e[fieldname], err) 61 | } 62 | } 63 | 64 | return e 65 | } 66 | 67 | // Composite allows adding multiple validators to the same value 68 | type Composite []Validator 69 | 70 | // Validate implements Validator 71 | func (c Composite) Validate(v interface{}) Error { 72 | e := ValidationError{ 73 | errorMap: make(map[string][]interface{}), 74 | } 75 | for _, validator := range c { 76 | if err := validator.Validate(v); err != nil { 77 | errMap := err.ErrorMap() 78 | for msg, params := range errMap { 79 | e.errorMap[msg] = params 80 | } 81 | } 82 | } 83 | 84 | if len(e.errorMap) == 0 { 85 | return nil 86 | } 87 | 88 | return e 89 | } 90 | 91 | // Error is the default validation error. The Params() method returns the params 92 | // to be used in error messages 93 | type Error interface { 94 | Error() string 95 | ErrorMap() map[string][]interface{} 96 | } 97 | 98 | // ValidationError implements Error 99 | type ValidationError struct { 100 | errorMap map[string][]interface{} 101 | } 102 | 103 | func (e ValidationError) Error() string { 104 | var errs []string 105 | for msg := range e.errorMap { 106 | errs = append(errs, msg) 107 | } 108 | 109 | return strings.Join(errs, "; ") 110 | } 111 | 112 | // ErrorMap returns the error map 113 | func (e ValidationError) ErrorMap() map[string][]interface{} { return e.errorMap } 114 | 115 | // NewValidationError create and return a ValidationError 116 | func NewValidationError(key string, params ...interface{}) *ValidationError { 117 | return &ValidationError{ 118 | map[string][]interface{}{ 119 | key: params, 120 | }, 121 | } 122 | } 123 | 124 | // StructError is a map with validation errors 125 | type StructError map[string][]Error 126 | 127 | // HasErrors return true if the ErrorMap has errors 128 | func (e StructError) HasErrors() bool { 129 | return len(e) > 0 130 | } 131 | 132 | // Error satisfy error interface. Will return a concatenated 133 | // strings of errors separated by "\n" 134 | func (e StructError) Error() string { 135 | var s []string 136 | for _, v := range e { 137 | for _, err := range v { 138 | s = append(s, err.Error()) 139 | } 140 | } 141 | 142 | return strings.Join(s, "\n") 143 | } 144 | 145 | // GetErrorsByKey return all errors for a specificed key 146 | func (e StructError) GetErrorsByKey(key string) ([]Error, bool) { 147 | v, ok := e[key] 148 | return v, ok 149 | } 150 | 151 | // ToMessages convert StructError to a map of field and their validation key with proper error messages 152 | func (e StructError) ToMessages() map[string]map[string]string { 153 | errMessages := make(map[string]map[string]string) 154 | 155 | for field, validationErrors := range e { 156 | errMessages[field] = make(map[string]string) 157 | for _, err := range validationErrors { 158 | for key, params := range err.ErrorMap() { 159 | msg, ok := ErrorMessages[key] 160 | if !ok { 161 | errMessages[field][key] = "invalid data" 162 | } else { 163 | if len(params) > 0 { 164 | errMessages[field][key] = fmt.Sprintf(msg, params...) 165 | } else { 166 | errMessages[field][key] = msg 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | return errMessages 174 | } 175 | 176 | // NonEmpty check that the value is not a zeroed value depending on its type 177 | type NonEmpty struct{} 178 | 179 | // Validate value to not be a zeroed value, return error and empty slice of strings 180 | func (validator NonEmpty) Validate(v interface{}) Error { 181 | t := reflect.TypeOf(v) 182 | 183 | switch t.Kind() { 184 | default: 185 | if reflect.DeepEqual(reflect.Zero(t).Interface(), v) { 186 | return NewValidationError("nonZero") 187 | } 188 | case reflect.Array, reflect.Slice, reflect.Map, reflect.Chan, reflect.String: 189 | if reflect.ValueOf(v).Len() == 0 { 190 | return NewValidationError("nonZero") 191 | } 192 | } 193 | 194 | return nil 195 | } 196 | -------------------------------------------------------------------------------- /check_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "testing" 4 | 5 | type foo struct { 6 | b bool 7 | } 8 | 9 | type testTable struct { 10 | v Validator 11 | val interface{} 12 | valid bool 13 | expected string 14 | } 15 | 16 | func runTableTest(table []testTable, t *testing.T) { 17 | for _, tt := range table { 18 | err := tt.v.Validate(tt.val) 19 | if (err != nil && tt.valid) || (err == nil && !tt.valid) { 20 | t.Errorf(tt.expected) 21 | } 22 | } 23 | } 24 | 25 | func TestNonEmpty(t *testing.T) { 26 | var validatorTests = []testTable{ 27 | {&NonEmpty{}, int(1), true, "Expected int 1 to be non-zero"}, 28 | {&NonEmpty{}, float64(1.0), true, "Expected float64 1.0 to be non-zero"}, 29 | {&NonEmpty{}, "foo", true, "Expected string 'foo' to be non-zero"}, 30 | {&NonEmpty{}, true, true, "Expected boolean true to be non-zero"}, 31 | {&NonEmpty{}, foo{true}, true, "Expected struct 'foo' with value to be non-zero"}, 32 | {&NonEmpty{}, []foo{foo{true}}, true, "Expected slice of structs 'foo' with value to be non-zero"}, 33 | {&NonEmpty{}, int(0), false, "Expected int 0 to be zero"}, 34 | {&NonEmpty{}, float64(0.0), false, "Expected float64 0.0 to be zero"}, 35 | {&NonEmpty{}, "", false, "Expected string '' to be zero"}, 36 | {&NonEmpty{}, false, false, "Expected boolean false to be zero"}, 37 | {&NonEmpty{}, foo{}, false, "Expected struct 'foo' with no value to be zero"}, 38 | {&NonEmpty{}, []foo{}, false, "Expected slice of structs 'foo' with no value to be zero"}, 39 | } 40 | 41 | runTableTest(validatorTests, t) 42 | } 43 | -------------------------------------------------------------------------------- /datetime.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "time" 4 | 5 | // Before check if a time in Value is before the time in Constraint 6 | type Before struct { 7 | Constraint time.Time 8 | } 9 | 10 | // Validate check if a time in Value is before the time in Constraint 11 | func (validator Before) Validate(v interface{}) Error { 12 | if !v.(time.Time).Before(validator.Constraint) { 13 | return NewValidationError("before", v.(time.Time).String(), validator.Constraint.String()) 14 | } 15 | 16 | return nil 17 | } 18 | 19 | // After check if a time in Value is before the time in Constraint 20 | type After struct { 21 | Constraint time.Time 22 | } 23 | 24 | // Validate check if a time in Value is after the time in Constraint 25 | func (validator After) Validate(v interface{}) Error { 26 | if !v.(time.Time).After(validator.Constraint) { 27 | return NewValidationError("after", v.(time.Time).String(), validator.Constraint.String()) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /datetime_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDatetimeValidators(t *testing.T) { 9 | var validatorTests = []testTable{ 10 | { 11 | Before{ 12 | time.Date(2014, time.January, 1, 1, 0, 0, 0, time.UTC), 13 | }, 14 | time.Date(2013, time.January, 1, 1, 0, 0, 0, time.UTC), 15 | true, 16 | "Expected '2013-01-01 01:00:00' to be before '2014-01-01 01:00:00:00'", 17 | }, 18 | { 19 | After{ 20 | time.Date(2014, time.January, 1, 1, 0, 0, 0, time.UTC), 21 | }, 22 | time.Date(2015, time.January, 1, 1, 0, 0, 0, time.UTC), 23 | true, 24 | "Expected '2015-01-01 01:00:00' to be after '2014-01-01 01:00:00:00'", 25 | }, 26 | } 27 | 28 | runTableTest(validatorTests, t) 29 | } 30 | -------------------------------------------------------------------------------- /e2e_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type CustomStringContainValidator struct { 10 | Constraint string 11 | } 12 | 13 | func (validator CustomStringContainValidator) Validate(v interface{}) Error { 14 | if !strings.Contains(v.(string), validator.Constraint) { 15 | return NewValidationError("customStringContainValidator", v, validator.Constraint) 16 | } 17 | 18 | return nil 19 | } 20 | 21 | type User struct { 22 | Username string 23 | Password string 24 | Name string 25 | Age int 26 | Email string 27 | Birthday time.Time 28 | } 29 | 30 | func (u *User) Validate() StructError { 31 | s := Struct{ 32 | "Username": Composite{ 33 | NonEmpty{}, 34 | Regex{`^[a-zA-Z0-9]+$`}, 35 | }, 36 | "Password": Composite{ 37 | NonEmpty{}, 38 | MinChar{8}, 39 | }, 40 | "Name": NonEmpty{}, 41 | "Age": Composite{ 42 | GreaterThan{3}, 43 | LowerThan{120}, 44 | }, 45 | "Email": Composite{ 46 | Email{}, 47 | CustomStringContainValidator{"test.com"}, 48 | }, 49 | "Birthday": Composite{ 50 | Before{time.Date(1990, time.January, 1, 1, 0, 0, 0, time.UTC)}, 51 | After{time.Date(1900, time.January, 1, 1, 0, 0, 0, time.UTC)}, 52 | }, 53 | } 54 | e := s.Validate(u) 55 | 56 | return e 57 | } 58 | 59 | func TestIntegration(t *testing.T) { 60 | invalidUser := &User{ 61 | "not-valid-username*", 62 | "123", // Invalid password length 63 | "", // Cannot be empty 64 | 150, // Invalid age 65 | "@test", // Invalid email address 66 | time.Date(1991, time.January, 1, 1, 0, 0, 0, time.UTC), // Invalid date 67 | } 68 | 69 | validUser := &User{ 70 | "testuser", 71 | "validPassword123", 72 | "Good Name", 73 | 20, 74 | "test@test.com", 75 | time.Date(1980, time.January, 1, 1, 0, 0, 0, time.UTC), 76 | } 77 | 78 | e := invalidUser.Validate() 79 | if !e.HasErrors() { 80 | t.Errorf("Expected 'invalidUser' to be invalid") 81 | } 82 | 83 | err, ok := e.GetErrorsByKey("Username") 84 | if !ok { 85 | t.Errorf("Expected errors for 'Username'") 86 | } else { 87 | if len(err) < 1 { 88 | t.Errorf("Expected 1 error for 'Username'") 89 | } 90 | } 91 | 92 | errMessages := e.ToMessages() 93 | if errMessages["Name"]["nonZero"] != ErrorMessages["nonZero"] { 94 | t.Errorf("Expected proper error message") 95 | } 96 | 97 | // json, _ := json.MarshalIndent(errMessages, "", " ") 98 | // log.Println(string(json)) 99 | 100 | e = validUser.Validate() 101 | if e.HasErrors() { 102 | t.Errorf("Expected 'validUser' to be valid") 103 | } 104 | } 105 | 106 | func BenchmarkValidate(b *testing.B) { 107 | for i := 0; i < b.N; i++ { 108 | invalidUser := &User{ 109 | "not-valid-username*", 110 | "123", // Invalid password length 111 | "", // Cannot be empty 112 | 150, // Invalid age 113 | "@test", // Invalid email address 114 | time.Date(1991, time.January, 1, 1, 0, 0, 0, time.UTC), // Invalid date 115 | } 116 | 117 | invalidUser.Validate() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /example_custom_validator_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "fmt" 4 | 5 | func ExampleValidator() { 6 | username := "invalid*" 7 | validator := CustomStringContainValidator{"admin"} 8 | e := validator.Validate(username) 9 | fmt.Println(ErrorMessages[e.Error()]) 10 | } 11 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "fmt" 4 | 5 | type Person struct { 6 | Name string 7 | } 8 | 9 | func Example() { 10 | p := &Person{ 11 | Name: "invalid*", 12 | } 13 | 14 | s := Struct{ 15 | "Name": Composite{ 16 | NonEmpty{}, 17 | Regex{`^[a-zA-Z0-9]+$`}, 18 | MinChar{10}, 19 | }, 20 | } 21 | 22 | e := s.Validate(*p) 23 | 24 | if e.HasErrors() { 25 | err, ok := e.GetErrorsByKey("Name") 26 | if !ok { 27 | panic("key 'Name' does not exists") 28 | } 29 | fmt.Println(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "strconv" 4 | 5 | // LowerThan validates that a number must be lower than its value 6 | type LowerThan struct { 7 | Constraint float64 8 | } 9 | 10 | // Validate check value against constraint 11 | func (validator LowerThan) Validate(v interface{}) Error { 12 | switch val := v.(type) { 13 | default: 14 | return NewValidationError("NaN") 15 | case int: 16 | if validator.Constraint <= float64(val) { 17 | return NewValidationError("lowerThan", strconv.Itoa(val), strconv.FormatFloat(validator.Constraint, 'f', -1, 64)) 18 | } 19 | case float64: 20 | if validator.Constraint <= val { 21 | return NewValidationError("lowerThan", strconv.FormatFloat(val, 'f', -1, 64), strconv.FormatFloat(validator.Constraint, 'f', -1, 64)) 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // GreaterThan validates that a number must be greater than its value 29 | type GreaterThan struct { 30 | Constraint float64 31 | } 32 | 33 | // Validate check value against constraint 34 | func (validator GreaterThan) Validate(v interface{}) Error { 35 | switch val := v.(type) { 36 | default: 37 | return NewValidationError("NaN") 38 | case int: 39 | if validator.Constraint >= float64(val) { 40 | return NewValidationError("greaterThan", strconv.Itoa(val), strconv.FormatFloat(validator.Constraint, 'f', -1, 64)) 41 | } 42 | case float64: 43 | if validator.Constraint >= val { 44 | return NewValidationError("greaterThan", strconv.FormatFloat(val, 'f', -1, 64), strconv.FormatFloat(validator.Constraint, 'f', -1, 64)) 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "testing" 4 | 5 | func TestNumberValidators(t *testing.T) { 6 | var validatorTests = []testTable{ 7 | {&LowerThan{2}, 1, true, "Expected 1 to be lower than 2"}, 8 | {&LowerThan{1}, 2, false, "Expected 2 NOT to be lower than 1"}, 9 | {&LowerThan{2}, 2, false, "Expected 2 NOT to be lower than 2"}, 10 | {&GreaterThan{1}, 2, true, "Expected 2 to be greater than 1"}, 11 | {&GreaterThan{2}, 1, false, "Expected 1 NOT to be greater than 2"}, 12 | {&GreaterThan{2}, 2, false, "Expected 2 NOT to be greater than 2"}, 13 | } 14 | 15 | runTableTest(validatorTests, t) 16 | } 17 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "strconv" 5 | "reflect" 6 | ) 7 | 8 | type Slice struct { 9 | Inner Validator 10 | } 11 | 12 | func (c Slice) Validate(v interface{}) Error { 13 | err := ValidationError{ 14 | errorMap: map[string][]interface{}{}, 15 | } 16 | 17 | t := reflect.ValueOf(v) 18 | 19 | switch t.Type().Kind() { 20 | case reflect.Array, reflect.Slice: 21 | for i := 0; i < t.Len(); i++ { 22 | if verr := c.Inner.Validate(t.Index(i).Interface()); verr != nil { 23 | err.errorMap[strconv.Itoa(i)] = append([]interface{}{}, verr) 24 | } 25 | } 26 | default: 27 | return NewValidationError("notSlice") 28 | } 29 | 30 | if len(err.errorMap) == 0 { 31 | return nil 32 | } 33 | 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // MinChar validates that a string must have a length minimum of its constraint 9 | type MinChar struct { 10 | Constraint int 11 | } 12 | 13 | // Validate check value against constraint 14 | func (validator MinChar) Validate(v interface{}) Error { 15 | if len(v.(string)) < validator.Constraint { 16 | return NewValidationError("minChar", validator.Constraint) 17 | } 18 | 19 | return nil 20 | } 21 | 22 | // MaxChar validates that a string must have a length maximum of its constraint 23 | type MaxChar struct { 24 | Constraint int 25 | } 26 | 27 | // Validate check value against constraint 28 | func (validator MaxChar) Validate(v interface{}) Error { 29 | if len(v.(string)) > validator.Constraint { 30 | return NewValidationError("maxChar", validator.Constraint) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // Email is a constraint to do a simple validation for email addresses, it only check if the string contains "@" 37 | // and that it is not in the first or last character of the string. Also if there is only one "@" and if there is a "." in the string after the "@". 38 | type Email struct{} 39 | 40 | // Validate email addresses 41 | func (validator Email) Validate(v interface{}) Error { 42 | err := NewValidationError("email", v) 43 | 44 | s, ok := v.(string) 45 | if !ok { 46 | return err 47 | } 48 | 49 | email := []rune(s) 50 | at := '@' 51 | dot := '.' 52 | firstChar := email[0] 53 | lastChar := email[len(email)-1] 54 | emailParts := strings.Split(string(s), "@") 55 | 56 | if len(emailParts) != 2 || // not containing "@" 57 | firstChar == at || // "@bar" 58 | lastChar == at || // "foo@" 59 | lastChar == dot || // "foo@bar." 60 | !strings.ContainsRune(emailParts[1], dot) || // "foo@bar" 61 | []rune(emailParts[1])[0] == dot { // "foo@.bar" 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // Regex allow validation usig regular expressions 69 | type Regex struct { 70 | Constraint string 71 | } 72 | 73 | // Validate using regex 74 | func (validator Regex) Validate(v interface{}) Error { 75 | regex, err := regexp.Compile(validator.Constraint) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | if !regex.MatchString(v.(string)) { 81 | return NewValidationError("regex", v, validator.Constraint) 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // UUID verify a string in the UUID format xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx 88 | type UUID struct{} 89 | 90 | // Validate checks a string as correct UUID format 91 | func (validator UUID) Validate(v interface{}) Error { 92 | regex := regexp.MustCompile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") 93 | 94 | if !regex.MatchString(v.(string)) { 95 | return NewValidationError("uuid", v) 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import "testing" 4 | 5 | func TestStringValidators(t *testing.T) { 6 | var validatorTests = []testTable{ 7 | {&MinChar{3}, "foo", true, "Expected 'foo' to be minimum of 3 char"}, 8 | {&MinChar{5}, "foo", false, "Expected 'foo' NOT to be minimum of 5 char"}, 9 | {&MaxChar{3}, "foo", true, "Expected 'foo' to be maximum of 3 char"}, 10 | {&MaxChar{2}, "foo", false, "Expected 'foo' NOT to be maximum of 2 char"}, 11 | {&Email{}, "@foo", false, "Expected '@foo' to be invalid email address"}, 12 | {&Email{}, "foo@", false, "Expected 'foo@' to be invalid email address"}, 13 | {&Email{}, "foo", false, "Expected 'foo' to be invalid email address"}, 14 | {&Email{}, "foo@bar.", false, "Expected 'foo@bar.' to be invalid email address"}, 15 | {&Email{}, "foo@bar.com", true, "Expected 'foo@bar.com' to be a valid email address"}, 16 | {&Email{}, "@語", false, "Expected '@語' to be invalid email address"}, 17 | {&Email{}, "語@", false, "Expected '語@' to be invalid email address"}, 18 | {&Email{}, "語", false, "Expected '語' to be invalid email address"}, 19 | {&Email{}, "語@語.", false, "Expected '語@語.' to be invalid email address"}, 20 | {&Email{}, "語@語.com", true, "Expected '語@語.com' to be a valid email address"}, 21 | {&Regex{"[a-zA-Z0-9]"}, "aA0", true, "Expected 'aA0' to match the regex '[a-zA-Z0-9]'"}, 22 | {&Regex{"[a-zA-Z0-9]"}, "*", false, "Expected '*' NOT to match the regex '[a-zA-Z0-9]'"}, 23 | {&UUID{}, "invalid-uuid", false, "Expected 'invalid-uuid' to be invalid uuid"}, 24 | {&UUID{}, "e3ef5847-2e83-4c67-be80-4e1c832afc4a", true, "Expected 'e3ef5847-2e83-4c67-be80-4e1c832afc4a' to be a valid uuid"}, 25 | } 26 | 27 | runTableTest(validatorTests, t) 28 | } 29 | --------------------------------------------------------------------------------