├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── doc ├── BENCHMARK.md ├── CUSTOM_RULE.md ├── FILE_VALIDATION.md ├── MAP_VALIDATION.md ├── NESTED_STRUCT.md ├── SIMPLE_STRUCT_VALIDATION.md └── STRUCT_VALIDATION.md ├── errors.go ├── govalidator.png ├── helper.go ├── helper_test.go ├── regex_patterns.go ├── roller.go ├── roller_test.go ├── rules.go ├── rules_test.go ├── type.go ├── utils.go ├── utils110.go ├── utils_pre110.go ├── utils_test.go ├── validate_file.go ├── validate_file_test.go ├── validator.go └── validator_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.8 7 | - go: 1.9 8 | - go: 1.10.x 9 | - go: 1.11.x 10 | - go: tip 11 | allow_failures: 12 | - go: tip 13 | before_install: 14 | - go get github.com/mattn/goveralls 15 | script: 16 | - $GOPATH/bin/goveralls -service=travis-ci 17 | - go get -t -v ./... 18 | - diff -u <(echo -n) <(gofmt -d .) 19 | - go vet $(go list ./... | grep -v /vendor/) 20 | - go test -v -race ./... 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Must follow the guide for issues 4 | - Use the search tool before opening a new issue. 5 | - Please provide source code and stack trace if you found a bug. 6 | - Please review the existing issues, [project cards](https://github.com/thedevsaddam/govalidator/projects/1) and provide feedback to them 7 | 8 | ## Pull Request Process 9 | - Open your pull request against `dev` branch 10 | - It should pass all tests in the available continuous integrations systems such as TravisCI. 11 | - You should add/modify tests to cover your proposed code changes. 12 | - If your pull request contains a new feature, please document it on the README. 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Saddam H 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all 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, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![govalidator](govalidator.png) 2 | 3 | [![Build Status](https://travis-ci.org/thedevsaddam/govalidator.svg?branch=master)](https://travis-ci.org/thedevsaddam/govalidator) 4 | [![Project status](https://img.shields.io/badge/version-1.9-green.svg)](https://github.com/thedevsaddam/govalidator/releases) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/thedevsaddam/govalidator)](https://goreportcard.com/report/github.com/thedevsaddam/govalidator) 6 | [![Coverage Status](https://coveralls.io/repos/github/thedevsaddam/govalidator/badge.svg?branch=master)](https://coveralls.io/github/thedevsaddam/govalidator?branch=master) 7 | [![GoDoc](https://godoc.org/github.com/thedevsaddam/govalidator?status.svg)](https://godoc.org/github.com/thedevsaddam/govalidator) 8 | [![License](https://img.shields.io/dub/l/vibe-d.svg)](https://github.com/thedevsaddam/govalidator/blob/dev/LICENSE.md) 9 | 10 | Validate golang request data with simple rules. Highly inspired by Laravel's request validation. 11 | 12 | 13 | ### Installation 14 | 15 | Install the package using 16 | ```go 17 | $ go get github.com/thedevsaddam/govalidator 18 | // or 19 | $ go get gopkg.in/thedevsaddam/govalidator.v1 20 | ``` 21 | 22 | ### Usage 23 | 24 | To use the package import it in your `*.go` code 25 | ```go 26 | import "github.com/thedevsaddam/govalidator" 27 | // or 28 | import "gopkg.in/thedevsaddam/govalidator.v1" 29 | ``` 30 | 31 | ### Example 32 | 33 | ***Validate `form-data`, `x-www-form-urlencoded` and `query params`*** 34 | 35 | ```go 36 | 37 | package main 38 | 39 | import ( 40 | "encoding/json" 41 | "fmt" 42 | "net/http" 43 | 44 | "github.com/thedevsaddam/govalidator" 45 | ) 46 | 47 | func handler(w http.ResponseWriter, r *http.Request) { 48 | rules := govalidator.MapData{ 49 | "username": []string{"required", "between:3,8"}, 50 | "email": []string{"required", "min:4", "max:20", "email"}, 51 | "web": []string{"url"}, 52 | "phone": []string{"digits:11"}, 53 | "agree": []string{"bool"}, 54 | "dob": []string{"date"}, 55 | } 56 | 57 | messages := govalidator.MapData{ 58 | "username": []string{"required:আপনাকে অবশ্যই ইউজারনেম দিতে হবে", "between:ইউজারনেম অবশ্যই ৩-৮ অক্ষর হতে হবে"}, 59 | "phone": []string{"digits:ফোন নাম্বার অবশ্যই ১১ নম্বারের হতে হবে"}, 60 | } 61 | 62 | opts := govalidator.Options{ 63 | Request: r, // request object 64 | Rules: rules, // rules map 65 | Messages: messages, // custom message map (Optional) 66 | RequiredDefault: true, // all the field to be pass the rules 67 | } 68 | v := govalidator.New(opts) 69 | e := v.Validate() 70 | err := map[string]interface{}{"validationError": e} 71 | w.Header().Set("Content-type", "application/json") 72 | json.NewEncoder(w).Encode(err) 73 | } 74 | 75 | func main() { 76 | http.HandleFunc("/", handler) 77 | fmt.Println("Listening on port: 9000") 78 | http.ListenAndServe(":9000", nil) 79 | } 80 | 81 | ``` 82 | 83 | Send request to the server using curl or postman: `curl GET "http://localhost:9000?web=&phone=&zip=&dob=&agree="` 84 | 85 | 86 | ***Response*** 87 | ```json 88 | { 89 | "validationError": { 90 | "agree": [ 91 | "The agree may only contain boolean value, string or int 0, 1" 92 | ], 93 | "dob": [ 94 | "The dob field must be a valid date format. e.g: yyyy-mm-dd, yyyy/mm/dd etc" 95 | ], 96 | "email": [ 97 | "The email field is required", 98 | "The email field must be a valid email address" 99 | ], 100 | "phone": [ 101 | "ফোন নাম্বার অবশ্যই ১১ নম্বারের হতে হবে" 102 | ], 103 | "username": [ 104 | "আপনাকে অবশ্যই ইউজারনেম দিতে হবে", 105 | "ইউজারনেম অবশ্যই ৩-৮ অক্ষর হতে হবে" 106 | ], 107 | "web": [ 108 | "The web field format is invalid" 109 | ] 110 | } 111 | } 112 | ``` 113 | 114 | ### More examples 115 | 116 | ***Validate file*** 117 | 118 | * [Validate file](doc/FILE_VALIDATION.md) 119 | 120 | ***Validate `application/json` or `text/plain` as raw body*** 121 | 122 | * [Validate JSON to simple struct](doc/SIMPLE_STRUCT_VALIDATION.md) 123 | * [Validate JSON to map](doc/MAP_VALIDATION.md) 124 | * [Validate JSON to nested struct](doc/NESTED_STRUCT.md) 125 | * [Validate using custom rule](doc/CUSTOM_RULE.md) 126 | 127 | ***Validate struct directly*** 128 | 129 | * [Validate Struct](doc/STRUCT_VALIDATION.md) 130 | 131 | ### Validation Rules 132 | * `alpha` The field under validation must be entirely alphabetic characters. 133 | * `alpha_dash` The field under validation may have alpha-numeric characters, as well as dashes and underscores. 134 | * `alpha_space` The field under validation may have alpha-numeric characters, as well as dashes, underscores and space. 135 | * `alpha_num` The field under validation must be entirely alpha-numeric characters. 136 | * `between:numeric,numeric` The field under validation check the length of characters/ length of array, slice, map/ range between two integer or float number etc. 137 | * `numeric` The field under validation must be entirely numeric characters. 138 | * `numeric_between:numeric,numeric` The field under validation must be a numeric value between the range. 139 | e.g: `numeric_between:18,65` may contains numeric value like `35`, `55` . You can also pass float value to check. Moreover, both bounds can be omitted to create an unbounded minimum (e.g: `numeric_between:,65`) or an unbounded maximum (e.g: `numeric_between:-1,`). 140 | * `bool` The field under validation must be able to be cast as a boolean. Accepted input are `true, false, 1, 0, "1" and "0"`. 141 | * `credit_card` The field under validation must have a valid credit card number. Accepted cards are `Visa, MasterCard, American Express, Diners Club, Discover and JCB card` 142 | * `coordinate` The field under validation must have a value of valid coordinate. 143 | * `css_color` The field under validation must have a value of valid CSS color. Accepted colors are `hex, rgb, rgba, hsl, hsla` like `#909, #00aaff, rgb(255,122,122)` 144 | * `date` The field under validation must have a valid date of format yyyy-mm-dd or yyyy/mm/dd. 145 | * `date:dd-mm-yyyy` The field under validation must have a valid date of format dd-mm-yyyy. 146 | * `digits:int` The field under validation must be numeric and must have an exact length of value. 147 | * `digits_between:int,int` The field under validation must be numeric and must have length between the range. 148 | e.g: `digits_between:3,5` may contains digits like `2323`, `12435` 149 | * `in:foo,bar` The field under validation must have one of the values. e.g: `in:admin,manager,user` must contain the values (admin or manager or user) 150 | * `not_in:foo,bar` The field under validation must have one value except foo,bar. e.g: `not_in:admin,manager,user` must not contain the values (admin or manager or user) 151 | * `email` The field under validation must have a valid email. 152 | * `float` The field under validation must have a valid float number. 153 | * `mac_address` The field under validation must have be a valid Mac Address. 154 | * `min:numeric` The field under validation must have a min length of characters for string, items length for slice/map, value for integer or float. 155 | e.g: `min:3` may contains characters minimum length of 3 like `"john", "jane", "jane321"` but not `"mr", "xy"` 156 | * `max:numeric` The field under validation must have a max length of characters for string, items length for slice/map, value for integer or float. 157 | e.g: `max:6` may contains characters maximum length of 6 like `"john doe", "jane doe"` but not `"john", "jane"` 158 | * `len:numeric` The field under validation must have an exact length of characters, exact integer or float value, exact size of map/slice. 159 | e.g: `len:4` may contains characters exact length of 4 like `Food, Mood, Good` 160 | * `ip` The field under validation must be a valid IP address. 161 | * `ip_v4` The field under validation must be a valid IP V4 address. 162 | * `ip_v6` The field under validation must be a valid IP V6 address. 163 | * `json` The field under validation must be a valid JSON string. 164 | * `lat` The field under validation must be a valid latitude. 165 | * `lon` The field under validation must be a valid longitude. 166 | * `regex:regular expression` The field under validation validate against the regex. e.g: `regex:^[a-zA-Z]+$` validate the letters. 167 | * `required` The field under validation must be present in the input data and not empty. A field is considered "empty" if one of the following conditions are true: 1) The value is null. 2)The value is an empty string. 3) Zero length of map, slice. 4) Zero value for integer or float 168 | * `size:integer` The field under validation validate a file size only in form-data ([see example](doc/FILE_VALIDATION.md)) 169 | * `ext:jpg,png` The field under validation validate a file extension ([see example](doc/FILE_VALIDATION.md)) 170 | * `mime:image/jpg,image/png` The field under validation validate a file mime type ([see example](doc/FILE_VALIDATION.md)) 171 | * `url` The field under validation must be a valid URL. 172 | * `uuid` The field under validation must be a valid UUID. 173 | * `uuid_v3` The field under validation must be a valid UUID V3. 174 | * `uuid_v4` The field under validation must be a valid UUID V4. 175 | * `uuid_v5` The field under validation must be a valid UUID V5. 176 | 177 | ### Add Custom Rules 178 | 179 | ```go 180 | func init() { 181 | // simple example 182 | govalidator.AddCustomRule("must_john", func(field string, rule string, message string, value interface{}) error { 183 | val := value.(string) 184 | if val != "john" || val != "John" { 185 | return fmt.Errorf("The %s field must be John or john", field) 186 | } 187 | return nil 188 | }) 189 | 190 | // custom rules to take fixed length word. 191 | // e.g: word:5 will throw error if the field does not contain exact 5 word 192 | govalidator.AddCustomRule("word", func(field string, rule string, message string, value interface{}) error { 193 | valSlice := strings.Fields(value.(string)) 194 | l, _ := strconv.Atoi(strings.TrimPrefix(rule, "word:")) //handle other error 195 | if len(valSlice) != l { 196 | return fmt.Errorf("The %s field must be %d word", field, l) 197 | } 198 | return nil 199 | }) 200 | 201 | } 202 | ``` 203 | Note: Array, map, slice can be validated by adding custom rules. 204 | 205 | ### Custom Message/ Localization 206 | If you need to translate validation message you can pass messages as options. 207 | 208 | ```go 209 | messages := govalidator.MapData{ 210 | "username": []string{"required:You must provide username", "between:The username field must be between 3 to 8 chars"}, 211 | "zip": []string{"numeric:Please provide zip field as numeric"}, 212 | } 213 | 214 | opts := govalidator.Options{ 215 | Messages: messages, 216 | } 217 | ``` 218 | 219 | ### Contribution 220 | If you are interested to make the package better please send pull requests or create an issue so that others can fix. 221 | [Read the contribution guide here](CONTRIBUTING.md) 222 | 223 | ### Contributors 224 | 225 | - [Jun Kimura](https://github.com/bluele) 226 | - [Steve HIll](https://github.com/stevehill1981) 227 | - [ErickSkrauch](https://github.com/erickskrauch) 228 | - [Sakib Sami](https://github.com/s4kibs4mi) 229 | - [Rip](https://github.com/ripbandit) 230 | - [Jose Nazario](https://github.com/paralax) 231 | 232 | ### See all [contributors](https://github.com/thedevsaddam/govalidator/graphs/contributors) 233 | 234 | ### See [benchmarks](doc/BENCHMARK.md) 235 | ### Read [API documentation](https://godoc.org/github.com/thedevsaddam/govalidator) 236 | 237 | ### **License** 238 | The **govalidator** is an open-source software licensed under the [MIT License](LICENSE.md). 239 | -------------------------------------------------------------------------------- /doc/BENCHMARK.md: -------------------------------------------------------------------------------- 1 | Benchmarks 2 | =================== 3 | 4 | Machine: XPS 13 9370 (07E6) 5 | Go version: go version go1.12.6 linux/amd64 6 | 7 | | ➜ go test -run=XXX -bench=. -benchmem=true | | | | | 8 | |--------------------------------------------|------------|--------------|--------------|--------------| 9 | |Benchmark_IsAlpha-8 | 10000000 | 205 ns/op | 0 B/op | 0 allocs/op | 10 | |Benchmark_IsAlphaDash-8 | 5000000 | 268 ns/op | 0 B/op | 0 allocs/op | 11 | |Benchmark_IsAlphaNumeric-8 | 10000000 | 182 ns/op | 0 B/op | 0 allocs/op | 12 | |Benchmark_IsBoolean-8 | 200000000 | 6.84 ns/op | 0 B/op | 0 allocs/op | 13 | |Benchmark_IsCreditCard-8 | 10000000 | 243 ns/op | 0 B/op | 0 allocs/op | 14 | |Benchmark_IsCoordinate-8 | 3000000 | 482 ns/op | 0 B/op | 0 allocs/op | 15 | |Benchmark_IsCSSColor-8 | 10000000 | 160 ns/op | 0 B/op | 0 allocs/op | 16 | |Benchmark_IsDate-8 | 3000000 | 531 ns/op | 0 B/op | 0 allocs/op | 17 | |Benchmark_IsDateDDMMYY-8 | 5000000 | 246 ns/op | 0 B/op | 0 allocs/op | 18 | |Benchmark_IsEmail-8 | 3000000 | 549 ns/op | 0 B/op | 0 allocs/op | 19 | |Benchmark_IsFloat-8 | 10000000 | 199 ns/op | 0 B/op | 0 allocs/op | 20 | |Benchmark_IsIn-8 | 5000000 | 3.77 ns/op | 0 B/op | 0 allocs/op | 21 | |Benchmark_IsJSON-8 | 2000000 | 956 ns/op | 640 B/op | 12 allocs/op | 22 | |Benchmark_IsMacAddress-8 | 5000000 | 277 ns/op | 0 B/op | 0 allocs/op | 23 | |Benchmark_IsNumeric-8 | 20000000 | 110 ns/op | 0 B/op | 0 allocs/op | 24 | |Benchmark_IsLatitude-8 | 5000000 | 249 ns/op | 0 B/op | 0 allocs/op | 25 | |Benchmark_IsLongitude-8 | 5000000 | 250 ns/op | 0 B/op | 0 allocs/op | 26 | |Benchmark_IsIP-8 | 3000000 | 578 ns/op | 0 B/op | 0 allocs/op | 27 | |Benchmark_IsIPV4-8 | 5000000 | 286 ns/op | 0 B/op | 0 allocs/op | 28 | |Benchmark_IsIPV6-8 | 2000000 | 931 ns/op | 0 B/op | 0 allocs/op | 29 | |Benchmark_IsMatchedRegex-8 | 200000 | 5786 ns/op | 4465 B/op | 57 allocs/op | 30 | |Benchmark_IsURL-8 | 2000000 | 866 ns/op | 0 B/op | 0 allocs/op | 31 | |Benchmark_IsUUID-8 | 3000000 | 455 ns/op | 0 B/op | 0 allocs/op | 32 | |Benchmark_IsUUID3-8 | 3000000 | 536 ns/op | 0 B/op | 0 allocs/op | 33 | |Benchmark_IsUUID4-8 | 3000000 | 411 ns/op | 0 B/op | 0 allocs/op | 34 | |Benchmark_IsUUID5-8 | 3000000 | 443 ns/op | 0 B/op | 0 allocs/op | 35 | |BenchmarkRoller_Start-8 | 300000 | 4659 ns/op | 2468 B/op | 28 allocs/op | 36 | |Benchmark_isContainRequiredField-8 | 1000000000 | 2.69 ns/op | 0 B/op | 0 allocs/op | 37 | |Benchmark_Validate-8 | 200000 | 6742 ns/op | 727 B/op | 29 allocs/op | 38 | -------------------------------------------------------------------------------- /doc/CUSTOM_RULE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Validate with custom rule 3 | 4 | You can register custom validation rules. This rule will work for both `Validate` and `ValidateJSON` method. You will get all the information you need to validate an input. 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/thedevsaddam/govalidator" 18 | ) 19 | 20 | func init() { 21 | // custom rules to take fixed length word. 22 | // e.g: max_word:5 will throw error if the field contains more than 5 words 23 | govalidator.AddCustomRule("max_word", func(field string, rule string, message string, value interface{}) error { 24 | valSlice := strings.Fields(value.(string)) 25 | l, _ := strconv.Atoi(strings.TrimPrefix(rule, "max_word:")) //handle other error 26 | if len(valSlice) > l { 27 | if message != "" { 28 | return errors.New(message) 29 | } 30 | return fmt.Errorf("The %s field must not be greater than %d words", field, l) 31 | } 32 | return nil 33 | }) 34 | } 35 | 36 | type article struct { 37 | Title string `json:"title"` 38 | Body string `json:"body"` 39 | Tags []string `json:"tags"` 40 | } 41 | 42 | func handler(w http.ResponseWriter, r *http.Request) { 43 | var article article 44 | rules := govalidator.MapData{ 45 | "title": []string{"between:10,120"}, 46 | "body": []string{"max_word:150"}, // using custom rule max_word 47 | "tags": []string{"between:3,5"}, 48 | } 49 | 50 | opts := govalidator.Options{ 51 | Request: r, 52 | Data: &article, 53 | Rules: rules, 54 | RequiredDefault: true, //force user to fill all the inputs 55 | } 56 | 57 | v := govalidator.New(opts) 58 | e := v.ValidateJSON() 59 | err := map[string]interface{}{"validationError": e} 60 | w.Header().Set("Content-type", "applciation/json") 61 | json.NewEncoder(w).Encode(err) 62 | } 63 | 64 | func main() { 65 | http.HandleFunc("/", handler) 66 | fmt.Println("Listening on port: 9000") 67 | http.ListenAndServe(":9000", nil) 68 | } 69 | 70 | ``` 71 | ***Resposne*** 72 | ```json 73 | { 74 | "validationError": { 75 | "body": [ 76 | "The body field must not be greater than 150 words" 77 | ], 78 | "tags": [ 79 | "The tags field must be between 3 and 5" 80 | ], 81 | "title": [ 82 | "The title field must be between 10 and 120" 83 | ] 84 | } 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /doc/FILE_VALIDATION.md: -------------------------------------------------------------------------------- 1 | 2 | ### Validate File 3 | 4 | For `multipart/form-data` validation, use `file:` prefix to _field_ name which contains the file. If use custom message then also use the `file:` prefix to Messages MapData key. 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "net/http" 13 | 14 | "github.com/thedevsaddam/govalidator" 15 | ) 16 | 17 | func handler(w http.ResponseWriter, r *http.Request) { 18 | rules := govalidator.MapData{ 19 | "file:photo": []string{"ext:jpg,png", "size:10000", "mime:jpg,png", "required"}, 20 | } 21 | 22 | messages := govalidator.MapData{ 23 | "file:photo": []string{"ext:Only jpg/png is allowed", "required:Photo is required"}, 24 | } 25 | 26 | opts := govalidator.Options{ 27 | Request: r, // request object 28 | Rules: rules, // rules map, 29 | Messages: messages, 30 | } 31 | v := govalidator.New(opts) 32 | e := v.Validate() 33 | err := map[string]interface{}{"validationError": e} 34 | w.Header().Set("Content-type", "applciation/json") 35 | json.NewEncoder(w).Encode(err) 36 | } 37 | 38 | func main() { 39 | http.HandleFunc("/", handler) 40 | fmt.Println("Listening on port: 9000") 41 | http.ListenAndServe(":9000", nil) 42 | } 43 | 44 | ``` 45 | ***Resposne*** 46 | ```json 47 | { 48 | "validationError": { 49 | "photo": [ 50 | "Photo is required" 51 | ] 52 | } 53 | } 54 | 55 | or 56 | 57 | { 58 | "validationError": { 59 | "photo": [ 60 | "Only jpg/png is allowed", 61 | "The photo field size is can not be greater than 10000 bytes", 62 | "The photo field file mime text/plain is invalid" 63 | ] 64 | } 65 | } 66 | ``` 67 | Note: At this time it can validate only single file. 68 | -------------------------------------------------------------------------------- /doc/MAP_VALIDATION.md: -------------------------------------------------------------------------------- 1 | ### Validate JSON body into Map 2 | 3 | When using ValidateJSON you must provide data struct or map, rules and request. You can also pass message rules if you need custom message or localization. 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "net/http" 12 | 13 | "github.com/thedevsaddam/govalidator" 14 | ) 15 | 16 | func handler(w http.ResponseWriter, r *http.Request) { 17 | rules := govalidator.MapData{ 18 | "username": []string{"required", "between:3,5"}, 19 | "email": []string{"required", "min:4", "max:20", "email"}, 20 | "web": []string{"url"}, 21 | "age": []string{"numeric_between:18,56"}, 22 | } 23 | 24 | data := make(map[string]interface{}, 0) 25 | 26 | opts := govalidator.Options{ 27 | Request: r, 28 | Rules: rules, 29 | Data: &data, 30 | } 31 | 32 | vd := govalidator.New(opts) 33 | e := vd.ValidateJSON() 34 | fmt.Println(data) 35 | err := map[string]interface{}{"validation error": e} 36 | w.Header().Set("Content-type", "application/json") 37 | json.NewEncoder(w).Encode(err) 38 | } 39 | 40 | func main() { 41 | http.HandleFunc("/", handler) 42 | fmt.Println("Listening on port: 9000") 43 | http.ListenAndServe(":9000", nil) 44 | } 45 | 46 | ``` 47 | 48 | ***Resposne*** 49 | ```json 50 | { 51 | "validationError": { 52 | "age": [ 53 | "The age field must be between 18 and 56" 54 | ], 55 | "dob": [ 56 | "The dob field must be a valid date format. e.g: yyyy-mm-dd, yyyy/mm/dd etc" 57 | ], 58 | "email": [ 59 | "The email field is required", 60 | "The email field must be a valid email address" 61 | ], 62 | "phone": [ 63 | "The phone field must be 11 digits" 64 | ], 65 | "postalCode": [ 66 | "The postalCode field must be 4 digits" 67 | ], 68 | "roles": [ 69 | "The roles field must be length of 4" 70 | ], 71 | "username": [ 72 | "The username field is required", 73 | "The username field must be between 3 and 8" 74 | ], 75 | "village": [ 76 | "The village field must be between 3 and 10" 77 | ], 78 | "web": [ 79 | "The web field format is invalid" 80 | ] 81 | } 82 | } 83 | ``` 84 | 85 | Note: You can pass custom message 86 | -------------------------------------------------------------------------------- /doc/NESTED_STRUCT.md: -------------------------------------------------------------------------------- 1 | 2 | ### Validate JSON body with nested struct and slice 3 | 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "net/http" 12 | 13 | "github.com/thedevsaddam/govalidator" 14 | ) 15 | 16 | type ( 17 | user struct { 18 | Username string `json:"username"` 19 | Email string `json:"email"` 20 | Web string `json:"web"` 21 | Age int `json:"age"` 22 | Phone string `json:"phone"` 23 | Agree bool `json:"agree"` 24 | DOB string `json:"dob"` 25 | Address address 26 | Roles []string `json:"roles"` 27 | } 28 | 29 | address struct { 30 | Village string `json:"village"` 31 | PostalCode string `json:"postalCode"` 32 | } 33 | ) 34 | 35 | func handler(w http.ResponseWriter, r *http.Request) { 36 | var usr user 37 | rules := govalidator.MapData{ 38 | "username": []string{"required", "between:3,8"}, 39 | "email": []string{"required", "min:4", "max:20", "email"}, 40 | "web": []string{"url"}, 41 | "age": []string{"between:18,56"}, 42 | "phone": []string{"digits:11"}, 43 | "agree": []string{"bool"}, 44 | "dob": []string{"date"}, 45 | "village": []string{"between:3,10"}, 46 | "postalCode": []string{"digits:4"}, 47 | "roles": []string{"len:4"}, 48 | } 49 | opts := govalidator.Options{ 50 | Request: r, // request object 51 | Rules: rules, // rules map 52 | Data: &usr, 53 | RequiredDefault: true, // all the field to be required 54 | } 55 | v := govalidator.New(opts) 56 | e := v.ValidateJSON() 57 | fmt.Println(usr) 58 | err := map[string]interface{}{"validationError": e} 59 | w.Header().Set("Content-type", "applciation/json") 60 | json.NewEncoder(w).Encode(err) 61 | } 62 | 63 | func main() { 64 | http.HandleFunc("/", handler) 65 | fmt.Println("Listening on port: 9000") 66 | http.ListenAndServe(":9000", nil) 67 | } 68 | 69 | ``` 70 | ***Resposne*** 71 | ```json 72 | { 73 | "validationError": { 74 | "email": [ 75 | "The email field must be minimum 4 char", 76 | "The email field must be a valid email address" 77 | ], 78 | "phone": [ 79 | "The phone field must be 11 digits" 80 | ], 81 | "postalCode": [ 82 | "The postalCode field must be 4 digits" 83 | ], 84 | "roles": [ 85 | "The roles field must be length of 4" 86 | ], 87 | "village": [ 88 | "The village field must be between 3 and 10" 89 | ], 90 | "web": [ 91 | "The web field format is invalid" 92 | ] 93 | } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /doc/SIMPLE_STRUCT_VALIDATION.md: -------------------------------------------------------------------------------- 1 | 2 | ### Validate JSON body into a simple Struct 3 | 4 | When using ValidateJSON you must provide data struct or map, rules and request. You can also pass message rules if you need custom message or localization. 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "net/http" 13 | 14 | "github.com/thedevsaddam/govalidator" 15 | ) 16 | 17 | type user struct { 18 | Username string `json:"username"` 19 | Email string `json:"email"` 20 | Web string `json:"web"` 21 | Age govalidator.Int `json:"age"` 22 | Agree govalidator.Bool `json:"agree"` 23 | } 24 | 25 | func handler(w http.ResponseWriter, r *http.Request) { 26 | var user user 27 | rules := govalidator.MapData{ 28 | "username": []string{"required", "between:3,5"}, 29 | "email": []string{"required", "min:4", "max:20", "email"}, 30 | "web": []string{"url"}, 31 | "age": []string{"required"}, 32 | "agree": []string{"required"}, 33 | } 34 | 35 | opts := govalidator.Options{ 36 | Request: r, 37 | Data: &user, 38 | Rules: rules, 39 | } 40 | 41 | v := govalidator.New(opts) 42 | e := v.ValidateJSON() 43 | fmt.Println(user) // your incoming JSON data in Go data struct 44 | err := map[string]interface{}{"validationError": e} 45 | w.Header().Set("Content-type", "application/json") 46 | json.NewEncoder(w).Encode(err) 47 | } 48 | 49 | func main() { 50 | http.HandleFunc("/", handler) 51 | fmt.Println("Listening on port: 9000") 52 | http.ListenAndServe(":9000", nil) 53 | } 54 | 55 | ``` 56 | ***Response*** 57 | ```json 58 | { 59 | "validationError": { 60 | "age": [ 61 | "The age field is required" 62 | ], 63 | "agree": [ 64 | "The agree field is required" 65 | ], 66 | "email": [ 67 | "The email field is required", 68 | "The email field must be minimum 4 char", 69 | "The email field must be a valid email address" 70 | ], 71 | "username": [ 72 | "The username field is required", 73 | "The username field must be between 3 and 5" 74 | ] 75 | } 76 | } 77 | ``` 78 | 79 | #### Note: When using `required` rule with number or boolean data, use provided custom type like: Int, Int64, Float32, Float64 or Bool 80 | -------------------------------------------------------------------------------- /doc/STRUCT_VALIDATION.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Validate Struct 4 | 5 | When using ValidateStruct you must provide data struct and rules. You can also pass message rules if you need custom message or localization. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | 14 | "github.com/thedevsaddam/govalidator" 15 | ) 16 | 17 | type user struct { 18 | Username string `json:"username"` 19 | Email string `json:"email"` 20 | Web string `json:"web"` 21 | } 22 | 23 | func validate(user *user) { 24 | rules := govalidator.MapData{ 25 | "username": []string{"required", "between:3,5"}, 26 | "email": []string{"required", "min:4", "max:20", "email"}, 27 | "web": []string{"url"}, 28 | } 29 | 30 | opts := govalidator.Options{ 31 | Data: &user, 32 | Rules: rules, 33 | } 34 | 35 | v := govalidator.New(opts) 36 | e := v.ValidateStruct() 37 | if len(e) > 0 { 38 | data, _ := json.MarshalIndent(e, "", " ") 39 | fmt.Println(string(data)) 40 | } 41 | } 42 | 43 | func main() { 44 | validate(&user{ 45 | Username: "john", 46 | Email: "invalid", 47 | }) 48 | } 49 | ``` 50 | ***Prints*** 51 | ```json 52 | { 53 | "email": [ 54 | "The email field is required", 55 | "The email field must be a valid email address" 56 | ], 57 | "username": [ 58 | "The username field is required" 59 | ] 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import "errors" 4 | 5 | var ( 6 | errStringToInt = errors.New("govalidator: unable to parse string to integer") 7 | errStringToFloat = errors.New("govalidator: unable to parse string to float") 8 | errRequireRules = errors.New("govalidator: provide at least rules for Validate* method") 9 | errValidateArgsMismatch = errors.New("govalidator: provide at least *http.Request and rules for Validate method") 10 | errInvalidArgument = errors.New("govalidator: invalid number of argument") 11 | errRequirePtr = errors.New("govalidator: provide pointer to the data structure") 12 | errRequireData = errors.New("govalidator: provide non-nil data structure for ValidateStruct method") 13 | errRequestNotAccepted = errors.New("govalidator: cannot provide an *http.Request for ValidateStruct method") 14 | ) 15 | -------------------------------------------------------------------------------- /govalidator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevsaddam/govalidator/410bf76327c31ef88a2cc858060b4016dcb80bb3/govalidator.png -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | ) 7 | 8 | // isAlpha check the input is letters (a-z,A-Z) or not 9 | func isAlpha(str string) bool { 10 | return regexAlpha.MatchString(str) 11 | } 12 | 13 | // isAlphaDash check the input is letters, number with dash and underscore 14 | func isAlphaDash(str string) bool { 15 | return regexAlphaDash.MatchString(str) 16 | } 17 | 18 | // isAlphaSpace check the input is letters, number with dash and underscore 19 | func isAlphaSpace(str string) bool { 20 | return regexAlphaSpace.MatchString(str) 21 | } 22 | 23 | // isAlphaNumeric check the input is alpha numeric or not 24 | func isAlphaNumeric(str string) bool { 25 | return regexAlphaNumeric.MatchString(str) 26 | } 27 | 28 | // isBoolean check the input contains boolean type values 29 | // in this case: "0", "1", "true", "false", "True", "False" 30 | func isBoolean(str string) bool { 31 | bools := []string{"0", "1", "true", "false", "True", "False"} 32 | for _, b := range bools { 33 | if b == str { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | //isCreditCard check the provided card number is a valid 41 | // Visa, MasterCard, American Express, Diners Club, Discover or JCB card 42 | func isCreditCard(card string) bool { 43 | return regexCreditCard.MatchString(card) 44 | } 45 | 46 | // isCoordinate is a valid Coordinate or not 47 | func isCoordinate(str string) bool { 48 | return regexCoordinate.MatchString(str) 49 | } 50 | 51 | // isCSSColor is a valid CSS color value (hex, rgb, rgba, hsl, hsla) etc like #909, #00aaff, rgb(255,122,122) 52 | func isCSSColor(str string) bool { 53 | return regexCSSColor.MatchString(str) 54 | } 55 | 56 | // isDate check the date string is valid or not 57 | func isDate(date string) bool { 58 | return regexDate.MatchString(date) 59 | } 60 | 61 | // isDateDDMMYY check the date string is valid or not 62 | func isDateDDMMYY(date string) bool { 63 | return regexDateDDMMYY.MatchString(date) 64 | } 65 | 66 | // isEmail check a email is valid or not 67 | func isEmail(email string) bool { 68 | return regexEmail.MatchString(email) 69 | } 70 | 71 | // isFloat check the input string is a float or not 72 | func isFloat(str string) bool { 73 | return regexFloat.MatchString(str) 74 | } 75 | 76 | // isIn check if the niddle exist in the haystack 77 | func isIn(haystack []string, niddle string) bool { 78 | for _, h := range haystack { 79 | if h == niddle { 80 | return true 81 | } 82 | } 83 | return false 84 | } 85 | 86 | // isJSON check wheather the input string is a valid json or not 87 | func isJSON(str string) bool { 88 | var data interface{} 89 | if err := json.Unmarshal([]byte(str), &data); err != nil { 90 | return false 91 | } 92 | return true 93 | } 94 | 95 | // isNumeric check the provided input string is numeric or not 96 | func isNumeric(str string) bool { 97 | return regexNumeric.MatchString(str) 98 | } 99 | 100 | // isMacAddres check the provided string is valid Mac Address or not 101 | func isMacAddress(str string) bool { 102 | return regexMacAddress.MatchString(str) 103 | } 104 | 105 | // isLatitude check the provided input string is a valid latitude or not 106 | func isLatitude(str string) bool { 107 | return regexLatitude.MatchString(str) 108 | } 109 | 110 | // isLongitude check the provided input string is a valid longitude or not 111 | func isLongitude(str string) bool { 112 | return regexLongitude.MatchString(str) 113 | } 114 | 115 | // isIP check the provided input string is a valid IP address or not 116 | func isIP(str string) bool { 117 | return regexIP.MatchString(str) 118 | } 119 | 120 | // isIPV4 check the provided input string is a valid IP address version 4 or not 121 | // Ref: https://en.wikipedia.org/wiki/IPv4 122 | func isIPV4(str string) bool { 123 | return regexIPV4.MatchString(str) 124 | } 125 | 126 | // isIPV6 check the provided input string is a valid IP address version 6 or not 127 | // Ref: https://en.wikipedia.org/wiki/IPv6 128 | func isIPV6(str string) bool { 129 | return regexIPV6.MatchString(str) 130 | } 131 | 132 | // isMatchedRegex match the regular expression string provided in first argument 133 | // with second argument which is also a string 134 | func isMatchedRegex(rxStr, str string) bool { 135 | rx := regexp.MustCompile(rxStr) 136 | return rx.MatchString(str) 137 | } 138 | 139 | // isURL check a URL is valid or not 140 | func isURL(url string) bool { 141 | return regexURL.MatchString(url) 142 | } 143 | 144 | // isUUID check the provided string is valid UUID or not 145 | func isUUID(str string) bool { 146 | return regexUUID.MatchString(str) 147 | } 148 | 149 | // isUUID3 check the provided string is valid UUID version 3 or not 150 | func isUUID3(str string) bool { 151 | return regexUUID3.MatchString(str) 152 | } 153 | 154 | // isUUID4 check the provided string is valid UUID version 4 or not 155 | func isUUID4(str string) bool { 156 | return regexUUID4.MatchString(str) 157 | } 158 | 159 | // isUUID5 check the provided string is valid UUID version 5 or not 160 | func isUUID5(str string) bool { 161 | return regexUUID5.MatchString(str) 162 | } 163 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type inputs map[string]bool 8 | 9 | var ( 10 | _alpha = inputs{ 11 | "abcdefghijgklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": true, 12 | "7877**": false, 13 | "abc": true, 14 | ")(^%&)": false, 15 | } 16 | _alphaDash = inputs{ 17 | "abcdefghijgklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-": true, 18 | "John_Do-E": true, 19 | "+=a(0)": false, 20 | } 21 | _alphaNumeric = inputs{ 22 | "abcdefghijgklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890": true, 23 | "090a": true, 24 | "*&*)": false, 25 | } 26 | _boolStringsList = inputs{ 27 | "0": true, 28 | "1": true, 29 | "true": true, 30 | "false": true, 31 | "o": false, 32 | "a": false, 33 | } 34 | // Ref: https://www.freeformatter.com/credit-card-number-generator-validator.html 35 | _creditCardList = inputs{ 36 | "4896644531043572": true, 37 | "2221005631780408": true, 38 | "349902515380498": true, 39 | "6011843157272458": true, 40 | "3543358904915048": true, 41 | "5404269782892303": true, 42 | "4508168417293390": true, 43 | "0604595245598387": false, 44 | "6388244169973297": false, 45 | } 46 | _coordinateList = inputs{ 47 | "30.297018,-78.486328": true, 48 | "40.044438,-104.0625": true, 49 | "58.068581,-99.580078": true, 50 | "abc, xyz": false, 51 | "0, 887": false, 52 | } 53 | _cssColorList = inputs{ 54 | "#000": true, 55 | "#00aaff": true, 56 | "rgb(123,32,12)": true, 57 | "#0": false, 58 | "#av": false, 59 | } 60 | _dateList = inputs{ 61 | "2016-10-14": true, 62 | "2013/02/18": true, 63 | "2020/12/30": true, 64 | "0001/14/30": false, 65 | } 66 | _dateDDMMYYList = inputs{ 67 | "01-01-2000": true, 68 | "28/02/2001": true, 69 | "01/12/2000": true, 70 | "2012/11/30": false, 71 | "201/11/30": false, 72 | } 73 | _emailList = inputs{ 74 | "john@example.com": true, 75 | "thedevsaddam@gmail.com": true, 76 | "jane@yahoo.com": true, 77 | "janeahoo.com": false, 78 | "janea@.com": false, 79 | } 80 | _floatList = inputs{"123": true, "12.50": true, "33.07": true, "abc": false, "o0.45": false, "100000000000000": true, "100000000000000.189": true} 81 | _roleList = []string{"admin", "manager", "supervisor"} 82 | _validJSONString = `{"FirstName": "Bob", "LastName": "Smith"}` 83 | _invalidJSONString = `{"invalid json"}` 84 | _macaddressList = inputs{ 85 | "fc:40:2e:f1:d3:6f": true, 86 | "87:7a:45:f6:8b:ed": true, 87 | "a5:91:91:80:d2:fd": true, 88 | "1f:ce:44:46:24:b4": true, 89 | "00:02:x2:34:72:a5": false, 90 | } 91 | _numericStringList = inputs{"12": true, "09": true, "878": true, "100": true, "a": false, "xyz": false, "1000000000000": true} 92 | _latList = inputs{"30.297018": true, "40.044438": true, "a": false, "xyz": false} 93 | _lonList = inputs{"-78.486328": true, "-104.0625": true, "a": false, "xyz": false} 94 | _ipList = inputs{"10.255.255.255": true, "172.31.255.255": true, "192.168.255.255": true, "a92.168.255.255": false, "172.31.255.25b": false} 95 | _ipV6List = inputs{ 96 | "1200:0000:AB00:1234:0000:2552:7777:1313": true, 97 | "21DA:D3:0:2F3B:2AA:FF:FE28:9C5A": true, 98 | "10.255.255.255": false, 99 | } 100 | _urlList = inputs{ 101 | "http://www.google.com": true, 102 | "https://www.google.com": true, 103 | "https://facebook.com": true, 104 | "yahoo.com": true, 105 | "adca": false, 106 | } 107 | _uuidList = inputs{ 108 | "ee7cf0a0-1922-401b-a1ae-6ec9261484c0": true, 109 | "ee7cf0a0-1922-401b-a1ae-6ec9261484c1": true, 110 | "ee7cf0a0-1922-401b-a1ae-6ec9261484a0": true, 111 | "39888f87-fb62-5988-a425-b2ea63f5b81e": false, 112 | } 113 | _uuidV3List = inputs{ 114 | "a987fbc9-4bed-3078-cf07-9141ba07c9f3": true, 115 | "b987fbc9-4bed-3078-cf07-9141ba07c9f3": true, 116 | "ee7cf0a0-1922-401b-a1ae-6ec9261484c0": false, 117 | } 118 | _uuidV4List = inputs{ 119 | "df7cca36-3d7a-40f4-8f06-ae03cc22f045": true, 120 | "ef7cca36-3d7a-40f4-8f06-ae03cc22f048": true, 121 | "b987fbc9-4bed-3078-cf07-9141ba07c9f3": false, 122 | } 123 | _uuidV5List = inputs{ 124 | "39888f87-fb62-5988-a425-b2ea63f5b81e": true, 125 | "33388f87-fb62-5988-a425-b2ea63f5b81f": true, 126 | "b987fbc9-4bed-3078-cf07-9141ba07c9f3": false, 127 | } 128 | ) 129 | 130 | func Test_IsAlpha(t *testing.T) { 131 | for a, s := range _alpha { 132 | if isAlpha(a) != s { 133 | t.Error("IsAlpha failed to determine alpha!") 134 | } 135 | } 136 | } 137 | 138 | func Benchmark_IsAlpha(b *testing.B) { 139 | for n := 0; n < b.N; n++ { 140 | isAlpha("abcdAXZY") 141 | } 142 | } 143 | 144 | func Test_IsAlphaDash(t *testing.T) { 145 | for a, s := range _alphaDash { 146 | if isAlphaDash(a) != s { 147 | t.Error("IsAlphaDash failed to determine alpha dash!") 148 | } 149 | } 150 | } 151 | 152 | func Benchmark_IsAlphaDash(b *testing.B) { 153 | for n := 0; n < b.N; n++ { 154 | isAlphaDash("John_Do-E") 155 | } 156 | } 157 | 158 | func Test_IsAlphaNumeric(t *testing.T) { 159 | for a, s := range _alphaNumeric { 160 | if isAlphaNumeric(a) != s { 161 | t.Error("IsAlphaNumeric failed to determine alpha numeric!") 162 | } 163 | } 164 | } 165 | 166 | func Benchmark_IsAlphaNumeric(b *testing.B) { 167 | for n := 0; n < b.N; n++ { 168 | isAlphaNumeric("abc12AZ") 169 | } 170 | } 171 | 172 | func Test_IsBoolean(t *testing.T) { 173 | for b, s := range _boolStringsList { 174 | if isBoolean(b) != s { 175 | t.Error("IsBoolean failed to determine boolean!") 176 | } 177 | } 178 | } 179 | 180 | func Benchmark_IsBoolean(b *testing.B) { 181 | for n := 0; n < b.N; n++ { 182 | isBoolean("true") 183 | } 184 | } 185 | 186 | func Test_IsCreditCard(t *testing.T) { 187 | for card, state := range _creditCardList { 188 | if isCreditCard(card) != state { 189 | t.Error("IsCreditCard failed to determine credit card!") 190 | } 191 | } 192 | } 193 | 194 | func Benchmark_IsCreditCard(b *testing.B) { 195 | for n := 0; n < b.N; n++ { 196 | isCreditCard("2221005631780408") 197 | } 198 | } 199 | 200 | func Test_IsCoordinate(t *testing.T) { 201 | for c, s := range _coordinateList { 202 | if isCoordinate(c) != s { 203 | t.Error("IsCoordinate failed to determine coordinate!") 204 | } 205 | } 206 | } 207 | 208 | func Benchmark_IsCoordinate(b *testing.B) { 209 | for n := 0; n < b.N; n++ { 210 | isCoordinate("30.297018,-78.486328") 211 | } 212 | } 213 | 214 | func Test_IsCSSColor(t *testing.T) { 215 | for c, s := range _cssColorList { 216 | if isCSSColor(c) != s { 217 | t.Error("IsCSSColor failed to determine css color code!") 218 | } 219 | } 220 | } 221 | 222 | func Benchmark_IsCSSColor(b *testing.B) { 223 | for n := 0; n < b.N; n++ { 224 | isCSSColor("#00aaff") 225 | } 226 | } 227 | 228 | func Test_IsDate(t *testing.T) { 229 | for d, s := range _dateList { 230 | if isDate(d) != s { 231 | t.Error("IsDate failed to determine date!") 232 | } 233 | } 234 | } 235 | 236 | func Benchmark_IsDate(b *testing.B) { 237 | for n := 0; n < b.N; n++ { 238 | isDate("2016-10-14") 239 | } 240 | } 241 | 242 | func Test_IsDateDDMMYY(t *testing.T) { 243 | for d, s := range _dateDDMMYYList { 244 | if isDateDDMMYY(d) != s { 245 | t.Error("IsDateDDMMYY failed to determine date!") 246 | } 247 | } 248 | } 249 | 250 | func Benchmark_IsDateDDMMYY(b *testing.B) { 251 | for n := 0; n < b.N; n++ { 252 | isDateDDMMYY("23-10-2014") 253 | } 254 | } 255 | 256 | func Test_IsEmail(t *testing.T) { 257 | for e, s := range _emailList { 258 | if isEmail(e) != s { 259 | t.Error("IsEmail failed to determine email!") 260 | } 261 | } 262 | } 263 | 264 | func Benchmark_IsEmail(b *testing.B) { 265 | for n := 0; n < b.N; n++ { 266 | isEmail("thedevsaddam@gmail.com") 267 | } 268 | } 269 | 270 | func Test_IsFloat(t *testing.T) { 271 | for f, s := range _floatList { 272 | if isFloat(f) != s { 273 | t.Error("IsFloat failed to determine float value!") 274 | } 275 | } 276 | } 277 | 278 | func Benchmark_IsFloat(b *testing.B) { 279 | for n := 0; n < b.N; n++ { 280 | isFloat("123.001") 281 | } 282 | } 283 | 284 | func Test_IsIn(t *testing.T) { 285 | if !isIn(_roleList, "admin") { 286 | t.Error("IsIn failed!") 287 | } 288 | } 289 | 290 | func Benchmark_IsIn(b *testing.B) { 291 | for n := 0; n < b.N; n++ { 292 | isIn(_roleList, "maager") 293 | } 294 | } 295 | 296 | func Test_IsJSON(t *testing.T) { 297 | if !isJSON(_validJSONString) { 298 | t.Error("IsJSON failed!") 299 | } 300 | if isJSON(_invalidJSONString) { 301 | t.Error("IsJSON unable to detect invalid json!") 302 | } 303 | } 304 | 305 | func Benchmark_IsJSON(b *testing.B) { 306 | for n := 0; n < b.N; n++ { 307 | isJSON(_validJSONString) 308 | } 309 | } 310 | 311 | func Test_IsMacAddress(t *testing.T) { 312 | for n, s := range _macaddressList { 313 | if isMacAddress(n) != s { 314 | t.Error("IsMacAddress failed!") 315 | } 316 | } 317 | } 318 | 319 | func Benchmark_IsMacAddress(b *testing.B) { 320 | for n := 0; n < b.N; n++ { 321 | isMacAddress("00:02:02:34:72:a5") 322 | } 323 | } 324 | 325 | func Test_IsNumeric(t *testing.T) { 326 | for n, s := range _numericStringList { 327 | if isNumeric(n) != s { 328 | t.Error("IsNumeric failed!") 329 | } 330 | } 331 | } 332 | 333 | func Benchmark_IsNumeric(b *testing.B) { 334 | for n := 0; n < b.N; n++ { 335 | isNumeric("123") 336 | } 337 | } 338 | 339 | func Test_IsLatitude(t *testing.T) { 340 | for n, s := range _latList { 341 | if isLatitude(n) != s { 342 | t.Error("IsLatitude failed!") 343 | } 344 | } 345 | } 346 | 347 | func Benchmark_IsLatitude(b *testing.B) { 348 | for n := 0; n < b.N; n++ { 349 | isLatitude("30.297018") 350 | } 351 | } 352 | 353 | func Test_IsLongitude(t *testing.T) { 354 | for n, s := range _lonList { 355 | if isLongitude(n) != s { 356 | t.Error("IsLongitude failed!") 357 | } 358 | } 359 | } 360 | 361 | func Benchmark_IsLongitude(b *testing.B) { 362 | for n := 0; n < b.N; n++ { 363 | isLongitude("-78.486328") 364 | } 365 | } 366 | 367 | func Test_IsIP(t *testing.T) { 368 | for i, s := range _ipList { 369 | if isIP(i) != s { 370 | t.Error("IsIP failed!") 371 | } 372 | } 373 | } 374 | 375 | func Benchmark_IsIP(b *testing.B) { 376 | for n := 0; n < b.N; n++ { 377 | isIP("10.255.255.255") 378 | } 379 | } 380 | 381 | func Test_IsIPV4(t *testing.T) { 382 | for i, s := range _ipList { 383 | if isIPV4(i) != s { 384 | t.Error("IsIPV4 failed!") 385 | } 386 | } 387 | } 388 | 389 | func Benchmark_IsIPV4(b *testing.B) { 390 | for n := 0; n < b.N; n++ { 391 | isIPV4("10.255.255.255") 392 | } 393 | } 394 | 395 | func Test_IsIPV6(t *testing.T) { 396 | for i, s := range _ipV6List { 397 | if isIPV6(i) != s { 398 | t.Error("IsIPV4 failed!") 399 | } 400 | } 401 | } 402 | 403 | func Benchmark_IsIPV6(b *testing.B) { 404 | for n := 0; n < b.N; n++ { 405 | isIPV6("10.255.255.255") 406 | } 407 | } 408 | 409 | func Test_IsMatchedRegex(t *testing.T) { 410 | if !isMatchedRegex("^(name|age)$", "name") { 411 | t.Error("IsMatchedRegex failed!") 412 | } 413 | } 414 | 415 | func Benchmark_IsMatchedRegex(b *testing.B) { 416 | for n := 0; n < b.N; n++ { 417 | isMatchedRegex("^(name|age)$", "name") 418 | } 419 | } 420 | 421 | func Test_IsURL(t *testing.T) { 422 | for u, s := range _urlList { 423 | if isURL(u) != s { 424 | t.Error("IsURL failed!") 425 | } 426 | } 427 | } 428 | 429 | func Benchmark_IsURL(b *testing.B) { 430 | for n := 0; n < b.N; n++ { 431 | isURL("https://www.facebook.com") 432 | } 433 | } 434 | 435 | func Test_IsUUID(t *testing.T) { 436 | for u, s := range _uuidList { 437 | if isUUID(u) != s { 438 | t.Error("IsUUID failed!") 439 | } 440 | } 441 | } 442 | 443 | func Benchmark_IsUUID(b *testing.B) { 444 | for n := 0; n < b.N; n++ { 445 | isUUID("ee7cf0a0-1922-401b-a1ae-6ec9261484c0") 446 | } 447 | } 448 | 449 | func Test_IsUUID3(t *testing.T) { 450 | for u, s := range _uuidV3List { 451 | if isUUID3(u) != s { 452 | t.Error("IsUUID3 failed!") 453 | } 454 | } 455 | } 456 | 457 | func Benchmark_IsUUID3(b *testing.B) { 458 | for n := 0; n < b.N; n++ { 459 | isUUID3("a987fbc9-4bed-3078-cf07-9141ba07c9f3") 460 | } 461 | } 462 | 463 | func Test_IsUUID4(t *testing.T) { 464 | for u, s := range _uuidV4List { 465 | if isUUID4(u) != s { 466 | t.Error("IsUUID4 failed!") 467 | } 468 | } 469 | } 470 | 471 | func Benchmark_IsUUID4(b *testing.B) { 472 | for n := 0; n < b.N; n++ { 473 | isUUID4("57b73598-8764-4ad0-a76a-679bb6640eb1") 474 | } 475 | } 476 | 477 | func Test_IsUUID5(t *testing.T) { 478 | for u, s := range _uuidV5List { 479 | if isUUID5(u) != s { 480 | t.Error("IsUUID5 failed!") 481 | } 482 | } 483 | } 484 | 485 | func Benchmark_IsUUID5(b *testing.B) { 486 | for n := 0; n < b.N; n++ { 487 | isUUID5("987fbc97-4bed-5078-9f07-9141ba07c9f3") 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /regex_patterns.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | const ( 8 | // Alpha represents regular expression for alpha characters 9 | Alpha string = "^[a-zA-Z]+$" 10 | // AlphaDash represents regular expression for alpha characters with underscore and dash 11 | AlphaDash string = "^[a-zA-Z0-9_-]+$" 12 | // AlphaSpace represents regular expression for alpha characters with underscore, space and dash 13 | AlphaSpace string = "^[-a-zA-Z0-9_ ]+$" 14 | // AlphaNumeric represents regular expression for alpha numeric characters 15 | AlphaNumeric string = "^[a-zA-Z0-9]+$" 16 | // CreditCard represents regular expression for credit cards like (Visa, MasterCard, American Express, Diners Club, Discover, and JCB cards). Ref: https://stackoverflow.com/questions/9315647/regex-credit-card-number-tests 17 | CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|[25][1-7][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$" 18 | // Coordinate represents latitude and longitude regular expression 19 | Coordinate string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?),\\s*[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$" // Ref: https://stackoverflow.com/questions/3518504/regular-expression-for-matching-latitude-longitude-coordinates 20 | // CSSColor represents css valid color code with hex, rgb, rgba, hsl, hsla etc. Ref: http://www.regexpal.com/97509 21 | CSSColor string = "^(#([\\da-f]{3}){1,2}|(rgb|hsl)a\\((\\d{1,3}%?,\\s?){3}(1|0?\\.\\d+)\\)|(rgb|hsl)\\(\\d{1,3}%?(,\\s?\\d{1,3}%?){2}\\))$" 22 | // Date represents regular expression for valid date like: yyyy-mm-dd 23 | Date string = "^(((19|20)([2468][048]|[13579][26]|0[48])|2000)[/-]02[/-]29|((19|20)[0-9]{2}[/-](0[469]|11)[/-](0[1-9]|[12][0-9]|30)|(19|20)[0-9]{2}[/-](0[13578]|1[02])[/-](0[1-9]|[12][0-9]|3[01])|(19|20)[0-9]{2}[/-]02[/-](0[1-9]|1[0-9]|2[0-8])))$" 24 | // DateDDMMYY represents regular expression for valid date of format dd/mm/yyyy , dd-mm-yyyy etc.Ref: http://regexr.com/346hf 25 | DateDDMMYY string = "^(0?[1-9]|[12][0-9]|3[01])[\\/\\-](0?[1-9]|1[012])[\\/\\-]\\d{4}$" 26 | // Digits represents regular epxression for validating digits 27 | Digits string = "^[+-]?([0-9]*\\.?[0-9]+|[0-9]+\\.?[0-9]*)([eE][+-]?[0-9]+)?$" 28 | // Email represents regular expression for email 29 | Email string = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$" 30 | // Float represents regular expression for finding float number 31 | Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$" 32 | // IP represents regular expression for ip address 33 | IP string = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" 34 | // IPV4 represents regular expression for ip address version 4 35 | IPV4 string = "^([0-9]{1,3}\\.){3}[0-9]{1,3}(\\/([0-9]|[1-2][0-9]|3[0-2]))?$" 36 | // IPV6 represents regular expression for ip address version 6 37 | IPV6 string = `^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$` 38 | // Latitude represents latitude regular expression 39 | Latitude string = "^(\\+|-)?(?:90(?:(?:\\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\\.[0-9]{1,6})?))$" 40 | // Longitude represents longitude regular expression 41 | Longitude string = "^(\\+|-)?(?:180(?:(?:\\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\\.[0-9]{1,6})?))$" 42 | // MacAddress represents regular expression for mac address 43 | MacAddress string = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" 44 | // Numeric represents regular expression for numeric 45 | Numeric string = "^-?[0-9]+$" 46 | // URL represents regular expression for url 47 | URL string = "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$" // Ref: https://stackoverflow.com/questions/136505/searching-for-uuids-in-text-with-regex 48 | // UUID represents regular expression for UUID 49 | UUID string = "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$" 50 | // UUID3 represents regular expression for UUID version 3 51 | UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" 52 | // UUID4 represents regular expression for UUID version 4 53 | UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 54 | // UUID5 represents regular expression for UUID version 5 55 | UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 56 | ) 57 | 58 | var ( 59 | regexAlpha = regexp.MustCompile(Alpha) 60 | regexAlphaDash = regexp.MustCompile(AlphaDash) 61 | regexAlphaSpace = regexp.MustCompile(AlphaSpace) 62 | regexAlphaNumeric = regexp.MustCompile(AlphaNumeric) 63 | regexCreditCard = regexp.MustCompile(CreditCard) 64 | regexCoordinate = regexp.MustCompile(Coordinate) 65 | regexCSSColor = regexp.MustCompile(CSSColor) 66 | regexDate = regexp.MustCompile(Date) 67 | regexDateDDMMYY = regexp.MustCompile(DateDDMMYY) 68 | regexDigits = regexp.MustCompile(Digits) 69 | regexEmail = regexp.MustCompile(Email) 70 | regexFloat = regexp.MustCompile(Float) 71 | regexMacAddress = regexp.MustCompile(MacAddress) 72 | regexNumeric = regexp.MustCompile(Numeric) 73 | regexLatitude = regexp.MustCompile(Latitude) 74 | regexLongitude = regexp.MustCompile(Longitude) 75 | regexIP = regexp.MustCompile(IP) 76 | regexIPV4 = regexp.MustCompile(IPV4) 77 | regexIPV6 = regexp.MustCompile(IPV6) 78 | regexURL = regexp.MustCompile(URL) 79 | regexUUID = regexp.MustCompile(UUID) 80 | regexUUID3 = regexp.MustCompile(UUID3) 81 | regexUUID4 = regexp.MustCompile(UUID4) 82 | regexUUID5 = regexp.MustCompile(UUID5) 83 | ) 84 | -------------------------------------------------------------------------------- /roller.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // ROADMAP 9 | // traverse map or struct 10 | // detect each type 11 | // if type is struct or map then traverse it 12 | // if type is not struct or map then just push them in parent map's key as key and value of it 13 | // make flatten all the type in map[string]interface{} 14 | // in this case mapWalker will do the task 15 | 16 | // roller represents a roller type that will be used to flatten our data in a map[string]interface{} 17 | type roller struct { 18 | root map[string]interface{} 19 | typeName string 20 | tagIdentifier string 21 | tagSeparator string 22 | } 23 | 24 | // start start traversing through the tree 25 | func (r *roller) start(iface interface{}) { 26 | //initialize the Tree 27 | r.root = make(map[string]interface{}) 28 | r.typeName = "" 29 | ifv := reflect.ValueOf(iface) 30 | ift := reflect.TypeOf(iface) 31 | if ift.Kind() == reflect.Ptr { 32 | ifv = ifv.Elem() 33 | ift = ift.Elem() 34 | } 35 | canInterface := ifv.CanInterface() 36 | //check the provided root elment 37 | switch ift.Kind() { 38 | case reflect.Struct: 39 | if canInterface { 40 | r.traverseStruct(ifv.Interface()) 41 | } 42 | case reflect.Map: 43 | if ifv.Len() > 0 { 44 | if canInterface { 45 | r.traverseMap(ifv.Interface()) 46 | } 47 | } 48 | case reflect.Slice: 49 | if canInterface { 50 | r.push("slice", ifv.Interface()) 51 | } 52 | } 53 | } 54 | 55 | // setTagIdentifier set the struct tag identifier. e.g: json, validate etc 56 | func (r *roller) setTagIdentifier(i string) { 57 | r.tagIdentifier = i 58 | } 59 | 60 | // setTagSeparator set the struct tag separator. e.g: pipe (|) or comma (,) 61 | func (r *roller) setTagSeparator(s string) { 62 | r.tagSeparator = s 63 | } 64 | 65 | // getFlatMap get the all flatten values 66 | func (r *roller) getFlatMap() map[string]interface{} { 67 | return r.root 68 | } 69 | 70 | // getFlatVal return interface{} value if exist 71 | func (r *roller) getFlatVal(key string) (interface{}, bool) { 72 | var val interface{} 73 | var ok bool 74 | if val, ok = r.root[key]; ok { 75 | return val, ok 76 | } 77 | return val, ok 78 | } 79 | 80 | // push add value to map if key does not exist 81 | func (r *roller) push(key string, val interface{}) bool { 82 | if _, ok := r.root[key]; ok { 83 | return false 84 | } 85 | r.root[key] = val 86 | return true 87 | } 88 | 89 | // traverseStruct through all structs and add it to root 90 | func (r *roller) traverseStruct(iface interface{}) { 91 | ifv := reflect.ValueOf(iface) 92 | ift := reflect.TypeOf(iface) 93 | 94 | if ift.Kind() == reflect.Ptr { 95 | ifv = ifv.Elem() 96 | ift = ift.Elem() 97 | } 98 | 99 | for i := 0; i < ift.NumField(); i++ { 100 | v := ifv.Field(i) 101 | rfv := ift.Field(i) 102 | 103 | switch v.Kind() { 104 | case reflect.Struct: 105 | var typeName string 106 | if len(rfv.Tag.Get(r.tagIdentifier)) > 0 { 107 | tags := strings.Split(rfv.Tag.Get(r.tagIdentifier), r.tagSeparator) 108 | if tags[0] != "-" { 109 | typeName = tags[0] 110 | } 111 | } else { 112 | typeName = rfv.Name 113 | } 114 | if v.CanInterface() { 115 | switch v.Type().String() { 116 | case "govalidator.Int": 117 | r.push(typeName, v.Interface()) 118 | case "govalidator.Int64": 119 | r.push(typeName, v.Interface()) 120 | case "govalidator.Float32": 121 | r.push(typeName, v.Interface()) 122 | case "govalidator.Float64": 123 | r.push(typeName, v.Interface()) 124 | case "govalidator.Bool": 125 | r.push(typeName, v.Interface()) 126 | default: 127 | r.typeName = ift.Name() 128 | r.traverseStruct(v.Interface()) 129 | } 130 | } 131 | case reflect.Map: 132 | if v.CanInterface() { 133 | r.traverseMap(v.Interface()) 134 | } 135 | case reflect.Ptr: // if the field inside struct is Ptr then get the type and underlying values as interface{} 136 | ptrReflectionVal := reflect.Indirect(v) 137 | if !isEmpty(ptrReflectionVal) { 138 | ptrField := ptrReflectionVal.Type() 139 | switch ptrField.Kind() { 140 | case reflect.Struct: 141 | if v.CanInterface() { 142 | r.traverseStruct(v.Interface()) 143 | } 144 | case reflect.Map: 145 | if v.CanInterface() { 146 | r.traverseMap(v.Interface()) 147 | } 148 | } 149 | } 150 | default: 151 | if len(rfv.Tag.Get(r.tagIdentifier)) > 0 { 152 | tags := strings.Split(rfv.Tag.Get(r.tagIdentifier), r.tagSeparator) 153 | // add if first tag is not hyphen 154 | if tags[0] != "-" { 155 | if v.CanInterface() { 156 | r.push(tags[0], v.Interface()) 157 | } 158 | } 159 | } else { 160 | if v.Kind() == reflect.Ptr { 161 | if ifv.CanInterface() { 162 | r.push(ift.Name()+"."+rfv.Name, ifv.Interface()) 163 | } 164 | } else { 165 | if v.CanInterface() { 166 | r.push(ift.Name()+"."+rfv.Name, v.Interface()) 167 | } 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | // traverseMap through all the map and add it to root 175 | func (r *roller) traverseMap(iface interface{}) { 176 | switch t := iface.(type) { 177 | case map[string]interface{}: 178 | for k, v := range t { 179 | // drop null values in json to prevent panic caused by reflect.TypeOf(nil) 180 | if v == nil { 181 | continue 182 | } 183 | switch reflect.TypeOf(v).Kind() { 184 | case reflect.Struct: 185 | r.typeName = k // set the map key as name 186 | r.traverseStruct(v) 187 | case reflect.Map: 188 | r.typeName = k // set the map key as name 189 | r.traverseMap(v) 190 | case reflect.Ptr: // if the field inside map is Ptr then get the type and underlying values as interface{} 191 | switch reflect.TypeOf(v).Elem().Kind() { 192 | case reflect.Struct: 193 | r.traverseStruct(v) 194 | case reflect.Map: 195 | switch mapType := v.(type) { 196 | case *map[string]interface{}: 197 | r.traverseMap(*mapType) 198 | case *map[string]string: 199 | r.traverseMap(*mapType) 200 | case *map[string]bool: 201 | r.traverseMap(*mapType) 202 | case *map[string]int: 203 | r.traverseMap(*mapType) 204 | case *map[string]int8: 205 | r.traverseMap(*mapType) 206 | case *map[string]int16: 207 | r.traverseMap(*mapType) 208 | case *map[string]int32: 209 | r.traverseMap(*mapType) 210 | case *map[string]int64: 211 | r.traverseMap(*mapType) 212 | case *map[string]float32: 213 | r.traverseMap(*mapType) 214 | case *map[string]float64: 215 | r.traverseMap(*mapType) 216 | case *map[string]uint: 217 | r.traverseMap(*mapType) 218 | case *map[string]uint8: 219 | r.traverseMap(*mapType) 220 | case *map[string]uint16: 221 | r.traverseMap(*mapType) 222 | case *map[string]uint32: 223 | r.traverseMap(*mapType) 224 | case *map[string]uint64: 225 | r.traverseMap(*mapType) 226 | case *map[string]uintptr: 227 | r.traverseMap(*mapType) 228 | } 229 | default: 230 | r.push(k, v.(interface{})) 231 | } 232 | default: 233 | r.push(k, v) 234 | } 235 | } 236 | case map[string]string: 237 | for k, v := range t { 238 | r.push(k, v) 239 | } 240 | case map[string]bool: 241 | for k, v := range t { 242 | r.push(k, v) 243 | } 244 | case map[string]int: 245 | for k, v := range t { 246 | r.push(k, v) 247 | } 248 | case map[string]int8: 249 | for k, v := range t { 250 | r.push(k, v) 251 | } 252 | case map[string]int16: 253 | for k, v := range t { 254 | r.push(k, v) 255 | } 256 | case map[string]int32: 257 | for k, v := range t { 258 | r.push(k, v) 259 | } 260 | case map[string]int64: 261 | for k, v := range t { 262 | r.push(k, v) 263 | } 264 | case map[string]float32: 265 | for k, v := range t { 266 | r.push(k, v) 267 | } 268 | case map[string]float64: 269 | for k, v := range t { 270 | r.push(k, v) 271 | } 272 | case map[string]uint: 273 | for k, v := range t { 274 | r.push(k, v) 275 | } 276 | case map[string]uint8: 277 | for k, v := range t { 278 | r.push(k, v) 279 | } 280 | case map[string]uint16: 281 | for k, v := range t { 282 | r.push(k, v) 283 | } 284 | case map[string]uint32: 285 | for k, v := range t { 286 | r.push(k, v) 287 | } 288 | case map[string]uint64: 289 | for k, v := range t { 290 | r.push(k, v) 291 | } 292 | case map[string]uintptr: 293 | for k, v := range t { 294 | r.push(k, v) 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /roller_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type Earth struct { 9 | Human 10 | Name string 11 | Liveable bool 12 | Planet map[string]interface{} 13 | } 14 | 15 | type Human struct { 16 | Male 17 | Female 18 | } 19 | 20 | type Male struct { 21 | Name string 22 | Age int 23 | } 24 | 25 | type Female struct { 26 | Name string 27 | Age int 28 | } 29 | 30 | type deepLevel struct { 31 | Deep string 32 | Levels map[string]string 33 | } 34 | 35 | type structWithTag struct { 36 | Name string `validate:"name"` 37 | Age int `validate:"age"` 38 | } 39 | 40 | var p = map[string]interface{}{ 41 | "naam": "Jane", 42 | "bois": 29, 43 | "white": true, 44 | } 45 | var dl = deepLevel{ 46 | Deep: "So much deep", 47 | Levels: map[string]string{ 48 | "level 1": "20 m", 49 | "level 2": "30 m", 50 | "level 3": "80 m", 51 | "level 4": "103 m", 52 | }, 53 | } 54 | var planet = map[string]interface{}{ 55 | "name": "mars", 56 | "age": 1000, 57 | "red": true, 58 | "deepLevel": dl, 59 | "p": p, 60 | } 61 | var male = Male{"John", 33} 62 | var female = Female{"Jane", 30} 63 | var h = Human{ 64 | male, 65 | female, 66 | } 67 | var e = Earth{ 68 | h, 69 | "green earth", 70 | true, 71 | planet, 72 | } 73 | 74 | var m = make(map[string]interface{}) 75 | 76 | type structWithPointerToEmbeddedStruct struct { 77 | Male *Male 78 | Female *Female 79 | Planet *map[string]interface{} 80 | } 81 | 82 | func init() { 83 | m["earth"] = e 84 | m["person"] = "John Doe" 85 | m["iface"] = map[string]string{"another_person": "does it change root!"} 86 | m["array"] = [5]int{1, 4, 5, 6, 7} 87 | m["null"] = nil 88 | } 89 | 90 | func TestRoller_push(t *testing.T) { 91 | r := roller{} 92 | r.setTagIdentifier("validate") 93 | r.setTagSeparator("|") 94 | r.start(male) 95 | if r.push("Male.Name", "set new name") != false { 96 | t.Error("push failed!") 97 | } 98 | } 99 | 100 | func TestRoller_Start(t *testing.T) { 101 | r := roller{} 102 | r.setTagIdentifier("validate") 103 | r.setTagSeparator("|") 104 | r.start(m) 105 | if len(r.getFlatMap()) != 20 { 106 | t.Error("Start failed!") 107 | } 108 | } 109 | 110 | func BenchmarkRoller_Start(b *testing.B) { 111 | r := roller{} 112 | r.setTagIdentifier("validate") 113 | r.setTagSeparator("|") 114 | for n := 0; n < b.N; n++ { 115 | r.start(m) 116 | } 117 | } 118 | 119 | func Test_Roller_Start_empty_map(t *testing.T) { 120 | r := roller{} 121 | emap := make(map[string]interface{}) 122 | r.setTagIdentifier("validate") 123 | r.setTagSeparator("|") 124 | r.start(emap) 125 | if len(r.getFlatMap()) > 0 { 126 | t.Error("Failed to validate empty map") 127 | } 128 | } 129 | 130 | func TestRoller_traverseStructWithEmbeddedPointerStructAndMap(t *testing.T) { 131 | r := roller{} 132 | s := structWithPointerToEmbeddedStruct{ 133 | &male, 134 | &female, 135 | &p, 136 | } 137 | r.setTagIdentifier("validate") 138 | r.setTagSeparator("|") 139 | r.start(s) 140 | if len(r.getFlatMap()) != 4 { 141 | t.Error("traverseStructWithEmbeddedPointerStructAndMap failed!") 142 | } 143 | } 144 | 145 | func TestRoller_traverseMapWithPointerStructAndMap(t *testing.T) { 146 | r := roller{} 147 | mapOfPointerVals := map[string]interface{}{ 148 | "structField": male, 149 | "structPointerField": &female, 150 | "mapPointerField": &p, 151 | } 152 | 153 | r.setTagIdentifier("validate") 154 | r.setTagSeparator("|") 155 | r.start(mapOfPointerVals) 156 | if len(r.getFlatMap()) != 7 { 157 | t.Error("traverseMapWithPointerStructAndMap failed!") 158 | } 159 | } 160 | 161 | func TestRoller_StartPointerToStruct(t *testing.T) { 162 | r := roller{} 163 | r.setTagIdentifier("validate") 164 | r.setTagSeparator("|") 165 | r.start(&male) 166 | if len(r.getFlatMap()) != 2 { 167 | t.Error("StartPointerToStruct failed!") 168 | } 169 | } 170 | 171 | func TestRoller_StartMap(t *testing.T) { 172 | r := roller{} 173 | r.setTagIdentifier("validate") 174 | r.setTagSeparator("|") 175 | r.start(m) 176 | if len(r.getFlatMap()) != 20 { 177 | t.Error("StartMap failed!") 178 | } 179 | } 180 | 181 | func TestRoller_StartPointerToMap(t *testing.T) { 182 | r := roller{} 183 | r.setTagIdentifier("validate") 184 | r.setTagSeparator("|") 185 | r.start(&p) 186 | if len(r.getFlatMap()) != 3 { 187 | t.Error("StartPointerToMap failed!") 188 | } 189 | } 190 | 191 | func TestRoller_StartStruct(t *testing.T) { 192 | r := roller{} 193 | r.setTagIdentifier("validate") 194 | r.setTagSeparator("|") 195 | r.start(h) 196 | 197 | if len(r.getFlatMap()) != 4 { 198 | t.Error("StartStruct failed!") 199 | } 200 | } 201 | 202 | func TestRoller_StartStructWithTag(t *testing.T) { 203 | r := roller{} 204 | swTag := structWithTag{"John", 44} 205 | r.setTagIdentifier("validate") 206 | r.setTagSeparator("|") 207 | r.start(swTag) 208 | 209 | if len(r.getFlatMap()) != 2 { 210 | t.Error("StartStructWithTag failed!") 211 | } 212 | } 213 | 214 | func TestRoller_StartStructPointerWithTag(t *testing.T) { 215 | r := roller{} 216 | swTag := structWithTag{"John", 44} 217 | r.setTagIdentifier("validate") 218 | r.setTagSeparator("|") 219 | r.start(&swTag) 220 | 221 | if len(r.getFlatMap()) != 2 { 222 | t.Error("StartStructPointerWithTag failed!") 223 | } 224 | } 225 | 226 | func TestRoller_GetFlatVal(t *testing.T) { 227 | r := roller{} 228 | r.setTagIdentifier("validate") 229 | r.setTagSeparator("|") 230 | r.start(m) 231 | 232 | //check struct field with string 233 | name, _ := r.getFlatVal("Male.Name") 234 | if name != "John" { 235 | t.Error("GetFlatVal failed for struct string field!") 236 | } 237 | 238 | //check struct field with int 239 | age, _ := r.getFlatVal("Male.Age") 240 | if age != 33 { 241 | t.Error("GetFlatVal failed for struct int field!") 242 | } 243 | 244 | //check struct field with array 245 | intArrOf5, _ := r.getFlatVal("array") 246 | if reflect.ValueOf(intArrOf5).Len() != 5 && reflect.TypeOf(intArrOf5).Kind() == reflect.Array { 247 | t.Error("GetFlatVal failed for struct array of [5]int field!") 248 | } 249 | 250 | //check map key of string 251 | person, _ := r.getFlatVal("person") 252 | if person != "John Doe" { 253 | t.Error("GetFlatVal failed for map[string]string!") 254 | } 255 | 256 | //check not existed key 257 | _, ok := r.getFlatVal("not_existed_key") 258 | if ok { 259 | t.Error("GetFlatVal failed for not available key!") 260 | } 261 | } 262 | 263 | func TestRoller_PremitiveDataType(t *testing.T) { 264 | mStr := map[string]string{"oneStr": "hello", "twoStr": "Jane", "threeStr": "Doe"} 265 | mBool := map[string]bool{"oneBool": true, "twoBool": false, "threeBool": true} 266 | mInt := map[string]int{"oneInt": 1, "twoInt": 2, "threeInt": 3} 267 | mInt8 := map[string]int8{"oneInt8": 1, "twoInt8": 2, "threeInt8": 3} 268 | mInt16 := map[string]int16{"oneInt16": 1, "twoInt16": 2, "threeInt16": 3} 269 | mInt32 := map[string]int32{"oneInt32": 1, "twoInt32": 2, "threeInt32": 3} 270 | mInt64 := map[string]int64{"oneInt64": 1, "twoInt64": 2, "threeInt64": 3} 271 | mFloat32 := map[string]float32{"onefloat32": 1.09, "twofloat32": 20.87, "threefloat32": 11.3} 272 | mFloat64 := map[string]float64{"onefloat64": 10.88, "twofloat64": 92.09, "threefloat64": 3.90} 273 | mUintptr := map[string]uintptr{"oneUintptr": 1, "twoUintptr": 2, "threeUintptr": 3} 274 | mUint := map[string]uint{"oneUint": 1, "twoUint": 2, "threeUint": 3} 275 | mUint8 := map[string]uint8{"oneUint8": 1, "twoUint8": 2, "threeUint8": 3} 276 | mUint16 := map[string]uint16{"oneUint16": 1, "twoUint16": 2, "threeUint16": 3} 277 | mUint32 := map[string]uint32{"oneUint32": 1, "twoUint32": 2, "threeUint32": 3} 278 | mUint64 := map[string]uint64{"oneUint64": 1, "twoUint64": 2, "threeUint64": 3} 279 | mComplex := map[string]interface{}{ 280 | "ptrToMapString": &mStr, 281 | "ptrToMapBool": &mBool, 282 | "ptrToMapInt": &mInt, 283 | "ptrToMapInt8": &mInt8, 284 | "ptrToMapInt16": &mInt16, 285 | "ptrToMapInt32": &mInt32, 286 | "ptrToMapInt64": &mInt64, 287 | "ptrToMapfloat32": &mFloat32, 288 | "ptrToMapfloat64": &mFloat64, 289 | "ptrToMapUintptr": &mUintptr, 290 | "ptrToMapUint": &mUint, 291 | "ptrToMapUint8": &mUint8, 292 | "ptrToMapUint16": &mUint16, 293 | "ptrToMapUint32": &mUint32, 294 | "ptrToMapUint64": &mUint64, 295 | } 296 | r := roller{} 297 | r.setTagIdentifier("validate") 298 | r.setTagSeparator("|") 299 | r.start(mComplex) 300 | itemsLen := len(mComplex) * 3 301 | if len(r.getFlatMap()) != itemsLen { 302 | t.Error("PremitiveDataType failed!") 303 | } 304 | } 305 | 306 | func TestRoller_sliceOfType(t *testing.T) { 307 | males := []Male{ 308 | {Name: "John", Age: 29}, 309 | {Name: "Jane", Age: 23}, 310 | {Name: "Tom", Age: 10}, 311 | } 312 | r := roller{} 313 | r.setTagIdentifier("validate") 314 | r.setTagSeparator("|") 315 | r.start(males) 316 | i, _ := r.getFlatVal("slice") 317 | if len(i.([]Male)) != len(males) { 318 | t.Error("slice failed!") 319 | } 320 | } 321 | 322 | func TestRoller_ptrSliceOfType(t *testing.T) { 323 | males := []Male{ 324 | {Name: "John", Age: 29}, 325 | {Name: "Jane", Age: 23}, 326 | {Name: "Tom", Age: 10}, 327 | } 328 | r := roller{} 329 | r.setTagIdentifier("validate") 330 | r.setTagSeparator("|") 331 | r.start(&males) 332 | i, _ := r.getFlatVal("slice") 333 | if len(i.([]Male)) != len(males) { 334 | t.Error("slice failed!") 335 | } 336 | } 337 | 338 | func TestRoller_MapWithPointerPremitives(t *testing.T) { 339 | type customType string 340 | var str string 341 | var varInt int 342 | var varInt8 int8 343 | var varInt16 int16 344 | var varInt32 int32 345 | var varInt64 int64 346 | var varFloat32 float32 347 | var varFloat64 float64 348 | var varUint uint 349 | var varUint8 uint8 350 | var varUint16 uint16 351 | var varUint32 uint32 352 | var varUint64 uint64 353 | var varUintptr uintptr 354 | var x customType = "custom" 355 | y := []string{"y", "z"} 356 | 357 | males := map[string]interface{}{ 358 | "string": &str, 359 | "int": &varInt, 360 | "int8": &varInt8, 361 | "int16": &varInt16, 362 | "int32": &varInt32, 363 | "int64": &varInt64, 364 | "float32": &varFloat32, 365 | "float64": &varFloat64, 366 | "uint": &varUint, 367 | "uint8": &varUint8, 368 | "uint16": &varUint16, 369 | "uint32": &varUint32, 370 | "uint64": &varUint64, 371 | "uintPtr": &varUintptr, 372 | "customType": &x, 373 | "y": &y, 374 | } 375 | r := roller{} 376 | r.setTagIdentifier("validate") 377 | r.setTagSeparator("|") 378 | r.start(males) 379 | 380 | val, _ := r.getFlatVal("customType") 381 | if *val.(*customType) != "custom" { 382 | t.Error("fetching custom type value failed!") 383 | } 384 | 385 | valY, _ := r.getFlatVal("y") 386 | if len(*valY.(*[]string)) != len(y) { 387 | t.Error("fetching pointer to struct value failed!") 388 | } 389 | 390 | if len(r.getFlatMap()) != len(males) { 391 | t.Error("MapWithPointerPremitives failed!") 392 | } 393 | } 394 | 395 | type structWithCustomType struct { 396 | Name string `json:"name"` 397 | Integer Int `json:"integer"` 398 | Integer64 Int64 `json:"integer64"` 399 | Fpoint32 Float32 `json:"float32"` 400 | Fpoint64 Float64 `json:"float64"` 401 | Boolean Bool `json:"bool"` 402 | } 403 | 404 | func TestRoller_StartCustomType(t *testing.T) { 405 | r := roller{} 406 | swTag := structWithCustomType{Name: "John Doe", Integer: Int{Value: 44}} 407 | r.setTagIdentifier("json") 408 | r.setTagSeparator("|") 409 | r.start(&swTag) 410 | if len(r.getFlatMap()) != 6 { 411 | t.Error("failed to push custom type") 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /rules.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "mime/multipart" 8 | "net/url" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var rulesFuncMap = make(map[string]func(string, string, string, interface{}) error) 15 | 16 | // AddCustomRule help to add custom rules for validator 17 | // First argument it takes the rule name and second arg a func 18 | // Second arg must have this signature below 19 | // fn func(name string, fn func(field string, rule string, message string, value interface{}) error 20 | // see example in readme: https://github.com/thedevsaddam/govalidator#add-custom-rules 21 | func AddCustomRule(name string, fn func(field string, rule string, message string, value interface{}) error) { 22 | if isRuleExist(name) { 23 | panic(fmt.Errorf("govalidator: %s is already defined in rules", name)) 24 | } 25 | rulesFuncMap[name] = fn 26 | } 27 | 28 | // validateCustomRules validate custom rules 29 | func validateCustomRules(field string, rule string, message string, value interface{}, errsBag url.Values) { 30 | for k, v := range rulesFuncMap { 31 | if k == rule || strings.HasPrefix(rule, k+":") { 32 | err := v(field, rule, message, value) 33 | if err != nil { 34 | errsBag.Add(field, err.Error()) 35 | } 36 | break 37 | } 38 | } 39 | } 40 | 41 | func init() { 42 | 43 | // Required check the Required fields 44 | AddCustomRule("required", func(field, rule, message string, value interface{}) error { 45 | err := fmt.Errorf("The %s field is required", field) 46 | if message != "" { 47 | err = errors.New(message) 48 | } 49 | if value == nil { 50 | return err 51 | } 52 | if _, ok := value.(multipart.File); ok { 53 | return nil 54 | } 55 | rv := reflect.ValueOf(value) 56 | switch rv.Kind() { 57 | case reflect.String, reflect.Array, reflect.Slice, reflect.Map: 58 | if rv.Len() == 0 { 59 | return err 60 | } 61 | case reflect.Int: 62 | if isEmpty(value.(int)) { 63 | return err 64 | } 65 | case reflect.Int8: 66 | if isEmpty(value.(int8)) { 67 | return err 68 | } 69 | case reflect.Int16: 70 | if isEmpty(value.(int16)) { 71 | return err 72 | } 73 | case reflect.Int32: 74 | if isEmpty(value.(int32)) { 75 | return err 76 | } 77 | case reflect.Int64: 78 | if isEmpty(value.(int64)) { 79 | return err 80 | } 81 | case reflect.Float32: 82 | if isEmpty(value.(float32)) { 83 | return err 84 | } 85 | case reflect.Float64: 86 | if isEmpty(value.(float64)) { 87 | return err 88 | } 89 | case reflect.Uint: 90 | if isEmpty(value.(uint)) { 91 | return err 92 | } 93 | case reflect.Uint8: 94 | if isEmpty(value.(uint8)) { 95 | return err 96 | } 97 | case reflect.Uint16: 98 | if isEmpty(value.(uint16)) { 99 | return err 100 | } 101 | case reflect.Uint32: 102 | if isEmpty(value.(uint32)) { 103 | return err 104 | } 105 | case reflect.Uint64: 106 | if isEmpty(value.(uint64)) { 107 | return err 108 | } 109 | case reflect.Uintptr: 110 | if isEmpty(value.(uintptr)) { 111 | return err 112 | } 113 | case reflect.Struct: 114 | switch rv.Type().String() { 115 | case "govalidator.Int": 116 | if v, ok := value.(Int); ok { 117 | if !v.IsSet { 118 | return err 119 | } 120 | } 121 | case "govalidator.Int64": 122 | if v, ok := value.(Int64); ok { 123 | if !v.IsSet { 124 | return err 125 | } 126 | } 127 | case "govalidator.Float32": 128 | if v, ok := value.(Float32); ok { 129 | if !v.IsSet { 130 | return err 131 | } 132 | } 133 | case "govalidator.Float64": 134 | if v, ok := value.(Float64); ok { 135 | if !v.IsSet { 136 | return err 137 | } 138 | } 139 | case "govalidator.Bool": 140 | if v, ok := value.(Bool); ok { 141 | if !v.IsSet { 142 | return err 143 | } 144 | } 145 | default: 146 | panic("govalidator: invalid custom type for required rule") 147 | 148 | } 149 | 150 | default: 151 | panic("govalidator: invalid type for required rule") 152 | 153 | } 154 | return nil 155 | }) 156 | 157 | // Regex check the custom Regex rules 158 | // Regex:^[a-zA-Z]+$ means this field can only contain alphabet (a-z and A-Z) 159 | AddCustomRule("regex", func(field, rule, message string, value interface{}) error { 160 | str := toString(value) 161 | err := fmt.Errorf("The %s field format is invalid", field) 162 | if message != "" { 163 | err = errors.New(message) 164 | } 165 | rxStr := strings.TrimPrefix(rule, "regex:") 166 | if !isMatchedRegex(rxStr, str) { 167 | return err 168 | } 169 | return nil 170 | }) 171 | 172 | // Alpha check if provided field contains valid letters 173 | AddCustomRule("alpha", func(field string, vlaue string, message string, value interface{}) error { 174 | str := toString(value) 175 | err := fmt.Errorf("The %s may only contain letters", field) 176 | if message != "" { 177 | err = errors.New(message) 178 | } 179 | if !isAlpha(str) { 180 | return err 181 | } 182 | return nil 183 | }) 184 | 185 | // AlphaDash check if provided field contains valid letters, numbers, underscore and dash 186 | AddCustomRule("alpha_dash", func(field string, vlaue string, message string, value interface{}) error { 187 | str := toString(value) 188 | err := fmt.Errorf("The %s may only contain letters, numbers, and dashes", field) 189 | if message != "" { 190 | err = errors.New(message) 191 | } 192 | if !isAlphaDash(str) { 193 | return err 194 | } 195 | return nil 196 | }) 197 | 198 | // AlphaDash check if provided field contains valid letters, numbers, underscore and dash 199 | AddCustomRule("alpha_space", func(field string, vlaue string, message string, value interface{}) error { 200 | str := toString(value) 201 | err := fmt.Errorf("The %s may only contain letters, numbers, dashes, space", field) 202 | if message != "" { 203 | err = errors.New(message) 204 | } 205 | if !isAlphaSpace(str) { 206 | return err 207 | } 208 | return nil 209 | }) 210 | 211 | // AlphaNumeric check if provided field contains valid letters and numbers 212 | AddCustomRule("alpha_num", func(field string, vlaue string, message string, value interface{}) error { 213 | str := toString(value) 214 | err := fmt.Errorf("The %s may only contain letters and numbers", field) 215 | if message != "" { 216 | err = errors.New(message) 217 | } 218 | if !isAlphaNumeric(str) { 219 | return err 220 | } 221 | return nil 222 | }) 223 | 224 | // Boolean check if provided field contains Boolean 225 | // in this case: "0", "1", 0, 1, "true", "false", true, false etc 226 | AddCustomRule("bool", func(field string, vlaue string, message string, value interface{}) error { 227 | err := fmt.Errorf("The %s may only contain boolean value, string or int 0, 1", field) 228 | if message != "" { 229 | err = errors.New(message) 230 | } 231 | switch t := value.(type) { 232 | case bool: 233 | //if value is boolean then pass 234 | case string: 235 | if !isBoolean(t) { 236 | return err 237 | } 238 | case int: 239 | if t != 0 && t != 1 { 240 | return err 241 | } 242 | case int8: 243 | if t != 0 && t != 1 { 244 | return err 245 | } 246 | case int16: 247 | if t != 0 && t != 1 { 248 | return err 249 | } 250 | case int32: 251 | if t != 0 && t != 1 { 252 | return err 253 | } 254 | case int64: 255 | if t != 0 && t != 1 { 256 | return err 257 | } 258 | case uint: 259 | if t != 0 && t != 1 { 260 | return err 261 | } 262 | case uint8: 263 | if t != 0 && t != 1 { 264 | return err 265 | } 266 | case uint16: 267 | if t != 0 && t != 1 { 268 | return err 269 | } 270 | case uint32: 271 | if t != 0 && t != 1 { 272 | return err 273 | } 274 | case uint64: 275 | if t != 0 && t != 1 { 276 | return err 277 | } 278 | case uintptr: 279 | if t != 0 && t != 1 { 280 | return err 281 | } 282 | } 283 | return nil 284 | }) 285 | 286 | // Between check the fields character length range 287 | // if the field is array, map, slice then the valdiation rule will be the length of the data 288 | // if the value is int or float then the valdiation rule will be the value comparison 289 | AddCustomRule("between", func(field string, rule string, message string, value interface{}) error { 290 | rng := strings.Split(strings.TrimPrefix(rule, "between:"), ",") 291 | if len(rng) != 2 { 292 | panic(errInvalidArgument) 293 | } 294 | minFloat, err := strconv.ParseFloat(rng[0], 64) 295 | if err != nil { 296 | panic(errStringToInt) 297 | } 298 | maxFloat, err := strconv.ParseFloat(rng[1], 64) 299 | if err != nil { 300 | panic(errStringToInt) 301 | } 302 | min := int(minFloat) 303 | 304 | max := int(maxFloat) 305 | 306 | err = fmt.Errorf("The %s field must be between %d and %d", field, min, max) 307 | if message != "" { 308 | err = errors.New(message) 309 | } 310 | rv := reflect.ValueOf(value) 311 | switch rv.Kind() { 312 | case reflect.String, reflect.Array, reflect.Map, reflect.Slice: 313 | inLen := rv.Len() 314 | if !(inLen >= min && inLen <= max) { 315 | return err 316 | } 317 | case reflect.Int: 318 | in := value.(int) 319 | if !(in >= min && in <= max) { 320 | return err 321 | } 322 | case reflect.Int8: 323 | in := int(value.(int8)) 324 | if !(in >= min && in <= max) { 325 | return err 326 | } 327 | case reflect.Int16: 328 | in := int(value.(int16)) 329 | if !(in >= min && in <= max) { 330 | return err 331 | } 332 | case reflect.Int32: 333 | in := int(value.(int32)) 334 | if !(in >= min && in <= max) { 335 | return err 336 | } 337 | case reflect.Int64: 338 | in := int(value.(int64)) 339 | if !(in >= min && in <= max) { 340 | return err 341 | } 342 | case reflect.Uint: 343 | in := int(value.(uint)) 344 | if !(in >= min && in <= max) { 345 | return err 346 | } 347 | case reflect.Uint8: 348 | in := int(value.(uint8)) 349 | if !(in >= min && in <= max) { 350 | return err 351 | } 352 | case reflect.Uint16: 353 | in := int(value.(uint16)) 354 | if !(in >= min && in <= max) { 355 | return err 356 | } 357 | case reflect.Uint32: 358 | in := int(value.(uint32)) 359 | if !(in >= min && in <= max) { 360 | return err 361 | } 362 | case reflect.Uint64: 363 | in := int(value.(uint64)) 364 | if !(in >= min && in <= max) { 365 | return err 366 | } 367 | case reflect.Uintptr: 368 | in := int(value.(uintptr)) 369 | if !(in >= min && in <= max) { 370 | return err 371 | } 372 | case reflect.Float32: 373 | in := float64(value.(float32)) 374 | if !(in >= minFloat && in <= maxFloat) { 375 | return fmt.Errorf("The %s field must be between %f and %f", field, minFloat, maxFloat) 376 | } 377 | case reflect.Float64: 378 | in := value.(float64) 379 | if !(in >= minFloat && in <= maxFloat) { 380 | return fmt.Errorf("The %s field must be between %f and %f", field, minFloat, maxFloat) 381 | } 382 | 383 | } 384 | 385 | return nil 386 | }) 387 | 388 | // CreditCard check if provided field contains valid credit card number 389 | // Accepted cards are Visa, MasterCard, American Express, Diners Club, Discover and JCB card 390 | AddCustomRule("credit_card", func(field string, rule string, message string, value interface{}) error { 391 | str := toString(value) 392 | err := fmt.Errorf("The %s field must be a valid credit card number", field) 393 | if message != "" { 394 | err = errors.New(message) 395 | } 396 | if !isCreditCard(str) { 397 | return err 398 | } 399 | return nil 400 | }) 401 | 402 | // Coordinate check if provided field contains valid Coordinate 403 | AddCustomRule("coordinate", func(field string, rule string, message string, value interface{}) error { 404 | str := toString(value) 405 | err := fmt.Errorf("The %s field must be a valid coordinate", field) 406 | if message != "" { 407 | err = errors.New(message) 408 | } 409 | if !isCoordinate(str) { 410 | return err 411 | } 412 | return nil 413 | }) 414 | 415 | // ValidateCSSColor check if provided field contains a valid CSS color code 416 | AddCustomRule("css_color", func(field string, rule string, message string, value interface{}) error { 417 | str := toString(value) 418 | err := fmt.Errorf("The %s field must be a valid CSS color code", field) 419 | if message != "" { 420 | err = errors.New(message) 421 | } 422 | if !isCSSColor(str) { 423 | return err 424 | } 425 | return nil 426 | }) 427 | 428 | // Digits check the exact matching length of digit (0,9) 429 | // Digits:5 means the field must have 5 digit of length. 430 | // e.g: 12345 or 98997 etc 431 | AddCustomRule("digits", func(field string, rule string, message string, value interface{}) error { 432 | l, err := strconv.Atoi(strings.TrimPrefix(rule, "digits:")) 433 | if err != nil { 434 | panic(errStringToInt) 435 | } 436 | err = fmt.Errorf("The %s field must be %d digits", field, l) 437 | if l == 1 { 438 | err = fmt.Errorf("The %s field must be 1 digit", field) 439 | } 440 | if message != "" { 441 | err = errors.New(message) 442 | } 443 | var str string 444 | switch v := value.(type) { 445 | case string: 446 | str = v 447 | case float64: 448 | str = toString(int64(v)) 449 | case float32: 450 | str = toString(int64(v)) 451 | default: 452 | str = toString(v) 453 | } 454 | if len(str) != l || !regexDigits.MatchString(str) { 455 | return err 456 | } 457 | 458 | return nil 459 | }) 460 | 461 | // DigitsBetween check if the field contains only digit and length between provided range 462 | // e.g: digits_between:4,5 means the field can have value like: 8887 or 12345 etc 463 | AddCustomRule("digits_between", func(field string, rule string, message string, value interface{}) error { 464 | rng := strings.Split(strings.TrimPrefix(rule, "digits_between:"), ",") 465 | if len(rng) != 2 { 466 | panic(errInvalidArgument) 467 | } 468 | min, err := strconv.Atoi(rng[0]) 469 | if err != nil { 470 | panic(errStringToInt) 471 | } 472 | max, err := strconv.Atoi(rng[1]) 473 | if err != nil { 474 | panic(errStringToInt) 475 | } 476 | err = fmt.Errorf("The %s field must be digits between %d and %d", field, min, max) 477 | if message != "" { 478 | err = errors.New(message) 479 | } 480 | str := toString(value) 481 | if !isNumeric(str) || !(len(str) >= min && len(str) <= max) { 482 | return err 483 | } 484 | 485 | return nil 486 | }) 487 | 488 | // Date check the provided field is valid Date 489 | AddCustomRule("date", func(field string, rule string, message string, value interface{}) error { 490 | str := toString(value) 491 | 492 | switch rule { 493 | case "date:dd-mm-yyyy": 494 | if !isDateDDMMYY(str) { 495 | if message != "" { 496 | return errors.New(message) 497 | } 498 | return fmt.Errorf("The %s field must be a valid date format. e.g: dd-mm-yyyy, dd/mm/yyyy etc", field) 499 | } 500 | default: 501 | if !isDate(str) { 502 | if message != "" { 503 | return errors.New(message) 504 | } 505 | return fmt.Errorf("The %s field must be a valid date format. e.g: yyyy-mm-dd, yyyy/mm/dd etc", field) 506 | } 507 | } 508 | 509 | return nil 510 | }) 511 | 512 | // Email check the provided field is valid Email 513 | AddCustomRule("email", func(field string, rule string, message string, value interface{}) error { 514 | str := toString(value) 515 | err := fmt.Errorf("The %s field must be a valid email address", field) 516 | if message != "" { 517 | err = errors.New(message) 518 | } 519 | if !isEmail(str) { 520 | return err 521 | } 522 | return nil 523 | }) 524 | 525 | // validFloat check the provided field is valid float number 526 | AddCustomRule("float", func(field string, rule string, message string, value interface{}) error { 527 | str := toString(value) 528 | err := fmt.Errorf("The %s field must be a float number", field) 529 | if message != "" { 530 | err = errors.New(message) 531 | } 532 | if !isFloat(str) { 533 | return err 534 | } 535 | return nil 536 | }) 537 | 538 | // IP check if provided field is valid IP address 539 | AddCustomRule("ip", func(field string, rule string, message string, value interface{}) error { 540 | str := toString(value) 541 | err := fmt.Errorf("The %s field must be a valid IP address", field) 542 | if message != "" { 543 | err = errors.New(message) 544 | } 545 | if !isIP(str) { 546 | return err 547 | } 548 | return nil 549 | }) 550 | 551 | // IP check if provided field is valid IP v4 address 552 | AddCustomRule("ip_v4", func(field string, rule string, message string, value interface{}) error { 553 | str := toString(value) 554 | err := fmt.Errorf("The %s field must be a valid IPv4 address", field) 555 | if message != "" { 556 | err = errors.New(message) 557 | } 558 | if !isIPV4(str) { 559 | return err 560 | } 561 | return nil 562 | }) 563 | 564 | // IP check if provided field is valid IP v6 address 565 | AddCustomRule("ip_v6", func(field string, rule string, message string, value interface{}) error { 566 | str := toString(value) 567 | err := fmt.Errorf("The %s field must be a valid IPv6 address", field) 568 | if message != "" { 569 | err = errors.New(message) 570 | } 571 | if !isIPV6(str) { 572 | return err 573 | } 574 | return nil 575 | }) 576 | 577 | // ValidateJSON check if provided field contains valid json string 578 | AddCustomRule("json", func(field string, rule string, message string, value interface{}) error { 579 | str := toString(value) 580 | err := fmt.Errorf("The %s field must contain valid JSON string", field) 581 | if message != "" { 582 | err = errors.New(message) 583 | } 584 | if !isJSON(str) { 585 | return err 586 | } 587 | return nil 588 | }) 589 | 590 | /// Latitude check if provided field contains valid Latitude 591 | AddCustomRule("lat", func(field string, rule string, message string, value interface{}) error { 592 | str := toString(value) 593 | err := fmt.Errorf("The %s field must contain valid latitude", field) 594 | if message != "" { 595 | err = errors.New(message) 596 | } 597 | if !isLatitude(str) { 598 | return err 599 | } 600 | return nil 601 | }) 602 | 603 | // Longitude check if provided field contains valid Longitude 604 | AddCustomRule("lon", func(field string, rule string, message string, value interface{}) error { 605 | str := toString(value) 606 | err := fmt.Errorf("The %s field must contain valid longitude", field) 607 | if message != "" { 608 | err = errors.New(message) 609 | } 610 | if !isLongitude(str) { 611 | return err 612 | } 613 | return nil 614 | }) 615 | 616 | // Length check the field's character Length 617 | AddCustomRule("len", func(field string, rule string, message string, value interface{}) error { 618 | l, err := strconv.Atoi(strings.TrimPrefix(rule, "len:")) 619 | if err != nil { 620 | panic(errStringToInt) 621 | } 622 | err = fmt.Errorf("The %s field must be length of %d", field, l) 623 | if message != "" { 624 | err = errors.New(message) 625 | } 626 | rv := reflect.ValueOf(value) 627 | switch rv.Kind() { 628 | case reflect.String, reflect.Array, reflect.Map, reflect.Slice: 629 | vLen := rv.Len() 630 | if vLen != l { 631 | return err 632 | } 633 | default: 634 | str := toString(value) //force the value to be string 635 | if len(str) != l { 636 | return err 637 | } 638 | } 639 | 640 | return nil 641 | }) 642 | 643 | // Min check the field's minimum character length for string, value for int, float and size for array, map, slice 644 | AddCustomRule("min", func(field string, rule string, message string, value interface{}) error { 645 | mustLen := strings.TrimPrefix(rule, "min:") 646 | lenInt, err := strconv.Atoi(mustLen) 647 | if err != nil { 648 | panic(errStringToInt) 649 | } 650 | lenFloat, err := strconv.ParseFloat(mustLen, 64) 651 | if err != nil { 652 | panic(errStringToFloat) 653 | } 654 | errMsg := fmt.Errorf("The %s field value can not be less than %d", field, lenInt) 655 | if message != "" { 656 | errMsg = errors.New(message) 657 | } 658 | errMsgFloat := fmt.Errorf("The %s field value can not be less than %f", field, lenFloat) 659 | if message != "" { 660 | errMsgFloat = errors.New(message) 661 | } 662 | rv := reflect.ValueOf(value) 663 | switch rv.Kind() { 664 | case reflect.String: 665 | inLen := rv.Len() 666 | if inLen < lenInt { 667 | if message != "" { 668 | return errors.New(message) 669 | } 670 | return fmt.Errorf("The %s field must be minimum %d char", field, lenInt) 671 | } 672 | case reflect.Array, reflect.Map, reflect.Slice: 673 | inLen := rv.Len() 674 | if inLen < lenInt { 675 | if message != "" { 676 | return errors.New(message) 677 | } 678 | return fmt.Errorf("The %s field must be minimum %d in size", field, lenInt) 679 | } 680 | case reflect.Int: 681 | in := value.(int) 682 | if in < lenInt { 683 | return errMsg 684 | } 685 | case reflect.Int8: 686 | in := int(value.(int8)) 687 | if in < lenInt { 688 | return errMsg 689 | } 690 | case reflect.Int16: 691 | in := int(value.(int16)) 692 | if in < lenInt { 693 | return errMsg 694 | } 695 | case reflect.Int32: 696 | in := int(value.(int32)) 697 | if in < lenInt { 698 | return errMsg 699 | } 700 | case reflect.Int64: 701 | in := int(value.(int64)) 702 | if in < lenInt { 703 | return errMsg 704 | } 705 | case reflect.Uint: 706 | in := int(value.(uint)) 707 | if in < lenInt { 708 | return errMsg 709 | } 710 | case reflect.Uint8: 711 | in := int(value.(uint8)) 712 | if in < lenInt { 713 | return errMsg 714 | } 715 | case reflect.Uint16: 716 | in := int(value.(uint16)) 717 | if in < lenInt { 718 | return errMsg 719 | } 720 | case reflect.Uint32: 721 | in := int(value.(uint32)) 722 | if in < lenInt { 723 | return errMsg 724 | } 725 | case reflect.Uint64: 726 | in := int(value.(uint64)) 727 | if in < lenInt { 728 | return errMsg 729 | } 730 | case reflect.Uintptr: 731 | in := int(value.(uintptr)) 732 | if in < lenInt { 733 | return errMsg 734 | } 735 | case reflect.Float32: 736 | in := value.(float32) 737 | if in < float32(lenFloat) { 738 | return errMsgFloat 739 | } 740 | case reflect.Float64: 741 | in := value.(float64) 742 | if in < lenFloat { 743 | return errMsgFloat 744 | } 745 | 746 | } 747 | 748 | return nil 749 | }) 750 | 751 | // Max check the field's maximum character length for string, value for int, float and size for array, map, slice 752 | AddCustomRule("max", func(field string, rule string, message string, value interface{}) error { 753 | mustLen := strings.TrimPrefix(rule, "max:") 754 | lenInt, err := strconv.Atoi(mustLen) 755 | if err != nil { 756 | panic(errStringToInt) 757 | } 758 | lenFloat, err := strconv.ParseFloat(mustLen, 64) 759 | if err != nil { 760 | panic(errStringToFloat) 761 | } 762 | errMsg := fmt.Errorf("The %s field value can not be greater than %d", field, lenInt) 763 | if message != "" { 764 | errMsg = errors.New(message) 765 | } 766 | errMsgFloat := fmt.Errorf("The %s field value can not be greater than %f", field, lenFloat) 767 | if message != "" { 768 | errMsgFloat = errors.New(message) 769 | } 770 | rv := reflect.ValueOf(value) 771 | switch rv.Kind() { 772 | case reflect.String: 773 | inLen := rv.Len() 774 | if inLen > lenInt { 775 | if message != "" { 776 | return errors.New(message) 777 | } 778 | return fmt.Errorf("The %s field must be maximum %d char", field, lenInt) 779 | } 780 | case reflect.Array, reflect.Map, reflect.Slice: 781 | inLen := rv.Len() 782 | if inLen > lenInt { 783 | if message != "" { 784 | return errors.New(message) 785 | } 786 | return fmt.Errorf("The %s field must be maximum %d in size", field, lenInt) 787 | } 788 | case reflect.Int: 789 | in := value.(int) 790 | if in > lenInt { 791 | return errMsg 792 | } 793 | case reflect.Int8: 794 | in := int(value.(int8)) 795 | if in > lenInt { 796 | return errMsg 797 | } 798 | case reflect.Int16: 799 | in := int(value.(int16)) 800 | if in > lenInt { 801 | return errMsg 802 | } 803 | case reflect.Int32: 804 | in := int(value.(int32)) 805 | if in > lenInt { 806 | return errMsg 807 | } 808 | case reflect.Int64: 809 | in := int(value.(int64)) 810 | if in > lenInt { 811 | return errMsg 812 | } 813 | case reflect.Uint: 814 | in := int(value.(uint)) 815 | if in > lenInt { 816 | return errMsg 817 | } 818 | case reflect.Uint8: 819 | in := int(value.(uint8)) 820 | if in > lenInt { 821 | return errMsg 822 | } 823 | case reflect.Uint16: 824 | in := int(value.(uint16)) 825 | if in > lenInt { 826 | return errMsg 827 | } 828 | case reflect.Uint32: 829 | in := int(value.(uint32)) 830 | if in > lenInt { 831 | return errMsg 832 | } 833 | case reflect.Uint64: 834 | in := int(value.(uint64)) 835 | if in > lenInt { 836 | return errMsg 837 | } 838 | case reflect.Uintptr: 839 | in := int(value.(uintptr)) 840 | if in > lenInt { 841 | return errMsg 842 | } 843 | case reflect.Float32: 844 | in := value.(float32) 845 | if in > float32(lenFloat) { 846 | return errMsgFloat 847 | } 848 | case reflect.Float64: 849 | in := value.(float64) 850 | if in > lenFloat { 851 | return errMsgFloat 852 | } 853 | 854 | } 855 | 856 | return nil 857 | }) 858 | 859 | // Numeric check if the value of the field is Numeric 860 | AddCustomRule("mac_address", func(field string, rule string, message string, value interface{}) error { 861 | str := toString(value) 862 | err := fmt.Errorf("The %s field must be a valid Mac Address", field) 863 | if message != "" { 864 | err = errors.New(message) 865 | } 866 | if !isMacAddress(str) { 867 | return err 868 | } 869 | return nil 870 | }) 871 | 872 | // Numeric check if the value of the field is Numeric 873 | AddCustomRule("numeric", func(field string, rule string, message string, value interface{}) error { 874 | str := toString(value) 875 | err := fmt.Errorf("The %s field must be numeric", field) 876 | if message != "" { 877 | err = errors.New(message) 878 | } 879 | if !isNumeric(str) { 880 | return err 881 | } 882 | return nil 883 | }) 884 | 885 | // NumericBetween check if the value field numeric value range 886 | // e.g: numeric_between:18, 65 means number value must be in between a numeric value 18 & 65 887 | // Both of the bounds can be omited turning it into a min only (`10,`) or a max only (`,10`) 888 | AddCustomRule("numeric_between", func(field string, rule string, message string, value interface{}) error { 889 | rng := strings.Split(strings.TrimPrefix(rule, "numeric_between:"), ",") 890 | if len(rng) != 2 { 891 | panic(errInvalidArgument) 892 | } 893 | 894 | if rng[0] == "" && rng[1] == "" { 895 | panic(errInvalidArgument) 896 | } 897 | 898 | // check for integer value 899 | min := math.MinInt64 900 | if rng[0] != "" { 901 | _min, err := strconv.ParseFloat(rng[0], 64) 902 | if err != nil { 903 | panic(errStringToInt) 904 | } 905 | min = int(_min) 906 | } 907 | 908 | max := math.MaxInt64 909 | if rng[1] != "" { 910 | _max, err := strconv.ParseFloat(rng[1], 64) 911 | if err != nil { 912 | panic(errStringToInt) 913 | } 914 | max = int(_max) 915 | } 916 | 917 | var errMsg error 918 | switch { 919 | case rng[0] == "": 920 | errMsg = fmt.Errorf("The %s field value can not be greater than %d", field, max) 921 | case rng[1] == "": 922 | errMsg = fmt.Errorf("The %s field value can not be less than %d", field, min) 923 | default: 924 | errMsg = fmt.Errorf("The %s field must be numeric value between %d and %d", field, min, max) 925 | } 926 | 927 | if message != "" { 928 | errMsg = errors.New(message) 929 | } 930 | 931 | val := toString(value) 932 | 933 | if !strings.Contains(rng[0], ".") || !strings.Contains(rng[1], ".") { 934 | digit, errs := strconv.Atoi(val) 935 | if errs != nil { 936 | return errMsg 937 | } 938 | if !(digit >= min && digit <= max) { 939 | return errMsg 940 | } 941 | } 942 | // check for float value 943 | var err error 944 | minFloat := -math.MaxFloat64 945 | if rng[0] != "" { 946 | minFloat, err = strconv.ParseFloat(rng[0], 64) 947 | if err != nil { 948 | panic(errStringToFloat) 949 | } 950 | } 951 | 952 | maxFloat := math.MaxFloat64 953 | if rng[1] != "" { 954 | maxFloat, err = strconv.ParseFloat(rng[1], 64) 955 | if err != nil { 956 | panic(errStringToFloat) 957 | } 958 | } 959 | 960 | switch { 961 | case rng[0] == "": 962 | errMsg = fmt.Errorf("The %s field value can not be greater than %f", field, maxFloat) 963 | case rng[1] == "": 964 | errMsg = fmt.Errorf("The %s field value can not be less than %f", field, minFloat) 965 | default: 966 | errMsg = fmt.Errorf("The %s field must be numeric value between %f and %f", field, minFloat, maxFloat) 967 | } 968 | 969 | if message != "" { 970 | errMsg = errors.New(message) 971 | } 972 | 973 | digit, err := strconv.ParseFloat(val, 64) 974 | if err != nil { 975 | return errMsg 976 | } 977 | if !(digit >= minFloat && digit <= maxFloat) { 978 | return errMsg 979 | } 980 | return nil 981 | }) 982 | 983 | // ValidateURL check if provided field is valid URL 984 | AddCustomRule("url", func(field string, rule string, message string, value interface{}) error { 985 | str := toString(value) 986 | err := fmt.Errorf("The %s field format is invalid", field) 987 | if message != "" { 988 | err = errors.New(message) 989 | } 990 | if !isURL(str) { 991 | return err 992 | } 993 | return nil 994 | }) 995 | 996 | // UUID check if provided field contains valid UUID 997 | AddCustomRule("uuid", func(field string, rule string, message string, value interface{}) error { 998 | str := toString(value) 999 | err := fmt.Errorf("The %s field must contain valid UUID", field) 1000 | if message != "" { 1001 | err = errors.New(message) 1002 | } 1003 | if !isUUID(str) { 1004 | return err 1005 | } 1006 | return nil 1007 | }) 1008 | 1009 | // UUID3 check if provided field contains valid UUID of version 3 1010 | AddCustomRule("uuid_v3", func(field string, rule string, message string, value interface{}) error { 1011 | str := toString(value) 1012 | err := fmt.Errorf("The %s field must contain valid UUID V3", field) 1013 | if message != "" { 1014 | err = errors.New(message) 1015 | } 1016 | if !isUUID3(str) { 1017 | return err 1018 | } 1019 | return nil 1020 | }) 1021 | 1022 | // UUID4 check if provided field contains valid UUID of version 4 1023 | AddCustomRule("uuid_v4", func(field string, rule string, message string, value interface{}) error { 1024 | str := toString(value) 1025 | err := fmt.Errorf("The %s field must contain valid UUID V4", field) 1026 | if message != "" { 1027 | err = errors.New(message) 1028 | } 1029 | if !isUUID4(str) { 1030 | return err 1031 | } 1032 | return nil 1033 | }) 1034 | 1035 | // UUID5 check if provided field contains valid UUID of version 5 1036 | AddCustomRule("uuid_v5", func(field string, rule string, message string, value interface{}) error { 1037 | str := toString(value) 1038 | err := fmt.Errorf("The %s field must contain valid UUID V5", field) 1039 | if message != "" { 1040 | err = errors.New(message) 1041 | } 1042 | if !isUUID5(str) { 1043 | return err 1044 | } 1045 | return nil 1046 | }) 1047 | 1048 | // In check if provided field equals one of the values specified in the rule 1049 | AddCustomRule("in", func(field string, rule string, message string, value interface{}) error { 1050 | rng := strings.Split(strings.TrimPrefix(rule, "in:"), ",") 1051 | if len(rng) == 0 { 1052 | panic(errInvalidArgument) 1053 | } 1054 | str := toString(value) 1055 | err := fmt.Errorf("The %s field must be one of %v", field, strings.Join(rng, ", ")) 1056 | if message != "" { 1057 | err = errors.New(message) 1058 | } 1059 | if !isIn(rng, str) { 1060 | return err 1061 | } 1062 | return nil 1063 | }) 1064 | 1065 | // In check if provided field equals one of the values specified in the rule 1066 | AddCustomRule("not_in", func(field string, rule string, message string, value interface{}) error { 1067 | rng := strings.Split(strings.TrimPrefix(rule, "not_in:"), ",") 1068 | if len(rng) == 0 { 1069 | panic(errInvalidArgument) 1070 | } 1071 | str := toString(value) 1072 | err := fmt.Errorf("The %s field must not be any of %v", field, strings.Join(rng, ", ")) 1073 | if message != "" { 1074 | err = errors.New(message) 1075 | } 1076 | if isIn(rng, str) { 1077 | return err 1078 | } 1079 | return nil 1080 | }) 1081 | } 1082 | -------------------------------------------------------------------------------- /rules_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | func Test_AddCustomRule(t *testing.T) { 13 | AddCustomRule("__x__", func(f string, rule string, message string, v interface{}) error { 14 | if v.(string) != "xyz" { 15 | return fmt.Errorf("The %s field must be xyz", f) 16 | } 17 | return nil 18 | }) 19 | if len(rulesFuncMap) <= 0 { 20 | t.Error("AddCustomRule failed to add new rule") 21 | } 22 | } 23 | 24 | func Test_AddCustomRule_panic(t *testing.T) { 25 | defer func() { 26 | if r := recover(); r == nil { 27 | t.Errorf("AddCustomRule failed to panic") 28 | } 29 | }() 30 | AddCustomRule("__x__", func(f string, rule string, message string, v interface{}) error { 31 | if v.(string) != "xyz" { 32 | return fmt.Errorf("The %s field must be xyz", f) 33 | } 34 | return nil 35 | }) 36 | } 37 | 38 | func Test_validateExtraRules(t *testing.T) { 39 | errsBag := url.Values{} 40 | validateCustomRules("f_field", "__x__", "a", "", errsBag) 41 | if len(errsBag) != 1 { 42 | t.Error("validateExtraRules failed") 43 | } 44 | } 45 | 46 | //================================= rules ================================= 47 | func Test_Required(t *testing.T) { 48 | type tRequired struct { 49 | Str string `json:"_str"` 50 | Int int `json:"_int"` 51 | Int8 int8 `json:"_int8"` 52 | Int16 int16 `json:"_int16"` 53 | Int32 int32 `json:"_int32"` 54 | Int64 int64 `json:"_int64"` 55 | Uint uint `json:"_uint"` 56 | Uint8 uint8 `json:"_uint8"` 57 | Uint16 uint16 `json:"_uint16"` 58 | Uint32 uint32 `json:"_uint32"` 59 | Uint64 uint64 `json:"_uint64"` 60 | Uintptr uintptr `json:"_uintptr"` 61 | Flaot32 float32 `json:"_float32"` 62 | Flaot64 float64 `json:"_float64"` 63 | Integer Int `json:"integer"` 64 | Integer64 Int64 `json:"integer64"` 65 | Fpoint32 Float32 `json:"float32"` 66 | Fpoint64 Float64 `json:"float64"` 67 | Boolean Bool `json:"boolean"` 68 | Arbitrary interface{} `json:"arbitrary"` 69 | } 70 | 71 | rules := MapData{ 72 | "_str": []string{"required"}, 73 | "_int": []string{"required"}, 74 | "_int8": []string{"required"}, 75 | "_int16": []string{"required"}, 76 | "_int32": []string{"required"}, 77 | "_int64": []string{"required"}, 78 | "_uint": []string{"required"}, 79 | "_uint8": []string{"required"}, 80 | "_uint16": []string{"required"}, 81 | "_uint32": []string{"required"}, 82 | "_uint64": []string{"required"}, 83 | "_uintptr": []string{"required"}, 84 | "_float32": []string{"required"}, 85 | "_float64": []string{"required"}, 86 | "integer": []string{"required"}, 87 | "integer64": []string{"required"}, 88 | "float32": []string{"required"}, 89 | "float64": []string{"required"}, 90 | "boolean": []string{"required"}, 91 | "arbitrary": []string{"required"}, 92 | } 93 | 94 | postRequired := map[string]string{} 95 | 96 | var trequired tRequired 97 | 98 | body, _ := json.Marshal(postRequired) 99 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 100 | 101 | messages := MapData{ 102 | "_str": []string{"required:custom_message"}, 103 | } 104 | 105 | opts := Options{ 106 | Request: req, 107 | Data: &trequired, 108 | Rules: rules, 109 | Messages: messages, 110 | } 111 | 112 | vd := New(opts) 113 | validationErr := vd.ValidateJSON() 114 | if len(validationErr) != 20 { 115 | t.Log(validationErr) 116 | t.Error("required validation failed!") 117 | } 118 | 119 | if validationErr.Get("_str") != "custom_message" { 120 | t.Error("required rule custom message failed") 121 | } 122 | } 123 | 124 | func Test_Regex(t *testing.T) { 125 | type tRegex struct { 126 | Name string `json:"name"` 127 | } 128 | 129 | postRegex := tRegex{Name: "john"} 130 | var tregex tRegex 131 | 132 | body, _ := json.Marshal(postRegex) 133 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 134 | 135 | messages := MapData{ 136 | "name": []string{"regex:custom_message"}, 137 | } 138 | 139 | rules := MapData{ 140 | "name": []string{"regex:^[0-9]+$"}, 141 | } 142 | 143 | opts := Options{ 144 | Request: req, 145 | Data: &tregex, 146 | Rules: rules, 147 | Messages: messages, 148 | } 149 | 150 | vd := New(opts) 151 | validationErr := vd.ValidateJSON() 152 | if len(validationErr) != 1 { 153 | t.Error("regex validation failed!") 154 | } 155 | 156 | if validationErr.Get("name") != "custom_message" { 157 | t.Error("regex rule custom message failed") 158 | } 159 | 160 | } 161 | 162 | func Test_Alpha(t *testing.T) { 163 | type user struct { 164 | Name string `json:"name"` 165 | } 166 | 167 | postUser := user{Name: "9080"} 168 | var userObj user 169 | 170 | body, _ := json.Marshal(postUser) 171 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 172 | 173 | messages := MapData{ 174 | "name": []string{"alpha:custom_message"}, 175 | } 176 | 177 | rules := MapData{ 178 | "name": []string{"alpha"}, 179 | } 180 | 181 | opts := Options{ 182 | Request: req, 183 | Data: &userObj, 184 | Rules: rules, 185 | Messages: messages, 186 | } 187 | 188 | vd := New(opts) 189 | validationErr := vd.ValidateJSON() 190 | if len(validationErr) != 1 { 191 | t.Error("alpha validation failed!") 192 | } 193 | 194 | if validationErr.Get("name") != "custom_message" { 195 | t.Error("alpha custom message failed!") 196 | } 197 | } 198 | 199 | func Test_AlphaDash(t *testing.T) { 200 | type user struct { 201 | Name string `json:"name"` 202 | } 203 | 204 | postUser := user{Name: "9090$"} 205 | var userObj user 206 | 207 | body, _ := json.Marshal(postUser) 208 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 209 | 210 | messages := MapData{ 211 | "name": []string{"alpha_dash:custom_message"}, 212 | } 213 | 214 | rules := MapData{ 215 | "name": []string{"alpha_dash"}, 216 | } 217 | 218 | opts := Options{ 219 | Request: req, 220 | Data: &userObj, 221 | Rules: rules, 222 | Messages: messages, 223 | } 224 | 225 | vd := New(opts) 226 | validationErr := vd.ValidateJSON() 227 | if len(validationErr) != 1 { 228 | t.Log(validationErr) 229 | t.Error("alpha_dash validation failed!") 230 | } 231 | 232 | if validationErr.Get("name") != "custom_message" { 233 | t.Error("alpha dash custom message failed!") 234 | } 235 | } 236 | 237 | func Test_AlphaSpace(t *testing.T) { 238 | type user struct { 239 | Name string `json:"name"` 240 | } 241 | 242 | postUser := user{Name: "999$"} 243 | var userObj user 244 | 245 | body, _ := json.Marshal(postUser) 246 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 247 | 248 | messages := MapData{ 249 | "name": []string{"alpha_space:custom_message"}, 250 | } 251 | 252 | rules := MapData{ 253 | "name": []string{"alpha_space"}, 254 | } 255 | 256 | opts := Options{ 257 | Request: req, 258 | Data: &userObj, 259 | Rules: rules, 260 | Messages: messages, 261 | } 262 | 263 | vd := New(opts) 264 | validationErr := vd.ValidateJSON() 265 | t.Log(len(validationErr)) 266 | if len(validationErr) != 1 { 267 | t.Log(validationErr) 268 | t.Error("alpha_space validation failed!") 269 | } 270 | 271 | if validationErr.Get("name") != "custom_message" { 272 | t.Error("alpha space custom message failed!") 273 | } 274 | } 275 | 276 | func Test_AlphaNumeric(t *testing.T) { 277 | type user struct { 278 | Name string `json:"name"` 279 | } 280 | 281 | postUser := user{Name: "aE*Sb$"} 282 | var userObj user 283 | 284 | body, _ := json.Marshal(postUser) 285 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 286 | 287 | rules := MapData{ 288 | "name": []string{"alpha_num"}, 289 | } 290 | 291 | messages := MapData{ 292 | "name": []string{"alpha_num:custom_message"}, 293 | } 294 | 295 | opts := Options{ 296 | Request: req, 297 | Data: &userObj, 298 | Rules: rules, 299 | Messages: messages, 300 | } 301 | 302 | vd := New(opts) 303 | validationErr := vd.ValidateJSON() 304 | if len(validationErr) != 1 { 305 | t.Log(validationErr) 306 | t.Error("alpha_num validation failed!") 307 | } 308 | 309 | if validationErr.Get("name") != "custom_message" { 310 | t.Error("alpha num custom message failed!") 311 | } 312 | } 313 | 314 | func Test_Boolean(t *testing.T) { 315 | type Bools struct { 316 | BoolStr string `json:"boolStr"` 317 | BoolInt int `json:"boolInt"` 318 | BoolInt8 int8 `json:"boolInt8"` 319 | BoolInt16 int16 `json:"boolInt16"` 320 | BoolInt32 int32 `json:"boolInt32"` 321 | BoolInt64 int64 `json:"boolInt64"` 322 | BoolUint uint `json:"boolUint"` 323 | BoolUint8 uint8 `json:"boolUint8"` 324 | BoolUint16 uint16 `json:"boolUint16"` 325 | BoolUint32 uint32 `json:"boolUint32"` 326 | BoolUint64 uint64 `json:"boolUint64"` 327 | BoolUintptr uintptr `json:"boolUintptr"` 328 | Bool bool `json:"_bool"` 329 | } 330 | 331 | postBools := Bools{ 332 | BoolStr: "abc", 333 | BoolInt: 90, 334 | BoolInt8: 10, 335 | BoolInt16: 22, 336 | BoolInt32: 76, 337 | BoolInt64: 9, 338 | BoolUint: 5, 339 | BoolUint8: 9, 340 | BoolUint16: 9, 341 | BoolUint32: 9, 342 | BoolUint64: 8, 343 | BoolUintptr: 9, 344 | } 345 | var boolObj Bools 346 | 347 | body, _ := json.Marshal(postBools) 348 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 349 | 350 | rules := MapData{ 351 | "boolStr": []string{"bool"}, 352 | "boolInt": []string{"bool"}, 353 | "boolInt8": []string{"bool"}, 354 | "boolInt16": []string{"bool"}, 355 | "boolInt32": []string{"bool"}, 356 | "boolInt64": []string{"bool"}, 357 | "boolUint": []string{"bool"}, 358 | "boolUint8": []string{"bool"}, 359 | "boolUint16": []string{"bool"}, 360 | "boolUint32": []string{"bool"}, 361 | "boolUint64": []string{"bool"}, 362 | "boolUintptr": []string{"bool"}, 363 | } 364 | 365 | messages := MapData{ 366 | "boolStr": []string{"bool:custom_message"}, 367 | "boolInt": []string{"bool:custom_message"}, 368 | "boolUint": []string{"bool:custom_message"}, 369 | } 370 | 371 | opts := Options{ 372 | Request: req, 373 | Data: &boolObj, 374 | Rules: rules, 375 | Messages: messages, 376 | } 377 | 378 | vd := New(opts) 379 | validationErr := vd.ValidateJSON() 380 | if len(validationErr) != 12 { 381 | t.Error("bool validation failed!") 382 | } 383 | 384 | if validationErr.Get("boolStr") != "custom_message" || 385 | validationErr.Get("boolInt") != "custom_message" || 386 | validationErr.Get("boolUint") != "custom_message" { 387 | t.Error("bool custom message failed!") 388 | } 389 | } 390 | 391 | func Test_Between(t *testing.T) { 392 | type user struct { 393 | Str string `json:"str"` 394 | Int int `json:"_int"` 395 | Int8 int8 `json:"_int8"` 396 | Int16 int16 `json:"_int16"` 397 | Int32 int32 `json:"_int32"` 398 | Int64 int64 `json:"_int64"` 399 | Uint uint `json:"_uint"` 400 | Uint8 uint8 `json:"_uint8"` 401 | Uint16 uint16 `json:"_uint16"` 402 | Uint32 uint32 `json:"_uint32"` 403 | Uint64 uint64 `json:"_uint64"` 404 | Uintptr uintptr `json:"_uintptr"` 405 | Float32 float32 `json:"_float32"` 406 | Float64 float64 `json:"_float64"` 407 | Slice []int `json:"_slice"` 408 | } 409 | 410 | postUser := user{} 411 | var userObj user 412 | 413 | body, _ := json.Marshal(postUser) 414 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 415 | 416 | rules := MapData{ 417 | "str": []string{"between:3,5"}, 418 | "_int": []string{"between:3,5"}, 419 | "_int8": []string{"between:3,5"}, 420 | "_int16": []string{"between:3,5"}, 421 | "_int32": []string{"between:3,5"}, 422 | "_int64": []string{"between:3,5"}, 423 | "_uint": []string{"between:3,5"}, 424 | "_uint8": []string{"between:3,5"}, 425 | "_uint16": []string{"between:3,5"}, 426 | "_uint32": []string{"between:3,5"}, 427 | "_uint64": []string{"between:3,5"}, 428 | "_uintptr": []string{"between:3,5"}, 429 | "_float32": []string{"between:3.5,5.9"}, 430 | "_float64": []string{"between:3.3,6.2"}, 431 | "_slice": []string{"between:3,5"}, 432 | } 433 | 434 | opts := Options{ 435 | Request: req, 436 | Data: &userObj, 437 | Rules: rules, 438 | } 439 | 440 | vd := New(opts) 441 | vd.SetDefaultRequired(true) 442 | validationErr := vd.ValidateJSON() 443 | if len(validationErr) != 15 { 444 | t.Error("between validation failed!") 445 | } 446 | } 447 | 448 | func Test_CreditCard(t *testing.T) { 449 | type user struct { 450 | CreditCard string `json:"credit_card"` 451 | } 452 | 453 | postUser := user{CreditCard: "87080"} 454 | var userObj user 455 | 456 | body, _ := json.Marshal(postUser) 457 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 458 | 459 | messages := MapData{ 460 | "credit_card": []string{"credit_card:custom_message"}, 461 | } 462 | 463 | rules := MapData{ 464 | "credit_card": []string{"credit_card"}, 465 | } 466 | 467 | opts := Options{ 468 | Request: req, 469 | Data: &userObj, 470 | Rules: rules, 471 | Messages: messages, 472 | } 473 | 474 | vd := New(opts) 475 | validationErr := vd.ValidateJSON() 476 | if len(validationErr) != 1 { 477 | t.Error("credit card validation failed!") 478 | } 479 | 480 | if validationErr.Get("credit_card") != "custom_message" { 481 | t.Error("credit_card custom message failed!") 482 | } 483 | } 484 | 485 | func Test_Coordinate(t *testing.T) { 486 | type user struct { 487 | Coordinate string `json:"coordinate"` 488 | } 489 | 490 | postUser := user{Coordinate: "8080"} 491 | var userObj user 492 | 493 | body, _ := json.Marshal(postUser) 494 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 495 | 496 | messages := MapData{ 497 | "coordinate": []string{"coordinate:custom_message"}, 498 | } 499 | 500 | rules := MapData{ 501 | "coordinate": []string{"coordinate"}, 502 | } 503 | 504 | opts := Options{ 505 | Request: req, 506 | Data: &userObj, 507 | Rules: rules, 508 | Messages: messages, 509 | } 510 | 511 | vd := New(opts) 512 | validationErr := vd.ValidateJSON() 513 | if len(validationErr) != 1 { 514 | t.Error("coordinate validation failed!") 515 | } 516 | 517 | if validationErr.Get("coordinate") != "custom_message" { 518 | t.Error("coordinate custom message failed!") 519 | } 520 | } 521 | 522 | func Test_CSSColor(t *testing.T) { 523 | type user struct { 524 | Color string `json:"color"` 525 | } 526 | 527 | postUser := user{Color: "8080"} 528 | var userObj user 529 | 530 | body, _ := json.Marshal(postUser) 531 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 532 | 533 | rules := MapData{ 534 | "color": []string{"css_color"}, 535 | } 536 | 537 | messages := MapData{ 538 | "color": []string{"css_color:custom_message"}, 539 | } 540 | 541 | opts := Options{ 542 | Request: req, 543 | Data: &userObj, 544 | Rules: rules, 545 | Messages: messages, 546 | } 547 | 548 | vd := New(opts) 549 | validationErr := vd.ValidateJSON() 550 | if len(validationErr) != 1 { 551 | t.Error("CSS color validation failed!") 552 | } 553 | 554 | if validationErr.Get("color") != "custom_message" { 555 | t.Error("css_color custom message failed!") 556 | } 557 | } 558 | 559 | func Test_Digits(t *testing.T) { 560 | type user struct { 561 | Zip string `json:"zip"` 562 | Level string `json:"level"` 563 | EpochInt int `json:"epoch_int"` 564 | EpochInt64 int64 `json:"epoch_int_64"` 565 | EpochFloat32 float32 `json:"epoch_float_32"` 566 | EpochFloat64 float64 `json:"epoch_float_64"` 567 | EpochString string `json:"epoch_string"` 568 | } 569 | 570 | postUser := user{ 571 | Zip: "8322", 572 | Level: "10", 573 | EpochInt: 1541689, 574 | EpochInt64: 15416890380008, 575 | EpochFloat32: 15416890380008, 576 | EpochFloat64: 15416890380008, 577 | EpochString: "15416890380008", 578 | } 579 | var userObj user 580 | 581 | body, _ := json.Marshal(postUser) 582 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 583 | 584 | rules := MapData{ 585 | "zip": []string{"digits:5"}, 586 | "level": []string{"digits:1"}, 587 | "epoch_int": []string{"digits:13"}, 588 | "epoch_int_64": []string{"digits:13"}, 589 | "epoch_float_32": []string{"digits:13"}, 590 | "epoch_float_64": []string{"digits:13"}, 591 | "epoch_string": []string{"digits:13"}, 592 | } 593 | 594 | opts := Options{ 595 | Request: req, 596 | Data: &userObj, 597 | Rules: rules, 598 | } 599 | 600 | vd := New(opts) 601 | validationErr := vd.ValidateJSON() 602 | if len(validationErr) != 7 { 603 | t.Log(validationErr) 604 | t.Error("Digits validation failed!") 605 | } 606 | } 607 | 608 | func Test_DigitsBetween(t *testing.T) { 609 | type user struct { 610 | Zip string `json:"zip"` 611 | Level string `json:"level"` 612 | } 613 | 614 | postUser := user{Zip: "8322", Level: "10"} 615 | var userObj user 616 | 617 | body, _ := json.Marshal(postUser) 618 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 619 | 620 | rules := MapData{ 621 | "zip": []string{"digits_between:5,10"}, 622 | "level": []string{"digits_between:5,10"}, 623 | } 624 | 625 | messages := MapData{ 626 | "zip": []string{"digits_between:custom_message"}, 627 | "level": []string{"digits_between:custom_message"}, 628 | } 629 | 630 | opts := Options{ 631 | Request: req, 632 | Data: &userObj, 633 | Rules: rules, 634 | Messages: messages, 635 | } 636 | 637 | vd := New(opts) 638 | validationErr := vd.ValidateJSON() 639 | if len(validationErr) != 2 { 640 | t.Error("digits between validation failed!") 641 | } 642 | 643 | if validationErr.Get("zip") != "custom_message" || 644 | validationErr.Get("level") != "custom_message" { 645 | t.Error("digits_between custom message failed!") 646 | } 647 | } 648 | 649 | func Test_DigitsBetweenPanic(t *testing.T) { 650 | defer func() { 651 | if r := recover(); r == nil { 652 | t.Errorf("Digits between failed to panic!") 653 | } 654 | }() 655 | type user struct { 656 | Zip string `json:"zip"` 657 | Level string `json:"level"` 658 | } 659 | 660 | postUser := user{Zip: "8322", Level: "10"} 661 | var userObj user 662 | 663 | body, _ := json.Marshal(postUser) 664 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 665 | 666 | rules := MapData{ 667 | "zip": []string{"digits_between:5"}, 668 | "level": []string{"digits_between:i,k"}, 669 | } 670 | 671 | opts := Options{ 672 | Request: req, 673 | Data: &userObj, 674 | Rules: rules, 675 | } 676 | 677 | vd := New(opts) 678 | validationErr := vd.ValidateJSON() 679 | if len(validationErr) != 2 { 680 | t.Error("Digits between panic failed!") 681 | } 682 | } 683 | 684 | func Test_Date(t *testing.T) { 685 | type user struct { 686 | DOB string `json:"dob"` 687 | JoiningDate string `json:"joining_date"` 688 | } 689 | 690 | postUser := user{DOB: "invalida date", JoiningDate: "10"} 691 | var userObj user 692 | 693 | body, _ := json.Marshal(postUser) 694 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 695 | 696 | rules := MapData{ 697 | "dob": []string{"date"}, 698 | "joining_date": []string{"date:dd-mm-yyyy"}, 699 | } 700 | 701 | opts := Options{ 702 | Request: req, 703 | Data: &userObj, 704 | Rules: rules, 705 | } 706 | 707 | vd := New(opts) 708 | validationErr := vd.ValidateJSON() 709 | if len(validationErr) != 2 { 710 | t.Log(validationErr) 711 | t.Error("Date validation failed!") 712 | } 713 | } 714 | 715 | func Test_Date_message(t *testing.T) { 716 | type user struct { 717 | DOB string `json:"dob"` 718 | JoiningDate string `json:"joining_date"` 719 | } 720 | 721 | postUser := user{DOB: "invalida date", JoiningDate: "10"} 722 | var userObj user 723 | 724 | body, _ := json.Marshal(postUser) 725 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 726 | 727 | rules := MapData{ 728 | "dob": []string{"date"}, 729 | "joining_date": []string{"date:dd-mm-yyyy"}, 730 | } 731 | 732 | messages := MapData{ 733 | "dob": []string{"date:custom_message"}, 734 | "joining_date": []string{"date:dd-mm-yyyy:custom_message"}, 735 | } 736 | 737 | opts := Options{ 738 | Request: req, 739 | Data: &userObj, 740 | Rules: rules, 741 | Messages: messages, 742 | } 743 | 744 | vd := New(opts) 745 | validationErr := vd.ValidateJSON() 746 | if validationErr.Get("dob") != "custom_message" { 747 | t.Error("Date custom message validation failed!") 748 | } 749 | if k := validationErr.Get("dob"); k != "custom_message" { 750 | t.Error("Date date:dd-mm-yyyy custom message validation failed!") 751 | } 752 | } 753 | 754 | func Test_Email(t *testing.T) { 755 | type user struct { 756 | Email string `json:"email"` 757 | } 758 | 759 | postUser := user{Email: "invalid email"} 760 | var userObj user 761 | 762 | body, _ := json.Marshal(postUser) 763 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 764 | 765 | rules := MapData{ 766 | "email": []string{"email"}, 767 | } 768 | 769 | opts := Options{ 770 | Request: req, 771 | Data: &userObj, 772 | Rules: rules, 773 | } 774 | 775 | vd := New(opts) 776 | validationErr := vd.ValidateJSON() 777 | if len(validationErr) != 1 { 778 | t.Log(validationErr) 779 | t.Error("Email validation failed!") 780 | } 781 | } 782 | 783 | func Test_Email_message(t *testing.T) { 784 | type user struct { 785 | Email string `json:"email"` 786 | } 787 | 788 | postUser := user{Email: "invalid email"} 789 | var userObj user 790 | 791 | body, _ := json.Marshal(postUser) 792 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 793 | 794 | rules := MapData{ 795 | "email": []string{"email"}, 796 | } 797 | 798 | messages := MapData{ 799 | "email": []string{"email:custom_message"}, 800 | } 801 | opts := Options{ 802 | Request: req, 803 | Data: &userObj, 804 | Rules: rules, 805 | Messages: messages, 806 | } 807 | 808 | vd := New(opts) 809 | validationErr := vd.ValidateJSON() 810 | if validationErr.Get("email") != "custom_message" { 811 | t.Error("Email message validation failed!") 812 | } 813 | } 814 | 815 | func Test_Float(t *testing.T) { 816 | type user struct { 817 | CGPA string `json:"cgpa"` 818 | } 819 | 820 | postUser := user{CGPA: "invalid cgpa"} 821 | var userObj user 822 | 823 | body, _ := json.Marshal(postUser) 824 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 825 | 826 | rules := MapData{ 827 | "cgpa": []string{"float"}, 828 | } 829 | 830 | opts := Options{ 831 | Request: req, 832 | Data: &userObj, 833 | Rules: rules, 834 | } 835 | 836 | vd := New(opts) 837 | validationErr := vd.ValidateJSON() 838 | if len(validationErr) != 1 { 839 | t.Log(validationErr) 840 | t.Error("Float validation failed!") 841 | } 842 | } 843 | 844 | func Test_Float_message(t *testing.T) { 845 | type user struct { 846 | CGPA string `json:"cgpa"` 847 | } 848 | 849 | postUser := user{CGPA: "invalid cgpa"} 850 | var userObj user 851 | 852 | body, _ := json.Marshal(postUser) 853 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 854 | 855 | rules := MapData{ 856 | "cgpa": []string{"float"}, 857 | } 858 | 859 | messages := MapData{ 860 | "cgpa": []string{"float:custom_message"}, 861 | } 862 | 863 | opts := Options{ 864 | Request: req, 865 | Data: &userObj, 866 | Rules: rules, 867 | Messages: messages, 868 | } 869 | 870 | vd := New(opts) 871 | validationErr := vd.ValidateJSON() 872 | if validationErr.Get("cgpa") != "custom_message" { 873 | t.Error("Float custom message failed!") 874 | } 875 | } 876 | 877 | func Test_IP(t *testing.T) { 878 | type user struct { 879 | IP string `json:"ip"` 880 | } 881 | 882 | postUser := user{IP: "invalid IP"} 883 | var userObj user 884 | 885 | body, _ := json.Marshal(postUser) 886 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 887 | 888 | rules := MapData{ 889 | "ip": []string{"ip"}, 890 | } 891 | 892 | opts := Options{ 893 | Request: req, 894 | Data: &userObj, 895 | Rules: rules, 896 | } 897 | 898 | vd := New(opts) 899 | validationErr := vd.ValidateJSON() 900 | if len(validationErr) != 1 { 901 | t.Log(validationErr) 902 | t.Error("IP validation failed!") 903 | } 904 | } 905 | 906 | func Test_IP_message(t *testing.T) { 907 | type user struct { 908 | IP string `json:"ip"` 909 | } 910 | 911 | postUser := user{IP: "invalid IP"} 912 | var userObj user 913 | 914 | body, _ := json.Marshal(postUser) 915 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 916 | 917 | messages := MapData{ 918 | "ip": []string{"ip:custom_message"}, 919 | } 920 | 921 | rules := MapData{ 922 | "ip": []string{"ip"}, 923 | } 924 | 925 | opts := Options{ 926 | Request: req, 927 | Data: &userObj, 928 | Rules: rules, 929 | Messages: messages, 930 | } 931 | 932 | vd := New(opts) 933 | validationErr := vd.ValidateJSON() 934 | if validationErr.Get("ip") != "custom_message" { 935 | t.Error("IP custom message failed!") 936 | } 937 | } 938 | 939 | func Test_IPv4(t *testing.T) { 940 | type user struct { 941 | IP string `json:"ip"` 942 | } 943 | 944 | postUser := user{IP: "invalid IP"} 945 | var userObj user 946 | 947 | body, _ := json.Marshal(postUser) 948 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 949 | 950 | rules := MapData{ 951 | "ip": []string{"ip_v4"}, 952 | } 953 | 954 | opts := Options{ 955 | Request: req, 956 | Data: &userObj, 957 | Rules: rules, 958 | } 959 | 960 | vd := New(opts) 961 | validationErr := vd.ValidateJSON() 962 | if len(validationErr) != 1 { 963 | t.Log(validationErr) 964 | t.Error("IP v4 validation failed!") 965 | } 966 | } 967 | 968 | func Test_IPv4_message(t *testing.T) { 969 | type user struct { 970 | IP string `json:"ip"` 971 | } 972 | 973 | postUser := user{IP: "invalid IP"} 974 | var userObj user 975 | 976 | body, _ := json.Marshal(postUser) 977 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 978 | 979 | messages := MapData{ 980 | "ip": []string{"ip_v4:custom_message"}, 981 | } 982 | 983 | rules := MapData{ 984 | "ip": []string{"ip_v4"}, 985 | } 986 | 987 | opts := Options{ 988 | Request: req, 989 | Data: &userObj, 990 | Rules: rules, 991 | Messages: messages, 992 | } 993 | 994 | vd := New(opts) 995 | validationErr := vd.ValidateJSON() 996 | if validationErr.Get("ip") != "custom_message" { 997 | t.Error("IP v4 custom message failed!") 998 | } 999 | } 1000 | 1001 | func Test_IPv6(t *testing.T) { 1002 | type user struct { 1003 | IP string `json:"ip"` 1004 | } 1005 | 1006 | postUser := user{IP: "invalid IP"} 1007 | var userObj user 1008 | 1009 | body, _ := json.Marshal(postUser) 1010 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1011 | 1012 | rules := MapData{ 1013 | "ip": []string{"ip_v6"}, 1014 | } 1015 | 1016 | opts := Options{ 1017 | Request: req, 1018 | Data: &userObj, 1019 | Rules: rules, 1020 | } 1021 | 1022 | vd := New(opts) 1023 | validationErr := vd.ValidateJSON() 1024 | if len(validationErr) != 1 { 1025 | t.Log(validationErr) 1026 | t.Error("IP v6 validation failed!") 1027 | } 1028 | } 1029 | 1030 | func Test_IPv6_message(t *testing.T) { 1031 | type user struct { 1032 | IP string `json:"ip"` 1033 | } 1034 | 1035 | postUser := user{IP: "invalid IP"} 1036 | var userObj user 1037 | 1038 | body, _ := json.Marshal(postUser) 1039 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1040 | 1041 | messages := MapData{ 1042 | "ip": []string{"ip_v6:custom_message"}, 1043 | } 1044 | 1045 | rules := MapData{ 1046 | "ip": []string{"ip_v6"}, 1047 | } 1048 | 1049 | opts := Options{ 1050 | Request: req, 1051 | Data: &userObj, 1052 | Rules: rules, 1053 | Messages: messages, 1054 | } 1055 | 1056 | vd := New(opts) 1057 | validationErr := vd.ValidateJSON() 1058 | if validationErr.Get("ip") != "custom_message" { 1059 | t.Error("IP v6 custom message failed!") 1060 | } 1061 | } 1062 | 1063 | func Test_JSON(t *testing.T) { 1064 | type user struct { 1065 | Settings string `json:"settings"` 1066 | } 1067 | 1068 | postUser := user{Settings: "invalid json"} 1069 | var userObj user 1070 | 1071 | body, _ := json.Marshal(postUser) 1072 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1073 | 1074 | rules := MapData{ 1075 | "settings": []string{"json"}, 1076 | } 1077 | 1078 | opts := Options{ 1079 | Request: req, 1080 | Data: &userObj, 1081 | Rules: rules, 1082 | } 1083 | 1084 | vd := New(opts) 1085 | validationErr := vd.ValidateJSON() 1086 | if len(validationErr) != 1 { 1087 | t.Log(validationErr) 1088 | t.Error("JSON validation failed!") 1089 | } 1090 | } 1091 | 1092 | func Test_JSON_valid(t *testing.T) { 1093 | type user struct { 1094 | Settings string `json:"settings"` 1095 | } 1096 | 1097 | postUser := user{Settings: `{"name": "John Doe", "age": 30}`} 1098 | var userObj user 1099 | 1100 | body, _ := json.Marshal(postUser) 1101 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1102 | 1103 | rules := MapData{ 1104 | "settings": []string{"json"}, 1105 | } 1106 | 1107 | opts := Options{ 1108 | Request: req, 1109 | Data: &userObj, 1110 | Rules: rules, 1111 | } 1112 | 1113 | vd := New(opts) 1114 | validationErr := vd.ValidateJSON() 1115 | if len(validationErr) != 0 { 1116 | t.Log(validationErr) 1117 | t.Error("Validation failed for valid JSON") 1118 | } 1119 | } 1120 | 1121 | func Test_JSON_message(t *testing.T) { 1122 | type user struct { 1123 | Settings string `json:"settings"` 1124 | } 1125 | 1126 | postUser := user{Settings: "invalid json"} 1127 | var userObj user 1128 | 1129 | body, _ := json.Marshal(postUser) 1130 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1131 | 1132 | messages := MapData{ 1133 | "settings": []string{"json:custom_message"}, 1134 | } 1135 | 1136 | rules := MapData{ 1137 | "settings": []string{"json"}, 1138 | } 1139 | 1140 | opts := Options{ 1141 | Request: req, 1142 | Data: &userObj, 1143 | Rules: rules, 1144 | Messages: messages, 1145 | } 1146 | 1147 | vd := New(opts) 1148 | validationErr := vd.ValidateJSON() 1149 | if validationErr.Get("settings") != "custom_message" { 1150 | t.Error("JSON custom message failed!") 1151 | } 1152 | } 1153 | 1154 | func Test_LatLon(t *testing.T) { 1155 | type Location struct { 1156 | Latitude string `json:"lat"` 1157 | Longitude string `json:"lon"` 1158 | } 1159 | 1160 | postLocation := Location{Latitude: "invalid lat", Longitude: "invalid lon"} 1161 | var loc Location 1162 | 1163 | body, _ := json.Marshal(postLocation) 1164 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1165 | 1166 | rules := MapData{ 1167 | "lat": []string{"lat"}, 1168 | "lon": []string{"lon"}, 1169 | } 1170 | 1171 | opts := Options{ 1172 | Request: req, 1173 | Data: &loc, 1174 | Rules: rules, 1175 | } 1176 | 1177 | vd := New(opts) 1178 | validationErr := vd.ValidateJSON() 1179 | if len(validationErr) != 2 { 1180 | t.Log(validationErr) 1181 | t.Error("Lat Lon validation failed!") 1182 | } 1183 | } 1184 | 1185 | func Test_LatLon_valid(t *testing.T) { 1186 | type Location struct { 1187 | Latitude string `json:"lat"` 1188 | Longitude string `json:"lon"` 1189 | } 1190 | 1191 | postLocation := Location{Latitude: "23.810332", Longitude: "90.412518"} 1192 | var loc Location 1193 | 1194 | body, _ := json.Marshal(postLocation) 1195 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1196 | 1197 | rules := MapData{ 1198 | "lat": []string{"lat"}, 1199 | "lon": []string{"lon"}, 1200 | } 1201 | 1202 | opts := Options{ 1203 | Request: req, 1204 | Data: &loc, 1205 | Rules: rules, 1206 | } 1207 | 1208 | vd := New(opts) 1209 | validationErr := vd.ValidateJSON() 1210 | if len(validationErr) != 0 { 1211 | t.Log(validationErr) 1212 | t.Error("Valid Lat Lon validation failed!") 1213 | } 1214 | } 1215 | 1216 | func Test_LatLon_message(t *testing.T) { 1217 | type Location struct { 1218 | Latitude string `json:"lat"` 1219 | Longitude string `json:"lon"` 1220 | } 1221 | 1222 | postLocation := Location{Latitude: "invalid lat", Longitude: "invalid lon"} 1223 | var loc Location 1224 | 1225 | body, _ := json.Marshal(postLocation) 1226 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1227 | 1228 | messages := MapData{ 1229 | "lat": []string{"lat:custom_message"}, 1230 | "lon": []string{"lon:custom_message"}, 1231 | } 1232 | 1233 | rules := MapData{ 1234 | "lat": []string{"lat"}, 1235 | "lon": []string{"lon"}, 1236 | } 1237 | 1238 | opts := Options{ 1239 | Request: req, 1240 | Data: &loc, 1241 | Rules: rules, 1242 | Messages: messages, 1243 | } 1244 | 1245 | vd := New(opts) 1246 | validationErr := vd.ValidateJSON() 1247 | if validationErr.Get("lat") != "custom_message" || 1248 | validationErr.Get("lon") != "custom_message" { 1249 | t.Error("Lat lon custom message failed") 1250 | } 1251 | } 1252 | 1253 | func Test_Len(t *testing.T) { 1254 | type user struct { 1255 | Name string `json:"name"` 1256 | Roll int `json:"roll"` 1257 | Permissions []string `json:"permissions"` 1258 | } 1259 | 1260 | postUser := user{ 1261 | Name: "john", 1262 | Roll: 11, 1263 | Permissions: []string{"create", "delete", "update"}, 1264 | } 1265 | var userObj user 1266 | 1267 | body, _ := json.Marshal(postUser) 1268 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1269 | 1270 | rules := MapData{ 1271 | "name": []string{"len:5"}, 1272 | "roll": []string{"len:5"}, 1273 | "permissions": []string{"len:10"}, 1274 | } 1275 | 1276 | opts := Options{ 1277 | Request: req, 1278 | Data: &userObj, 1279 | Rules: rules, 1280 | } 1281 | 1282 | vd := New(opts) 1283 | validationErr := vd.ValidateJSON() 1284 | if len(validationErr) != 3 { 1285 | t.Log(validationErr) 1286 | t.Error("Len validation failed!") 1287 | } 1288 | } 1289 | 1290 | func Test_Len_message(t *testing.T) { 1291 | type user struct { 1292 | Name string `json:"name"` 1293 | Roll int `json:"roll"` 1294 | Permissions []string `json:"permissions"` 1295 | } 1296 | 1297 | postUser := user{ 1298 | Name: "john", 1299 | Roll: 11, 1300 | Permissions: []string{"create", "delete", "update"}, 1301 | } 1302 | var userObj user 1303 | 1304 | body, _ := json.Marshal(postUser) 1305 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1306 | 1307 | messages := MapData{ 1308 | "name": []string{"len:custom_message"}, 1309 | "roll": []string{"len:custom_message"}, 1310 | "permissions": []string{"len:custom_message"}, 1311 | } 1312 | 1313 | rules := MapData{ 1314 | "name": []string{"len:5"}, 1315 | "roll": []string{"len:5"}, 1316 | "permissions": []string{"len:10"}, 1317 | } 1318 | 1319 | opts := Options{ 1320 | Request: req, 1321 | Data: &userObj, 1322 | Rules: rules, 1323 | Messages: messages, 1324 | } 1325 | 1326 | vd := New(opts) 1327 | validationErr := vd.ValidateJSON() 1328 | if validationErr.Get("name") != "custom_message" || 1329 | validationErr.Get("roll") != "custom_message" || 1330 | validationErr.Get("permissions") != "custom_message" { 1331 | t.Error("len custom message failed") 1332 | } 1333 | } 1334 | 1335 | func Test_MacAddress(t *testing.T) { 1336 | type user struct { 1337 | MacAddress string `json:"mac_address"` 1338 | } 1339 | 1340 | postUser := user{MacAddress: "e4:2b:e8:d3:41:0f"} 1341 | var userObj user 1342 | 1343 | body, _ := json.Marshal(postUser) 1344 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1345 | 1346 | rules := MapData{ 1347 | "mac_address": []string{"mac_address"}, 1348 | } 1349 | 1350 | messages := MapData{ 1351 | "mac_address": []string{"mac_address:custom_message"}, 1352 | } 1353 | 1354 | opts := Options{ 1355 | Request: req, 1356 | Data: &userObj, 1357 | Rules: rules, 1358 | Messages: messages, 1359 | } 1360 | 1361 | vd := New(opts) 1362 | validationErr := vd.ValidateJSON() 1363 | if len(validationErr) != 0 { 1364 | t.Error("Valid Mac Address validation failed!") 1365 | } 1366 | } 1367 | 1368 | func Test_MacAddress_message(t *testing.T) { 1369 | type user struct { 1370 | MacAddress string `json:"mac_address"` 1371 | } 1372 | 1373 | postUser := user{MacAddress: "invalid_mac_address"} 1374 | var userObj user 1375 | 1376 | body, _ := json.Marshal(postUser) 1377 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1378 | 1379 | rules := MapData{ 1380 | "mac_address": []string{"mac_address"}, 1381 | } 1382 | 1383 | messages := MapData{ 1384 | "mac_address": []string{"mac_address:custom_message"}, 1385 | } 1386 | 1387 | opts := Options{ 1388 | Request: req, 1389 | Data: &userObj, 1390 | Rules: rules, 1391 | Messages: messages, 1392 | } 1393 | 1394 | vd := New(opts) 1395 | validationErr := vd.ValidateJSON() 1396 | if validationErr.Get("mac_address") != "custom_message" { 1397 | t.Error("Mac Address custom message failed!") 1398 | } 1399 | } 1400 | 1401 | func Test_Numeric(t *testing.T) { 1402 | type user struct { 1403 | NID string `json:"nid"` 1404 | } 1405 | 1406 | postUser := user{NID: "invalid nid"} 1407 | var userObj user 1408 | 1409 | body, _ := json.Marshal(postUser) 1410 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1411 | 1412 | rules := MapData{ 1413 | "nid": []string{"numeric"}, 1414 | } 1415 | 1416 | messages := MapData{ 1417 | "nid": []string{"numeric:custom_message"}, 1418 | } 1419 | 1420 | opts := Options{ 1421 | Request: req, 1422 | Data: &userObj, 1423 | Rules: rules, 1424 | Messages: messages, 1425 | } 1426 | 1427 | vd := New(opts) 1428 | validationErr := vd.ValidateJSON() 1429 | if len(validationErr) != 1 { 1430 | t.Log(validationErr) 1431 | t.Error("Numeric validation failed!") 1432 | } 1433 | 1434 | if validationErr.Get("nid") != "custom_message" { 1435 | t.Error("Numeric custom message failed!") 1436 | } 1437 | } 1438 | 1439 | func Test_Numeric_valid(t *testing.T) { 1440 | type user struct { 1441 | NID string `json:"nid"` 1442 | NNID string `json:"nnid"` 1443 | } 1444 | 1445 | postUser := user{NID: "109922", NNID: "-109922"} 1446 | var userObj user 1447 | 1448 | body, _ := json.Marshal(postUser) 1449 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1450 | 1451 | rules := MapData{ 1452 | "nid": []string{"numeric"}, 1453 | "nnid": []string{"numeric"}, 1454 | } 1455 | 1456 | messages := MapData{ 1457 | "nid": []string{"numeric:custom_message"}, 1458 | "nnid": []string{"numeric:custom_message"}, 1459 | } 1460 | 1461 | opts := Options{ 1462 | Request: req, 1463 | Data: &userObj, 1464 | Rules: rules, 1465 | Messages: messages, 1466 | } 1467 | 1468 | vd := New(opts) 1469 | validationErr := vd.ValidateJSON() 1470 | if len(validationErr) != 0 { 1471 | t.Log(validationErr) 1472 | t.Error("Valid numeric validation failed!") 1473 | } 1474 | } 1475 | 1476 | func Test_NumericBetween(t *testing.T) { 1477 | type user struct { 1478 | Age int `json:"age"` 1479 | CGPA string `json:"cgpa"` 1480 | NAge int `json:"nage"` 1481 | NCGPA string `json:"ncgpa"` 1482 | Height int `json:"height"` 1483 | Weight string `json:"weight"` 1484 | } 1485 | 1486 | postUser := user{Age: 77, CGPA: "2.90", NAge: -55, NCGPA: "-2.90", Height: 5, Weight: "-200.0"} 1487 | var userObj user 1488 | 1489 | body, _ := json.Marshal(postUser) 1490 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1491 | 1492 | rules := MapData{ 1493 | "age": []string{"numeric_between:18,60"}, 1494 | "cgpa": []string{"numeric_between:3.5,4.9"}, 1495 | "nage": []string{"numeric_between:-60,-18"}, 1496 | "ncgpa": []string{"numeric_between:-4.9,-3.5"}, 1497 | "height": []string{"numeric_between:6,"}, 1498 | "weight": []string{"numeric_between:,-2000.0"}, 1499 | } 1500 | 1501 | messages := MapData{ 1502 | "age": []string{"numeric_between:custom_message"}, 1503 | "cgpa": []string{"numeric_between:custom_message"}, 1504 | "nage": []string{"numeric_between:custom_message"}, 1505 | "ncgpa": []string{"numeric_between:custom_message"}, 1506 | } 1507 | 1508 | opts := Options{ 1509 | Request: req, 1510 | Data: &userObj, 1511 | Rules: rules, 1512 | Messages: messages, 1513 | } 1514 | 1515 | vd := New(opts) 1516 | validationErr := vd.ValidateJSON() 1517 | 1518 | if len(validationErr) != 5 { 1519 | t.Error("numeric_between validation failed!") 1520 | } 1521 | 1522 | if validationErr.Get("age") != "custom_message" || 1523 | validationErr.Get("cgpa") != "custom_message" || 1524 | validationErr.Get("ncgpa") != "custom_message" { 1525 | t.Error("numeric_between custom message failed!") 1526 | } 1527 | 1528 | if validationErr.Get("height") != "The height field value can not be less than 6" { 1529 | t.Error("height unbounded max message failed!") 1530 | } 1531 | 1532 | if validationErr.Get("weight") != "The weight field value can not be greater than -2000" { 1533 | t.Error("height unbounded min message failed!") 1534 | } 1535 | } 1536 | 1537 | func Test_NumericBetween_invalid(t *testing.T) { 1538 | req, _ := http.NewRequest("GET", "/?field=1", bytes.NewReader([]byte{})) 1539 | validate := func(argument string) { 1540 | New(Options{ 1541 | Request: req, 1542 | Rules: MapData{ 1543 | "field": []string{argument}, 1544 | }, 1545 | }).Validate() 1546 | } 1547 | 1548 | assertPanicWith(t, errInvalidArgument, func() { validate("numeric_between:1") }) 1549 | assertPanicWith(t, errInvalidArgument, func() { validate("numeric_between:1,2,3") }) 1550 | assertPanicWith(t, errInvalidArgument, func() { validate("numeric_between:,") }) 1551 | } 1552 | 1553 | func assertPanicWith(t *testing.T, expectedError error, executer func()) { 1554 | defer func() { 1555 | r := recover() 1556 | if r == nil { 1557 | t.Errorf("the code did not panic") 1558 | } else if r != expectedError { 1559 | t.Errorf("expecting %v, got %v", expectedError, r) 1560 | } 1561 | }() 1562 | 1563 | executer() 1564 | } 1565 | 1566 | func Test_URL(t *testing.T) { 1567 | type user struct { 1568 | Web string `json:"web"` 1569 | } 1570 | 1571 | postUser := user{Web: "invalid url"} 1572 | var userObj user 1573 | 1574 | body, _ := json.Marshal(postUser) 1575 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1576 | 1577 | rules := MapData{ 1578 | "web": []string{"url"}, 1579 | } 1580 | 1581 | messages := MapData{ 1582 | "web": []string{"url:custom_message"}, 1583 | } 1584 | 1585 | opts := Options{ 1586 | Request: req, 1587 | Data: &userObj, 1588 | Rules: rules, 1589 | Messages: messages, 1590 | } 1591 | 1592 | vd := New(opts) 1593 | validationErr := vd.ValidateJSON() 1594 | if len(validationErr) != 1 { 1595 | t.Log(validationErr) 1596 | t.Error("URL validation failed!") 1597 | } 1598 | 1599 | if validationErr.Get("web") != "custom_message" { 1600 | t.Error("URL custom message failed!") 1601 | } 1602 | } 1603 | 1604 | func Test_UR_valid(t *testing.T) { 1605 | type user struct { 1606 | Web string `json:"web"` 1607 | } 1608 | 1609 | postUser := user{Web: "www.google.com"} 1610 | var userObj user 1611 | 1612 | body, _ := json.Marshal(postUser) 1613 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1614 | 1615 | rules := MapData{ 1616 | "web": []string{"url"}, 1617 | } 1618 | 1619 | messages := MapData{ 1620 | "web": []string{"url:custom_message"}, 1621 | } 1622 | 1623 | opts := Options{ 1624 | Request: req, 1625 | Data: &userObj, 1626 | Rules: rules, 1627 | Messages: messages, 1628 | } 1629 | 1630 | vd := New(opts) 1631 | validationErr := vd.ValidateJSON() 1632 | if len(validationErr) != 0 { 1633 | t.Error("Valid URL validation failed!") 1634 | } 1635 | } 1636 | 1637 | func Test_UUIDS(t *testing.T) { 1638 | type user struct { 1639 | UUID string `json:"uuid"` 1640 | UUIDV3 string `json:"uuid3"` 1641 | UUIDV4 string `json:"uuid4"` 1642 | UUIDV5 string `json:"uuid5"` 1643 | } 1644 | 1645 | postUser := user{ 1646 | UUID: "invalid uuid", 1647 | UUIDV3: "invalid uuid", 1648 | UUIDV4: "invalid uuid", 1649 | UUIDV5: "invalid uuid", 1650 | } 1651 | var userObj user 1652 | 1653 | body, _ := json.Marshal(postUser) 1654 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1655 | 1656 | rules := MapData{ 1657 | "uuid": []string{"uuid"}, 1658 | "uuid3": []string{"uuid_v3"}, 1659 | "uuid4": []string{"uuid_v4"}, 1660 | "uuid5": []string{"uuid_v5"}, 1661 | } 1662 | 1663 | messages := MapData{ 1664 | "uuid": []string{"uuid:custom_message"}, 1665 | "uuid3": []string{"uuid_v3:custom_message"}, 1666 | "uuid4": []string{"uuid_v4:custom_message"}, 1667 | "uuid5": []string{"uuid_v5:custom_message"}, 1668 | } 1669 | 1670 | opts := Options{ 1671 | Request: req, 1672 | Data: &userObj, 1673 | Rules: rules, 1674 | Messages: messages, 1675 | } 1676 | 1677 | vd := New(opts) 1678 | validationErr := vd.ValidateJSON() 1679 | if len(validationErr) != 4 { 1680 | t.Error("UUID validation failed!") 1681 | } 1682 | 1683 | if validationErr.Get("uuid") != "custom_message" || 1684 | validationErr.Get("uuid3") != "custom_message" || 1685 | validationErr.Get("uuid4") != "custom_message" || 1686 | validationErr.Get("uuid5") != "custom_message" { 1687 | t.Error("UUID custom message failed!") 1688 | } 1689 | 1690 | } 1691 | 1692 | func Test_min(t *testing.T) { 1693 | type Body struct { 1694 | Str string `json:"_str"` 1695 | Slice []string `json:"_slice"` 1696 | Int int `json:"_int"` 1697 | Int8 int8 `json:"_int8"` 1698 | Int16 int16 `json:"_int16"` 1699 | Int32 int32 `json:"_int32"` 1700 | Int64 int64 `json:"_int64"` 1701 | Uint uint `json:"_uint"` 1702 | Uint8 uint8 `json:"_uint8"` 1703 | Uint16 uint16 `json:"_uint16"` 1704 | Uint32 uint32 `json:"_uint32"` 1705 | Uint64 uint64 `json:"_uint64"` 1706 | Uintptr uintptr `json:"_uintptr"` 1707 | Float32 float32 `json:"_float32"` 1708 | Float64 float64 `json:"_float64"` 1709 | NInt int `json:"_nint"` 1710 | NInt8 int8 `json:"_nint8"` 1711 | NInt16 int16 `json:"_nint16"` 1712 | NInt32 int32 `json:"_nint32"` 1713 | NInt64 int64 `json:"_nint64"` 1714 | NFloat32 float32 `json:"_nfloat32"` 1715 | NFloat64 float64 `json:"_nfloat64"` 1716 | } 1717 | 1718 | postBody := Body{ 1719 | Str: "xyz", 1720 | Slice: []string{"x", "y"}, 1721 | Int: 2, 1722 | Int8: 2, 1723 | Int16: 2, 1724 | Int32: 2, 1725 | Int64: 2, 1726 | Uint: 2, 1727 | Uint8: 2, 1728 | Uint16: 2, 1729 | Uint32: 2, 1730 | Uint64: 2, 1731 | Uintptr: 2, 1732 | Float32: 2.4, 1733 | Float64: 3.2, 1734 | NInt: -2, 1735 | NInt8: -2, 1736 | NInt16: -2, 1737 | NInt32: -2, 1738 | NInt64: -2, 1739 | NFloat32: -2.4, 1740 | NFloat64: -3.2, 1741 | } 1742 | 1743 | var bodyObj Body 1744 | 1745 | body, _ := json.Marshal(postBody) 1746 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1747 | 1748 | rules := MapData{ 1749 | "_str": []string{"min:5"}, 1750 | "_slice": []string{"min:5"}, 1751 | "_int": []string{"min:5"}, 1752 | "_int8": []string{"min:5"}, 1753 | "_int16": []string{"min:5"}, 1754 | "_int32": []string{"min:5"}, 1755 | "_int64": []string{"min:5"}, 1756 | "_uint": []string{"min:5"}, 1757 | "_uint8": []string{"min:5"}, 1758 | "_uint16": []string{"min:5"}, 1759 | "_uint32": []string{"min:5"}, 1760 | "_uint64": []string{"min:5"}, 1761 | "_uintptr": []string{"min:5"}, 1762 | "_float32": []string{"min:5"}, 1763 | "_float64": []string{"min:5"}, 1764 | "_nint": []string{"min:-1"}, 1765 | "_nint8": []string{"min:-1"}, 1766 | "_nint16": []string{"min:-1"}, 1767 | "_nint32": []string{"min:-1"}, 1768 | "_nint64": []string{"min:-1"}, 1769 | "_nfloat32": []string{"min:-1"}, 1770 | "_nfloat64": []string{"min:-1"}, 1771 | } 1772 | 1773 | messages := MapData{ 1774 | "_str": []string{"min:custom_message"}, 1775 | "_slice": []string{"min:custom_message"}, 1776 | "_int": []string{"min:custom_message"}, 1777 | "_uint": []string{"min:custom_message"}, 1778 | "_float32": []string{"min:custom_message"}, 1779 | } 1780 | 1781 | opts := Options{ 1782 | Request: req, 1783 | Data: &bodyObj, 1784 | Rules: rules, 1785 | Messages: messages, 1786 | } 1787 | 1788 | vd := New(opts) 1789 | validationErr := vd.ValidateJSON() 1790 | if len(validationErr) != 22 { 1791 | t.Error("min validation failed!") 1792 | } 1793 | 1794 | if validationErr.Get("_str") != "custom_message" || 1795 | validationErr.Get("_slice") != "custom_message" || 1796 | validationErr.Get("_int") != "custom_message" || 1797 | validationErr.Get("_uint") != "custom_message" || 1798 | validationErr.Get("_float32") != "custom_message" { 1799 | t.Error("min custom message failed!") 1800 | } 1801 | } 1802 | 1803 | func Test_max(t *testing.T) { 1804 | type Body struct { 1805 | Str string `json:"_str"` 1806 | Slice []string `json:"_slice"` 1807 | Int int `json:"_int"` 1808 | Int8 int8 `json:"_int8"` 1809 | Int16 int16 `json:"_int16"` 1810 | Int32 int32 `json:"_int32"` 1811 | Int64 int64 `json:"_int64"` 1812 | Uint uint `json:"_uint"` 1813 | Uint8 uint8 `json:"_uint8"` 1814 | Uint16 uint16 `json:"_uint16"` 1815 | Uint32 uint32 `json:"_uint32"` 1816 | Uint64 uint64 `json:"_uint64"` 1817 | Uintptr uintptr `json:"_uintptr"` 1818 | Float32 float32 `json:"_float32"` 1819 | Float64 float64 `json:"_float64"` 1820 | NInt int `json:"_nint"` 1821 | NInt8 int8 `json:"_nint8"` 1822 | NInt16 int16 `json:"_nint16"` 1823 | NInt32 int32 `json:"_nint32"` 1824 | NInt64 int64 `json:"_nint64"` 1825 | NFloat32 float32 `json:"_nfloat32"` 1826 | NFloat64 float64 `json:"_nfloat64"` 1827 | } 1828 | 1829 | postBody := Body{ 1830 | Str: "xyzabc", 1831 | Slice: []string{"x", "y", "z"}, 1832 | Int: 20, 1833 | Int8: 20, 1834 | Int16: 20, 1835 | Int32: 20, 1836 | Int64: 20, 1837 | Uint: 20, 1838 | Uint8: 20, 1839 | Uint16: 20, 1840 | Uint32: 20, 1841 | Uint64: 20, 1842 | Uintptr: 20, 1843 | Float32: 20.4, 1844 | Float64: 30.2, 1845 | NInt: -20, 1846 | NInt8: -20, 1847 | NInt16: -20, 1848 | NInt32: -20, 1849 | NInt64: -20, 1850 | NFloat32: -20.4, 1851 | NFloat64: -30.2, 1852 | } 1853 | 1854 | var bodyObj Body 1855 | 1856 | body, _ := json.Marshal(postBody) 1857 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1858 | 1859 | rules := MapData{ 1860 | "_str": []string{"max:5"}, 1861 | "_slice": []string{"max:2"}, 1862 | "_int": []string{"max:5"}, 1863 | "_int8": []string{"max:5"}, 1864 | "_int16": []string{"max:5"}, 1865 | "_int32": []string{"max:5"}, 1866 | "_int64": []string{"max:5"}, 1867 | "_uint": []string{"max:5"}, 1868 | "_uint8": []string{"max:5"}, 1869 | "_uint16": []string{"max:5"}, 1870 | "_uint32": []string{"max:5"}, 1871 | "_uint64": []string{"max:5"}, 1872 | "_uintptr": []string{"max:5"}, 1873 | "_float32": []string{"max:5"}, 1874 | "_float64": []string{"max:5"}, 1875 | "_nint": []string{"max:-50"}, 1876 | "_nint8": []string{"max:-50"}, 1877 | "_nint16": []string{"max:-50"}, 1878 | "_nint32": []string{"max:-50"}, 1879 | "_nint64": []string{"max:-50"}, 1880 | "_nfloat32": []string{"max:-50"}, 1881 | "_nfloat64": []string{"max:-50"}, 1882 | } 1883 | 1884 | messages := MapData{ 1885 | "_str": []string{"max:custom_message"}, 1886 | "_slice": []string{"max:custom_message"}, 1887 | "_int": []string{"max:custom_message"}, 1888 | "_uint": []string{"max:custom_message"}, 1889 | "_float32": []string{"max:custom_message"}, 1890 | } 1891 | 1892 | opts := Options{ 1893 | Request: req, 1894 | Data: &bodyObj, 1895 | Rules: rules, 1896 | Messages: messages, 1897 | } 1898 | 1899 | vd := New(opts) 1900 | validationErr := vd.ValidateJSON() 1901 | if len(validationErr) != 22 { 1902 | t.Error(validationErr) 1903 | t.Error("max validation failed!") 1904 | } 1905 | 1906 | if validationErr.Get("_str") != "custom_message" || 1907 | validationErr.Get("_slice") != "custom_message" || 1908 | validationErr.Get("_int") != "custom_message" || 1909 | validationErr.Get("_uint") != "custom_message" || 1910 | validationErr.Get("_float32") != "custom_message" { 1911 | t.Error("max custom message failed!") 1912 | } 1913 | } 1914 | 1915 | func Test_In(t *testing.T) { 1916 | type user struct { 1917 | Input string `json:"input"` 1918 | } 1919 | 1920 | postUser := user{Input: "4"} 1921 | var userObj user 1922 | 1923 | body, _ := json.Marshal(postUser) 1924 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1925 | 1926 | messages := MapData{ 1927 | "input": []string{"in:custom_message"}, 1928 | } 1929 | 1930 | rules := MapData{ 1931 | "input": []string{"in:1,2,3"}, 1932 | } 1933 | 1934 | opts := Options{ 1935 | Request: req, 1936 | Data: &userObj, 1937 | Rules: rules, 1938 | Messages: messages, 1939 | } 1940 | 1941 | vd := New(opts) 1942 | validationErr := vd.ValidateJSON() 1943 | if len(validationErr) != 1 { 1944 | t.Error("in validation failed!") 1945 | } 1946 | 1947 | if validationErr.Get("input") != "custom_message" { 1948 | t.Error("in custom message failed!") 1949 | } 1950 | } 1951 | 1952 | func Test_In_valid(t *testing.T) { 1953 | type user struct { 1954 | Input string `json:"input"` 1955 | } 1956 | 1957 | postUser := user{Input: "1"} 1958 | var userObj user 1959 | 1960 | body, _ := json.Marshal(postUser) 1961 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1962 | 1963 | messages := MapData{ 1964 | "input": []string{"in:custom_message"}, 1965 | } 1966 | 1967 | rules := MapData{ 1968 | "input": []string{"in:1,2,3"}, 1969 | } 1970 | 1971 | opts := Options{ 1972 | Request: req, 1973 | Data: &userObj, 1974 | Rules: rules, 1975 | Messages: messages, 1976 | } 1977 | 1978 | vd := New(opts) 1979 | validationErr := vd.ValidateJSON() 1980 | if len(validationErr) != 0 { 1981 | t.Error("in validation was triggered when valid!") 1982 | } 1983 | } 1984 | 1985 | func Test_In_string(t *testing.T) { 1986 | type user struct { 1987 | Input string `json:"input"` 1988 | } 1989 | 1990 | postUser := user{Input: "bob"} 1991 | var userObj user 1992 | 1993 | body, _ := json.Marshal(postUser) 1994 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 1995 | 1996 | messages := MapData{ 1997 | "input": []string{"in:custom_message"}, 1998 | } 1999 | 2000 | rules := MapData{ 2001 | "input": []string{"in:tom,dick,harry"}, 2002 | } 2003 | 2004 | opts := Options{ 2005 | Request: req, 2006 | Data: &userObj, 2007 | Rules: rules, 2008 | Messages: messages, 2009 | } 2010 | 2011 | vd := New(opts) 2012 | validationErr := vd.ValidateJSON() 2013 | if len(validationErr) != 1 { 2014 | t.Error("in validation failed!") 2015 | } 2016 | 2017 | if validationErr.Get("input") != "custom_message" { 2018 | t.Error("in custom message failed!") 2019 | } 2020 | } 2021 | 2022 | func Test_In_string_valid(t *testing.T) { 2023 | type user struct { 2024 | Input string `json:"input"` 2025 | } 2026 | 2027 | postUser := user{Input: "dick"} 2028 | var userObj user 2029 | 2030 | body, _ := json.Marshal(postUser) 2031 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 2032 | 2033 | messages := MapData{ 2034 | "input": []string{"in:custom_message"}, 2035 | } 2036 | 2037 | rules := MapData{ 2038 | "input": []string{"in:tom,dick,harry"}, 2039 | } 2040 | 2041 | opts := Options{ 2042 | Request: req, 2043 | Data: &userObj, 2044 | Rules: rules, 2045 | Messages: messages, 2046 | } 2047 | 2048 | vd := New(opts) 2049 | validationErr := vd.ValidateJSON() 2050 | if len(validationErr) != 0 { 2051 | t.Error("in validation was triggered when valid!") 2052 | } 2053 | } 2054 | 2055 | func Test_NotIn(t *testing.T) { 2056 | type user struct { 2057 | Input string `json:"input"` 2058 | } 2059 | 2060 | postUser := user{Input: "2"} 2061 | var userObj user 2062 | 2063 | body, _ := json.Marshal(postUser) 2064 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 2065 | 2066 | messages := MapData{ 2067 | "input": []string{"not_in:custom_message"}, 2068 | } 2069 | 2070 | rules := MapData{ 2071 | "input": []string{"not_in:1,2,3"}, 2072 | } 2073 | 2074 | opts := Options{ 2075 | Request: req, 2076 | Data: &userObj, 2077 | Rules: rules, 2078 | Messages: messages, 2079 | } 2080 | 2081 | vd := New(opts) 2082 | validationErr := vd.ValidateJSON() 2083 | if len(validationErr) != 1 { 2084 | t.Error("not_in validation failed!") 2085 | } 2086 | 2087 | if validationErr.Get("input") != "custom_message" { 2088 | t.Error("not_in custom message failed!") 2089 | } 2090 | } 2091 | 2092 | func Test_NotIn_valid(t *testing.T) { 2093 | type user struct { 2094 | Input string `json:"input"` 2095 | } 2096 | 2097 | postUser := user{Input: "4"} 2098 | var userObj user 2099 | 2100 | body, _ := json.Marshal(postUser) 2101 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 2102 | 2103 | messages := MapData{ 2104 | "input": []string{"not_in:custom_message"}, 2105 | } 2106 | 2107 | rules := MapData{ 2108 | "input": []string{"not_in:1,2,3"}, 2109 | } 2110 | 2111 | opts := Options{ 2112 | Request: req, 2113 | Data: &userObj, 2114 | Rules: rules, 2115 | Messages: messages, 2116 | } 2117 | 2118 | vd := New(opts) 2119 | validationErr := vd.ValidateJSON() 2120 | if len(validationErr) != 0 { 2121 | t.Error("not_in validation was triggered when valid!") 2122 | } 2123 | } 2124 | 2125 | func Test_NotIn_string(t *testing.T) { 2126 | type user struct { 2127 | Input string `json:"input"` 2128 | } 2129 | 2130 | postUser := user{Input: "harry"} 2131 | var userObj user 2132 | 2133 | body, _ := json.Marshal(postUser) 2134 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 2135 | 2136 | messages := MapData{ 2137 | "input": []string{"not_in:custom_message"}, 2138 | } 2139 | 2140 | rules := MapData{ 2141 | "input": []string{"not_in:tom,dick,harry"}, 2142 | } 2143 | 2144 | opts := Options{ 2145 | Request: req, 2146 | Data: &userObj, 2147 | Rules: rules, 2148 | Messages: messages, 2149 | } 2150 | 2151 | vd := New(opts) 2152 | validationErr := vd.ValidateJSON() 2153 | if len(validationErr) != 1 { 2154 | t.Error("not_in validation failed!") 2155 | } 2156 | 2157 | if validationErr.Get("input") != "custom_message" { 2158 | t.Error("not_in custom message failed!") 2159 | } 2160 | } 2161 | 2162 | func Test_NotIn_string_valid(t *testing.T) { 2163 | type user struct { 2164 | Input string `json:"input"` 2165 | } 2166 | 2167 | postUser := user{Input: "bob"} 2168 | var userObj user 2169 | 2170 | body, _ := json.Marshal(postUser) 2171 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 2172 | 2173 | messages := MapData{ 2174 | "input": []string{"not_in:custom_message"}, 2175 | } 2176 | 2177 | rules := MapData{ 2178 | "input": []string{"not_in:tom,dick,harry"}, 2179 | } 2180 | 2181 | opts := Options{ 2182 | Request: req, 2183 | Data: &userObj, 2184 | Rules: rules, 2185 | Messages: messages, 2186 | } 2187 | 2188 | vd := New(opts) 2189 | validationErr := vd.ValidateJSON() 2190 | if len(validationErr) != 0 { 2191 | t.Error("not_in validation was triggered when valid!") 2192 | } 2193 | } 2194 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // Int describes a custom type of built-in int data type 9 | type Int struct { 10 | Value int `json:"value"` 11 | IsSet bool `json:"isSet"` 12 | } 13 | 14 | var null = []byte("null") 15 | 16 | // UnmarshalJSON ... 17 | func (i *Int) UnmarshalJSON(data []byte) error { 18 | if bytes.Equal(data, null) { 19 | return nil 20 | } 21 | i.IsSet = true 22 | var temp int 23 | if err := json.Unmarshal(data, &temp); err != nil { 24 | return err 25 | } 26 | i.Value = temp 27 | return nil 28 | } 29 | 30 | // MarshalJSON ... 31 | func (i *Int) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(i.Value) 33 | } 34 | 35 | // Int64 describes a custom type of built-in int64 data type 36 | type Int64 struct { 37 | Value int64 `json:"value"` 38 | IsSet bool `json:"isSet"` 39 | } 40 | 41 | // UnmarshalJSON ... 42 | func (i *Int64) UnmarshalJSON(data []byte) error { 43 | if bytes.Equal(data, null) { 44 | return nil 45 | } 46 | i.IsSet = true 47 | var temp int64 48 | if err := json.Unmarshal(data, &temp); err != nil { 49 | return err 50 | } 51 | i.Value = temp 52 | return nil 53 | } 54 | 55 | // MarshalJSON ... 56 | func (i *Int64) MarshalJSON() ([]byte, error) { 57 | return json.Marshal(i.Value) 58 | } 59 | 60 | // Float32 describes a custom type of built-in float32 data type 61 | type Float32 struct { 62 | Value float32 `json:"value"` 63 | IsSet bool `json:"isSet"` 64 | } 65 | 66 | // UnmarshalJSON ... 67 | func (i *Float32) UnmarshalJSON(data []byte) error { 68 | if bytes.Equal(data, null) { 69 | return nil 70 | } 71 | i.IsSet = true 72 | var temp float32 73 | if err := json.Unmarshal(data, &temp); err != nil { 74 | return err 75 | } 76 | i.Value = temp 77 | return nil 78 | } 79 | 80 | // MarshalJSON ... 81 | func (i *Float32) MarshalJSON() ([]byte, error) { 82 | return json.Marshal(i.Value) 83 | } 84 | 85 | // Float64 describes a custom type of built-in float64 data type 86 | type Float64 struct { 87 | Value float64 `json:"value"` 88 | IsSet bool `json:"isSet"` 89 | } 90 | 91 | // UnmarshalJSON ... 92 | func (i *Float64) UnmarshalJSON(data []byte) error { 93 | if bytes.Equal(data, null) { 94 | return nil 95 | } 96 | i.IsSet = true 97 | var temp float64 98 | if err := json.Unmarshal(data, &temp); err != nil { 99 | return err 100 | } 101 | i.Value = temp 102 | return nil 103 | } 104 | 105 | // MarshalJSON ... 106 | func (i *Float64) MarshalJSON() ([]byte, error) { 107 | return json.Marshal(i.Value) 108 | } 109 | 110 | // Bool describes a custom type of built-in bool data type 111 | type Bool struct { 112 | Value bool `json:"value"` 113 | IsSet bool `json:"isSet"` 114 | } 115 | 116 | // UnmarshalJSON ... 117 | func (i *Bool) UnmarshalJSON(data []byte) error { 118 | if bytes.Equal(data, null) { 119 | return nil 120 | } 121 | i.IsSet = true 122 | var temp bool 123 | if err := json.Unmarshal(data, &temp); err != nil { 124 | return err 125 | } 126 | i.Value = temp 127 | return nil 128 | } 129 | 130 | // MarshalJSON ... 131 | func (i *Bool) MarshalJSON() ([]byte, error) { 132 | return json.Marshal(i.Value) 133 | } 134 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // containsRequiredField check rules contain any required field 10 | func isContainRequiredField(rules []string) bool { 11 | for _, rule := range rules { 12 | if rule == "required" { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | 19 | // isRuleExist check if the provided rule name is exist or not 20 | func isRuleExist(rule string) bool { 21 | if strings.Contains(rule, ":") { 22 | rule = strings.Split(rule, ":")[0] 23 | } 24 | extendedRules := []string{"size", "mime", "ext"} 25 | for _, r := range extendedRules { 26 | if r == rule { 27 | return true 28 | } 29 | } 30 | if _, ok := rulesFuncMap[rule]; ok { 31 | return true 32 | } 33 | return false 34 | } 35 | 36 | // toString force data to be string 37 | func toString(v interface{}) string { 38 | str, ok := v.(string) 39 | if !ok { 40 | str = fmt.Sprintf("%v", v) 41 | } 42 | return str 43 | } 44 | 45 | // isEmpty check a type is Zero 46 | func isEmpty(x interface{}) bool { 47 | rt := reflect.TypeOf(x) 48 | if rt == nil { 49 | return true 50 | } 51 | rv := reflect.ValueOf(x) 52 | switch rv.Kind() { 53 | case reflect.Array, reflect.Map, reflect.Slice: 54 | return rv.Len() == 0 55 | } 56 | return reflect.DeepEqual(x, reflect.Zero(rt).Interface()) 57 | } 58 | -------------------------------------------------------------------------------- /utils110.go: -------------------------------------------------------------------------------- 1 | // +build go1.10 2 | 3 | package govalidator 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // getFileInfo read file from request and return file name, extension, mime and size 13 | func getFileInfo(r *http.Request, field string) (bool, string, string, string, int64, error) { 14 | file, multipartFileHeader, err := r.FormFile(field) 15 | if err != nil { 16 | return false, "", "", "", 0, err 17 | } 18 | // Create a buffer to store the header of the file in 19 | fileHeader := make([]byte, 512) 20 | 21 | // Copy the headers into the FileHeader buffer 22 | if _, err := file.Read(fileHeader); err != nil { 23 | if err != io.EOF { 24 | return false, "", "", "", 0, err 25 | } 26 | } 27 | 28 | // set position back to start. 29 | if _, err := file.Seek(0, 0); err != nil { 30 | return false, "", "", "", 0, err 31 | } 32 | 33 | mime := http.DetectContentType(fileHeader) 34 | if subs := "; charset=utf-8"; strings.Contains(mime, subs) { 35 | mime = strings.Replace(mime, subs, "", -1) 36 | } 37 | if subs := ";charset=utf-8"; strings.Contains(mime, subs) { 38 | mime = strings.Replace(mime, subs, "", -1) 39 | } 40 | if subs := "; charset=UTF-8"; strings.Contains(mime, subs) { 41 | mime = strings.Replace(mime, subs, "", -1) 42 | } 43 | if subs := ";charset=UTF-8"; strings.Contains(mime, subs) { 44 | mime = strings.Replace(mime, subs, "", -1) 45 | } 46 | fExist := false 47 | if file != nil { 48 | fExist = true 49 | } 50 | return fExist, multipartFileHeader.Filename, 51 | strings.TrimPrefix(filepath.Ext(multipartFileHeader.Filename), "."), 52 | strings.TrimSpace(mime), 53 | multipartFileHeader.Size, 54 | nil 55 | } 56 | -------------------------------------------------------------------------------- /utils_pre110.go: -------------------------------------------------------------------------------- 1 | // +build !go1.10 2 | 3 | package govalidator 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // Sizer interface 13 | type Sizer interface { 14 | Size() int64 15 | } 16 | 17 | // getFileInfo read file from request and return file name, extension, mime and size 18 | func getFileInfo(r *http.Request, field string) (bool, string, string, string, int64, error) { 19 | file, multipartFileHeader, err := r.FormFile(field) 20 | if err != nil { 21 | return false, "", "", "", 0, err 22 | } 23 | // Create a buffer to store the header of the file in 24 | fileHeader := make([]byte, 512) 25 | 26 | // Copy the headers into the FileHeader buffer 27 | if _, err := file.Read(fileHeader); err != nil { 28 | if err != io.EOF { 29 | return false, "", "", "", 0, err 30 | } 31 | } 32 | 33 | // set position back to start. 34 | if _, err := file.Seek(0, 0); err != nil { 35 | return false, "", "", "", 0, err 36 | } 37 | 38 | mime := http.DetectContentType(fileHeader) 39 | if subs := "; charset=utf-8"; strings.Contains(mime, subs) { 40 | mime = strings.Replace(mime, subs, "", -1) 41 | } 42 | if subs := ";charset=utf-8"; strings.Contains(mime, subs) { 43 | mime = strings.Replace(mime, subs, "", -1) 44 | } 45 | if subs := "; charset=UTF-8"; strings.Contains(mime, subs) { 46 | mime = strings.Replace(mime, subs, "", -1) 47 | } 48 | if subs := ";charset=UTF-8"; strings.Contains(mime, subs) { 49 | mime = strings.Replace(mime, subs, "", -1) 50 | } 51 | fExist := false 52 | if file != nil { 53 | fExist = true 54 | } 55 | return fExist, multipartFileHeader.Filename, 56 | strings.TrimPrefix(filepath.Ext(multipartFileHeader.Filename), "."), 57 | strings.TrimSpace(mime), 58 | file.(Sizer).Size(), 59 | nil 60 | } 61 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func Test_isContainRequiredField(t *testing.T) { 10 | if !isContainRequiredField([]string{"required", "email"}) { 11 | t.Error("isContainRequiredField failed!") 12 | } 13 | 14 | if isContainRequiredField([]string{"numeric", "min:5"}) { 15 | t.Error("isContainRequiredField failed!") 16 | } 17 | } 18 | 19 | func Benchmark_isContainRequiredField(b *testing.B) { 20 | for n := 0; n < b.N; n++ { 21 | isContainRequiredField([]string{"required", "email"}) 22 | } 23 | } 24 | 25 | func Test_isRuleExist(t *testing.T) { 26 | if !isRuleExist("required") { 27 | t.Error("isRuleExist failed for valid rule") 28 | } 29 | if isRuleExist("not exist") { 30 | t.Error("isRuleExist failed for invalid rule") 31 | } 32 | if !isRuleExist("mime") { 33 | t.Error("extended rules failed") 34 | } 35 | } 36 | 37 | func Test_toString(t *testing.T) { 38 | Int := 100 39 | str := toString(Int) 40 | typ := reflect.ValueOf(str).Kind() 41 | if typ != reflect.String { 42 | t.Error("toString failed!") 43 | } 44 | } 45 | 46 | func Test_isEmpty(t *testing.T) { 47 | var Int int 48 | var Int8 int 49 | var Float32 float32 50 | var Str string 51 | var Slice []int 52 | var e interface{} 53 | list := map[string]interface{}{ 54 | "_int": Int, 55 | "_int8": Int8, 56 | "_float32": Float32, 57 | "_str": Str, 58 | "_slice": Slice, 59 | "_empty_interface": e, 60 | } 61 | for k, v := range list { 62 | if !isEmpty(v) { 63 | t.Errorf("%v failed", k) 64 | } 65 | } 66 | } 67 | 68 | func Test_getFileInfo(t *testing.T) { 69 | req, err := buildMocFormReq() 70 | if err != nil { 71 | t.Error("request failed", err) 72 | } 73 | fExist, fn, ext, mime, size, _ := getFileInfo(req, "file") 74 | if !fExist { 75 | t.Error("file does not exist") 76 | } 77 | if fn != "BENCHMARK.md" { 78 | t.Error("failed to get file name") 79 | } 80 | if ext != "md" { 81 | t.Error("failed to get file extension") 82 | } 83 | if !strings.Contains(mime, "text/plain") { 84 | t.Log(mime) 85 | t.Error("failed to get file mime") 86 | } 87 | if size <= 0 { 88 | t.Error("failed to get file size") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /validate_file.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // validateFiles validate file size, mimes, extension etc 12 | func validateFiles(r *http.Request, field, rule, msg string, errsBag url.Values) { 13 | _, _, ext, mime, size, fErr := getFileInfo(r, field) 14 | // check size 15 | if strings.HasPrefix(rule, "size:") { 16 | l, err := strconv.ParseInt(strings.TrimPrefix(rule, "size:"), 10, 64) 17 | if err != nil { 18 | panic(errStringToInt) 19 | } 20 | if size > l { 21 | if msg != "" { 22 | errsBag.Add(field, msg) 23 | } else { 24 | errsBag.Add(field, fmt.Sprintf("The %s field size is can not be greater than %d bytes", field, l)) 25 | } 26 | } 27 | if fErr != nil { 28 | errsBag.Add(field, fmt.Sprintf("The %s field failed to read file when fetching size", field)) 29 | } 30 | } 31 | 32 | // check extension 33 | if strings.HasPrefix(rule, "ext:") { 34 | exts := strings.Split(strings.TrimPrefix(rule, "ext:"), ",") 35 | f := false 36 | for _, e := range exts { 37 | if e == ext { 38 | f = true 39 | } 40 | } 41 | if !f { 42 | if msg != "" { 43 | errsBag.Add(field, msg) 44 | } else { 45 | errsBag.Add(field, fmt.Sprintf("The %s field file extension %s is invalid", field, ext)) 46 | } 47 | } 48 | if fErr != nil { 49 | errsBag.Add(field, fmt.Sprintf("The %s field failed to read file when fetching extension", field)) 50 | } 51 | } 52 | 53 | // check mimes 54 | if strings.HasPrefix(rule, "mime:") { 55 | mimes := strings.Split(strings.TrimPrefix(rule, "mime:"), ",") 56 | f := false 57 | for _, m := range mimes { 58 | if m == mime { 59 | f = true 60 | } 61 | } 62 | if !f { 63 | if msg != "" { 64 | errsBag.Add(field, msg) 65 | } else { 66 | errsBag.Add(field, fmt.Sprintf("The %s field file mime %s is invalid", field, mime)) 67 | } 68 | } 69 | if fErr != nil { 70 | errsBag.Add(field, fmt.Sprintf("The %s field failed to read file when fetching mime", field)) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /validate_file_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime/multipart" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | ) 12 | 13 | // buildMocFormReq prepare a moc form data request with a test file 14 | func buildMocFormReq() (*http.Request, error) { 15 | fPath := "doc/BENCHMARK.md" 16 | body := &bytes.Buffer{} 17 | writer := multipart.NewWriter(body) 18 | file, err := os.Open(fPath) 19 | if err != nil { 20 | return nil, err 21 | } 22 | part, err := writer.CreateFormFile("file", filepath.Base(fPath)) 23 | if err != nil { 24 | return nil, err 25 | } 26 | _, _ = io.Copy(part, file) 27 | _ = file.Close() 28 | err = writer.Close() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | req, err := http.NewRequest("POST", "www.example.com", body) 34 | if err != nil { 35 | return nil, err 36 | } 37 | req.Header.Set("Content-Type", writer.FormDataContentType()) 38 | return req, nil 39 | } 40 | 41 | func Test_validateFiles(t *testing.T) { 42 | req, err := buildMocFormReq() 43 | if err != nil { 44 | t.Error("request failed", err) 45 | } 46 | rules := MapData{ 47 | "file:file": []string{"ext:jpg,pdf", "size:10", "mime:application/pdf", "required"}, 48 | } 49 | 50 | opts := Options{ 51 | Request: req, 52 | Rules: rules, 53 | } 54 | 55 | vd := New(opts) 56 | validationErr := vd.Validate() 57 | if len(validationErr) != 1 { 58 | t.Error("file validation failed!") 59 | } 60 | } 61 | 62 | func Test_validateFiles_message(t *testing.T) { 63 | req, err := buildMocFormReq() 64 | if err != nil { 65 | t.Error("request failed", err) 66 | } 67 | rules := MapData{ 68 | "file:file": []string{"ext:jpg,pdf", "size:10", "mime:application/pdf", "required"}, 69 | } 70 | 71 | msgs := MapData{ 72 | "file:file": []string{"ext:custom_message"}, 73 | } 74 | 75 | opts := Options{ 76 | Request: req, 77 | Rules: rules, 78 | Messages: msgs, 79 | } 80 | 81 | vd := New(opts) 82 | validationErr := vd.Validate() 83 | if len(validationErr) != 1 { 84 | t.Error("file validation failed!") 85 | } 86 | if validationErr.Get("file") != "custom_message" { 87 | t.Log(validationErr) 88 | t.Error("failed custom message for file validation") 89 | } 90 | } 91 | 92 | func Test_validateFiles_CustomRule(t *testing.T) { 93 | req, err := buildMocFormReq() 94 | if err != nil { 95 | t.Error("request failed", err) 96 | } 97 | 98 | customRule1WasExecuted := false 99 | isMultipartFile := false 100 | AddCustomRule("customRule1", func(field string, rule string, message string, value interface{}) error { 101 | customRule1WasExecuted = true 102 | _, isMultipartFile = value.(multipart.File) 103 | return nil 104 | }) 105 | 106 | customRule2WasExecuted := false 107 | isValueNil := false 108 | AddCustomRule("customRule2", func(field string, rule string, message string, value interface{}) error { 109 | customRule2WasExecuted = true 110 | isValueNil = value == nil 111 | return nil 112 | }) 113 | 114 | rules := MapData{ 115 | "file:file": []string{"customRule1"}, 116 | "file:avatar": []string{"customRule2"}, 117 | } 118 | 119 | opts := Options{ 120 | Request: req, 121 | Rules: rules, 122 | } 123 | 124 | vd := New(opts) 125 | vd.Validate() 126 | if !customRule1WasExecuted { 127 | t.Error("file validation performed without custom rule!") 128 | } 129 | 130 | if !isMultipartFile { 131 | t.Error("passed to custom rule value is not file!") 132 | } 133 | 134 | if !customRule2WasExecuted { 135 | t.Error("file validation performed without custom rule!") 136 | } 137 | 138 | if !isValueNil { 139 | t.Error("passed to custom rule value is not nil!") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | tagIdentifier = "json" //tagName idetify the struct tag for govalidator 14 | tagSeparator = "|" //tagSeparator use to separate tags in struct 15 | defaultFormSize int64 = 1024 * 1024 * 1 16 | ) 17 | 18 | type ( 19 | // MapData represents basic data structure for govalidator Rules and Messages 20 | MapData map[string][]string 21 | 22 | // Options describes configuration option for validator 23 | Options struct { 24 | Data interface{} // Data represents structure for JSON body 25 | Request *http.Request 26 | RequiredDefault bool // RequiredDefault represents if all the fields are by default required or not 27 | Rules MapData // Rules represents rules for form-data/x-url-encoded/query params data 28 | Messages MapData // Messages represents custom/localize message for rules 29 | TagIdentifier string // TagIdentifier represents struct tag identifier, e.g: json or validate etc 30 | FormSize int64 //Form represents the multipart forom data max memory size in bytes 31 | } 32 | 33 | // Validator represents a validator with options 34 | Validator struct { 35 | Opts Options // Opts contains all the options for validator 36 | } 37 | ) 38 | 39 | // New return a new validator object using provided options 40 | func New(opts Options) *Validator { 41 | return &Validator{Opts: opts} 42 | } 43 | 44 | // getMessage return if a custom message exist against the field name and rule 45 | // if not available it return an empty string 46 | func (v *Validator) getCustomMessage(field, rule string) string { 47 | if msgList, ok := v.Opts.Messages[field]; ok { 48 | for _, m := range msgList { 49 | //if rules has params, remove params. e.g: between:3,5 would be between 50 | if strings.Contains(rule, ":") { 51 | rule = strings.Split(rule, ":")[0] 52 | } 53 | if strings.HasPrefix(m, rule+":") { 54 | return strings.TrimPrefix(m, rule+":") 55 | } 56 | } 57 | } 58 | return "" 59 | } 60 | 61 | // SetDefaultRequired change the required behavior of fields 62 | // Default value if false 63 | // If SetDefaultRequired set to true then it will mark all the field in the rules list as required 64 | func (v *Validator) SetDefaultRequired(required bool) { 65 | v.Opts.RequiredDefault = required 66 | } 67 | 68 | // SetTagIdentifier change the default tag identifier (json) to your custom tag. 69 | func (v *Validator) SetTagIdentifier(identifier string) { 70 | v.Opts.TagIdentifier = identifier 71 | } 72 | 73 | // Validate validate request data like form-data, x-www-form-urlencoded and query params 74 | // see example in README.md file 75 | // ref: https://github.com/thedevsaddam/govalidator#example 76 | func (v *Validator) Validate() url.Values { 77 | // if request object and rules not passed rise a panic 78 | if len(v.Opts.Rules) == 0 || v.Opts.Request == nil { 79 | panic(errValidateArgsMismatch) 80 | } 81 | errsBag := url.Values{} 82 | 83 | // get non required rules 84 | nr := v.getNonRequiredFields() 85 | 86 | for field, rules := range v.Opts.Rules { 87 | if _, ok := nr[field]; ok { 88 | continue 89 | } 90 | for _, rule := range rules { 91 | if !isRuleExist(rule) { 92 | panic(fmt.Errorf("govalidator: %s is not a valid rule", rule)) 93 | } 94 | msg := v.getCustomMessage(field, rule) 95 | // validate file 96 | if strings.HasPrefix(field, "file:") { 97 | fld := strings.TrimPrefix(field, "file:") 98 | file, fh, _ := v.Opts.Request.FormFile(fld) 99 | if file != nil && fh.Filename != "" { 100 | validateFiles(v.Opts.Request, fld, rule, msg, errsBag) 101 | validateCustomRules(fld, rule, msg, file, errsBag) 102 | } else { 103 | validateCustomRules(fld, rule, msg, nil, errsBag) 104 | } 105 | } else { 106 | // validate if custom rules exist 107 | reqVal := strings.TrimSpace(v.Opts.Request.Form.Get(field)) 108 | validateCustomRules(field, rule, msg, reqVal, errsBag) 109 | } 110 | } 111 | } 112 | 113 | return errsBag 114 | } 115 | 116 | // getNonRequiredFields remove non required rules fields from rules if requiredDefault field is false 117 | // and if the input data is empty for this field 118 | func (v *Validator) getNonRequiredFields() map[string]struct{} { 119 | if v.Opts.FormSize > 0 { 120 | _ = v.Opts.Request.ParseMultipartForm(v.Opts.FormSize) 121 | } else { 122 | _ = v.Opts.Request.ParseMultipartForm(defaultFormSize) 123 | } 124 | 125 | inputs := v.Opts.Request.Form 126 | nr := make(map[string]struct{}) 127 | if !v.Opts.RequiredDefault { 128 | for k, r := range v.Opts.Rules { 129 | isFile := strings.HasPrefix(k, "file:") 130 | if _, ok := inputs[k]; !ok && !isFile { 131 | if !isContainRequiredField(r) { 132 | nr[k] = struct{}{} 133 | } 134 | } 135 | } 136 | } 137 | return nr 138 | } 139 | 140 | // ValidateJSON validate request data from JSON body to Go struct 141 | // see example in README.md file 142 | func (v *Validator) ValidateJSON() url.Values { 143 | if len(v.Opts.Rules) == 0 || v.Opts.Request == nil { 144 | panic(errValidateArgsMismatch) 145 | } 146 | if reflect.TypeOf(v.Opts.Data).Kind() != reflect.Ptr { 147 | panic(errRequirePtr) 148 | } 149 | 150 | return v.internalValidateStruct() 151 | } 152 | 153 | func (v *Validator) ValidateStruct() url.Values { 154 | if len(v.Opts.Rules) == 0 { 155 | panic(errRequireRules) 156 | } 157 | if v.Opts.Request != nil { 158 | panic(errRequestNotAccepted) 159 | } 160 | if v.Opts.Data != nil && reflect.TypeOf(v.Opts.Data).Kind() != reflect.Ptr { 161 | panic(errRequirePtr) 162 | } 163 | if v.Opts.Data == nil { 164 | panic(errRequireData) 165 | } 166 | 167 | return v.internalValidateStruct() 168 | } 169 | 170 | func (v *Validator) internalValidateStruct() url.Values { 171 | errsBag := url.Values{} 172 | 173 | if v.Opts.Request != nil && v.Opts.Request.Body != http.NoBody { 174 | defer v.Opts.Request.Body.Close() 175 | err := json.NewDecoder(v.Opts.Request.Body).Decode(v.Opts.Data) 176 | if err != nil { 177 | errsBag.Add("_error", err.Error()) 178 | return errsBag 179 | } 180 | } 181 | 182 | r := roller{} 183 | r.setTagIdentifier(tagIdentifier) 184 | if v.Opts.TagIdentifier != "" { 185 | r.setTagIdentifier(v.Opts.TagIdentifier) 186 | } 187 | r.setTagSeparator(tagSeparator) 188 | r.start(v.Opts.Data) 189 | 190 | //clean if the key is not exist or value is empty or zero value 191 | nr := v.getNonRequiredJSONFields(r.getFlatMap()) 192 | 193 | for field, rules := range v.Opts.Rules { 194 | if _, ok := nr[field]; ok { 195 | continue 196 | } 197 | value, _ := r.getFlatVal(field) 198 | for _, rule := range rules { 199 | if !isRuleExist(rule) { 200 | panic(fmt.Errorf("govalidator: %s is not a valid rule", rule)) 201 | } 202 | msg := v.getCustomMessage(field, rule) 203 | validateCustomRules(field, rule, msg, value, errsBag) 204 | } 205 | } 206 | 207 | return errsBag 208 | } 209 | 210 | // getNonRequiredJSONFields get non required rules fields from rules if requiredDefault field is false 211 | // and if the input data is empty for this field 212 | func (v *Validator) getNonRequiredJSONFields(inputs map[string]interface{}) map[string]struct{} { 213 | nr := make(map[string]struct{}) 214 | if !v.Opts.RequiredDefault { 215 | for k, r := range v.Opts.Rules { 216 | if val := inputs[k]; isEmpty(val) { 217 | if !isContainRequiredField(r) { 218 | nr[k] = struct{}{} 219 | } 220 | } 221 | } 222 | } 223 | return nr 224 | } 225 | -------------------------------------------------------------------------------- /validator_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | ) 10 | 11 | func TestValidator_SetDefaultRequired(t *testing.T) { 12 | v := New(Options{}) 13 | v.SetDefaultRequired(true) 14 | if !v.Opts.RequiredDefault { 15 | t.Error("SetDefaultRequired failed") 16 | } 17 | } 18 | 19 | func TestValidator_Validate(t *testing.T) { 20 | var URL *url.URL 21 | URL, _ = url.Parse("http://www.example.com") 22 | params := url.Values{} 23 | params.Add("name", "John Doe") 24 | params.Add("username", "jhondoe") 25 | params.Add("email", "john@mail.com") 26 | params.Add("zip", "8233") 27 | URL.RawQuery = params.Encode() 28 | r, _ := http.NewRequest("GET", URL.String(), nil) 29 | rulesList := MapData{ 30 | "name": []string{"required"}, 31 | "age": []string{"between:5,16"}, 32 | "email": []string{"email"}, 33 | "zip": []string{"digits:4"}, 34 | } 35 | 36 | opts := Options{ 37 | Request: r, 38 | Rules: rulesList, 39 | } 40 | v := New(opts) 41 | validationError := v.Validate() 42 | if len(validationError) > 0 { 43 | t.Log(validationError) 44 | t.Error("Validate failed to validate correct inputs!") 45 | } 46 | 47 | defer func() { 48 | if r := recover(); r == nil { 49 | t.Errorf("Validate did not panic") 50 | } 51 | }() 52 | 53 | v1 := New(Options{Rules: MapData{}}) 54 | v1.Validate() 55 | } 56 | 57 | func Benchmark_Validate(b *testing.B) { 58 | var URL *url.URL 59 | URL, _ = url.Parse("http://www.example.com") 60 | params := url.Values{} 61 | params.Add("name", "John Doe") 62 | params.Add("age", "27") 63 | params.Add("email", "john@mail.com") 64 | params.Add("zip", "8233") 65 | URL.RawQuery = params.Encode() 66 | r, _ := http.NewRequest("GET", URL.String(), nil) 67 | rulesList := MapData{ 68 | "name": []string{"required"}, 69 | "age": []string{"numeric_between:18,60"}, 70 | "email": []string{"email"}, 71 | "zip": []string{"digits:4"}, 72 | } 73 | 74 | opts := Options{ 75 | Request: r, 76 | Rules: rulesList, 77 | } 78 | v := New(opts) 79 | for n := 0; n < b.N; n++ { 80 | v.Validate() 81 | } 82 | } 83 | 84 | //============ validate json test ==================== 85 | 86 | func TestValidator_ValidateJSON(t *testing.T) { 87 | type User struct { 88 | Name string `json:"name"` 89 | Email string `json:"email"` 90 | Address string `json:"address"` 91 | Age int `json:"age"` 92 | Zip string `json:"zip"` 93 | Color int `json:"color"` 94 | } 95 | 96 | postUser := User{ 97 | Name: "", 98 | Email: "inalid email", 99 | Address: "", 100 | Age: 1, 101 | Zip: "122", 102 | Color: 5, 103 | } 104 | 105 | rules := MapData{ 106 | "name": []string{"required"}, 107 | "email": []string{"email"}, 108 | "address": []string{"required", "between:3,5"}, 109 | "age": []string{"bool"}, 110 | "zip": []string{"len:4"}, 111 | "color": []string{"min:10"}, 112 | } 113 | 114 | var user User 115 | 116 | body, _ := json.Marshal(postUser) 117 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 118 | 119 | opts := Options{ 120 | Request: req, 121 | Data: &user, 122 | Rules: rules, 123 | } 124 | 125 | vd := New(opts) 126 | vd.SetTagIdentifier("json") 127 | validationErr := vd.ValidateJSON() 128 | if len(validationErr) != 5 { 129 | t.Error("ValidateJSON failed") 130 | } 131 | } 132 | 133 | func TestValidator_ValidateJSON_NULLValue(t *testing.T) { 134 | type User struct { 135 | Name string `json:"name"` 136 | Count Int `json:"count"` 137 | Option Int `json:"option"` 138 | Active Bool `json:"active"` 139 | } 140 | 141 | rules := MapData{ 142 | "name": []string{"required"}, 143 | "count": []string{"required"}, 144 | "option": []string{"required"}, 145 | "active": []string{"required"}, 146 | } 147 | 148 | postUser := map[string]interface{}{ 149 | "name": "John Doe", 150 | "count": 0, 151 | "option": nil, 152 | "active": nil, 153 | } 154 | 155 | var user User 156 | body, _ := json.Marshal(postUser) 157 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(body)) 158 | 159 | opts := Options{ 160 | Request: req, 161 | Data: &user, 162 | Rules: rules, 163 | } 164 | 165 | vd := New(opts) 166 | vd.SetTagIdentifier("json") 167 | validationErr := vd.ValidateJSON() 168 | if len(validationErr) != 2 { 169 | t.Error("ValidateJSON failed") 170 | } 171 | } 172 | 173 | func TestValidator_ValidateJSON_NoBody(t *testing.T) { 174 | type User struct { 175 | Name string `json:"name"` 176 | Count Int `json:"count"` 177 | Option Int `json:"option"` 178 | Active Bool `json:"active"` 179 | } 180 | 181 | rules := MapData{ 182 | "name": []string{"required"}, 183 | "count": []string{"required"}, 184 | "option": []string{"required"}, 185 | "active": []string{"required"}, 186 | } 187 | 188 | var user User 189 | req, _ := http.NewRequest("POST", "http://www.example.com", bytes.NewReader(nil)) 190 | 191 | opts := Options{ 192 | Request: req, 193 | Data: &user, 194 | Rules: rules, 195 | } 196 | 197 | vd := New(opts) 198 | vd.SetTagIdentifier("json") 199 | validationErr := vd.ValidateJSON() 200 | if len(validationErr) != len(rules) { 201 | t.Error("ValidateJSON with empty request body failed") 202 | } 203 | } 204 | 205 | func TestValidator_ValidateStruct(t *testing.T) { 206 | type User struct { 207 | Name string `json:"name"` 208 | Email string `json:"email"` 209 | Address string `json:"address"` 210 | Age int `json:"age"` 211 | Zip string `json:"zip"` 212 | Color int `json:"color"` 213 | } 214 | 215 | postUser := User{ 216 | Name: "", 217 | Email: "inalid email", 218 | Address: "", 219 | Age: 1, 220 | Zip: "122", 221 | Color: 5, 222 | } 223 | 224 | rules := MapData{ 225 | "name": []string{"required"}, 226 | "email": []string{"email"}, 227 | "address": []string{"required", "between:3,5"}, 228 | "age": []string{"bool"}, 229 | "zip": []string{"len:4"}, 230 | "color": []string{"min:10"}, 231 | } 232 | 233 | opts := Options{ 234 | Data: &postUser, 235 | Rules: rules, 236 | } 237 | 238 | vd := New(opts) 239 | vd.SetTagIdentifier("json") 240 | validationErr := vd.ValidateStruct() 241 | if len(validationErr) != 5 { 242 | t.Error("ValidateStruct failed") 243 | } 244 | } 245 | 246 | func TestValidator_ValidateJSON_NoRules_panic(t *testing.T) { 247 | opts := Options{} 248 | 249 | assertPanicWith(t, errValidateArgsMismatch, func() { 250 | New(opts).ValidateJSON() 251 | }) 252 | } 253 | 254 | func TestValidator_ValidateJSON_NonPointer_panic(t *testing.T) { 255 | req, _ := http.NewRequest("POST", "/", nil) 256 | 257 | type User struct { 258 | } 259 | 260 | var user User 261 | opts := Options{ 262 | Request: req, 263 | Data: user, 264 | Rules: MapData{ 265 | "name": []string{"required"}, 266 | }, 267 | } 268 | 269 | assertPanicWith(t, errRequirePtr, func() { 270 | New(opts).ValidateJSON() 271 | }) 272 | } 273 | 274 | func TestValidator_ValidateStruct_NoRules_panic(t *testing.T) { 275 | opts := Options{} 276 | 277 | assertPanicWith(t, errRequireRules, func() { 278 | New(opts).ValidateStruct() 279 | }) 280 | } 281 | 282 | func TestValidator_ValidateStruct_RequestProvided_panic(t *testing.T) { 283 | req, _ := http.NewRequest("POST", "/", nil) 284 | opts := Options{ 285 | Request: req, 286 | Rules: MapData{ 287 | "name": []string{"required"}, 288 | }, 289 | } 290 | 291 | assertPanicWith(t, errRequestNotAccepted, func() { 292 | New(opts).ValidateStruct() 293 | }) 294 | } 295 | 296 | func TestValidator_ValidateStruct_NonPointer_panic(t *testing.T) { 297 | type User struct { 298 | } 299 | 300 | var user User 301 | opts := Options{ 302 | Data: user, 303 | Rules: MapData{ 304 | "name": []string{"required"}, 305 | }, 306 | } 307 | 308 | assertPanicWith(t, errRequirePtr, func() { 309 | New(opts).ValidateStruct() 310 | }) 311 | } 312 | 313 | func TestValidator_ValidateStruct_DataNil_panic(t *testing.T) { 314 | opts := Options{ 315 | Rules: MapData{ 316 | "name": []string{"required"}, 317 | }, 318 | } 319 | 320 | assertPanicWith(t, errRequireData, func() { 321 | New(opts).ValidateStruct() 322 | }) 323 | } 324 | --------------------------------------------------------------------------------