add map key validation support (#324)
Dean Karn authored 6 years ago
GitHub committed 6 years ago
0 | 0 | Package validator |
1 | 1 | ================ |
2 | 2 | <img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) |
3 | ![Project status](https://img.shields.io/badge/version-9.8.0-green.svg) | |
3 | ![Project status](https://img.shields.io/badge/version-9.9.0-green.svg) | |
4 | 4 | [![Build Status](https://semaphoreci.com/api/v1/joeybloggs/validator/branches/v9/badge.svg)](https://semaphoreci.com/joeybloggs/validator) |
5 | 5 | [![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=v9&service=github)](https://coveralls.io/github/go-playground/validator?branch=v9) |
6 | 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator) |
12 | 12 | It has the following **unique** features: |
13 | 13 | |
14 | 14 | - Cross Field and Cross Struct validations by using validation tags or custom validators. |
15 | - Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated. | |
15 | - Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated. | |
16 | - Ability to dive into both map keys and values for validation | |
16 | 17 | - Handles type interface by determining it's underlying type prior to validation. |
17 | 18 | - Handles custom field types such as sql driver Valuer see [Valuer](https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29) |
18 | 19 | - Alias validation tags, which allows for mapping of several validations to a single tag for easier defining of validations on structs |
64 | 65 | |
65 | 66 | Benchmarks |
66 | 67 | ------ |
67 | ###### Run on MacBook Pro (15-inch, 2017) Go version go1.9.1 linux/amd64 | |
68 | ###### Run on MacBook Pro (15-inch, 2017) Go version go1.9.2 darwin/amd64 | |
68 | 69 | ```go |
69 | 70 | go test -bench=. -benchmem=true |
70 | BenchmarkFieldSuccess-8 20000000 87.2 ns/op 0 B/op 0 allocs/op | |
71 | BenchmarkFieldSuccessParallel-8 50000000 26.1 ns/op 0 B/op 0 allocs/op | |
72 | BenchmarkFieldFailure-8 5000000 299 ns/op 208 B/op 4 allocs/op | |
73 | BenchmarkFieldFailureParallel-8 20000000 100 ns/op 208 B/op 4 allocs/op | |
74 | BenchmarkFieldDiveSuccess-8 2000000 645 ns/op 201 B/op 11 allocs/op | |
75 | BenchmarkFieldDiveSuccessParallel-8 10000000 198 ns/op 201 B/op 11 allocs/op | |
76 | BenchmarkFieldDiveFailure-8 2000000 876 ns/op 412 B/op 16 allocs/op | |
77 | BenchmarkFieldDiveFailureParallel-8 5000000 268 ns/op 413 B/op 16 allocs/op | |
78 | BenchmarkFieldCustomTypeSuccess-8 10000000 228 ns/op 32 B/op 2 allocs/op | |
79 | BenchmarkFieldCustomTypeSuccessParallel-8 20000000 70.0 ns/op 32 B/op 2 allocs/op | |
80 | BenchmarkFieldCustomTypeFailure-8 5000000 286 ns/op 208 B/op 4 allocs/op | |
81 | BenchmarkFieldCustomTypeFailureParallel-8 20000000 95.6 ns/op 208 B/op 4 allocs/op | |
82 | BenchmarkFieldOrTagSuccess-8 2000000 857 ns/op 16 B/op 1 allocs/op | |
83 | BenchmarkFieldOrTagSuccessParallel-8 3000000 397 ns/op 16 B/op 1 allocs/op | |
84 | BenchmarkFieldOrTagFailure-8 3000000 495 ns/op 224 B/op 5 allocs/op | |
85 | BenchmarkFieldOrTagFailureParallel-8 5000000 376 ns/op 224 B/op 5 allocs/op | |
86 | BenchmarkStructLevelValidationSuccess-8 10000000 226 ns/op 32 B/op 2 allocs/op | |
87 | BenchmarkStructLevelValidationSuccessParallel-8 20000000 68.4 ns/op 32 B/op 2 allocs/op | |
88 | BenchmarkStructLevelValidationFailure-8 3000000 497 ns/op 304 B/op 8 allocs/op | |
89 | BenchmarkStructLevelValidationFailureParallel-8 10000000 170 ns/op 304 B/op 8 allocs/op | |
90 | BenchmarkStructSimpleCustomTypeSuccess-8 3000000 420 ns/op 32 B/op 2 allocs/op | |
91 | BenchmarkStructSimpleCustomTypeSuccessParallel-8 20000000 124 ns/op 32 B/op 2 allocs/op | |
92 | BenchmarkStructSimpleCustomTypeFailure-8 2000000 681 ns/op 424 B/op 9 allocs/op | |
93 | BenchmarkStructSimpleCustomTypeFailureParallel-8 10000000 244 ns/op 440 B/op 10 allocs/op | |
94 | BenchmarkStructFilteredSuccess-8 2000000 659 ns/op 288 B/op 9 allocs/op | |
95 | BenchmarkStructFilteredSuccessParallel-8 10000000 211 ns/op 288 B/op 9 allocs/op | |
96 | BenchmarkStructFilteredFailure-8 3000000 482 ns/op 256 B/op 7 allocs/op | |
97 | BenchmarkStructFilteredFailureParallel-8 10000000 162 ns/op 256 B/op 7 allocs/op | |
98 | BenchmarkStructPartialSuccess-8 3000000 564 ns/op 256 B/op 6 allocs/op | |
99 | BenchmarkStructPartialSuccessParallel-8 10000000 180 ns/op 256 B/op 6 allocs/op | |
100 | BenchmarkStructPartialFailure-8 2000000 779 ns/op 480 B/op 11 allocs/op | |
101 | BenchmarkStructPartialFailureParallel-8 5000000 268 ns/op 480 B/op 11 allocs/op | |
102 | BenchmarkStructExceptSuccess-8 2000000 879 ns/op 496 B/op 12 allocs/op | |
71 | BenchmarkFieldSuccess-8 20000000 79.9 ns/op 0 B/op 0 allocs/op | |
72 | BenchmarkFieldSuccessParallel-8 50000000 25.0 ns/op 0 B/op 0 allocs/op | |
73 | BenchmarkFieldFailure-8 5000000 281 ns/op 208 B/op 4 allocs/op | |
74 | BenchmarkFieldFailureParallel-8 20000000 97.0 ns/op 208 B/op 4 allocs/op | |
75 | BenchmarkFieldArrayDiveSuccess-8 3000000 591 ns/op 201 B/op 11 allocs/op | |
76 | BenchmarkFieldArrayDiveSuccessParallel-8 10000000 195 ns/op 201 B/op 11 allocs/op | |
77 | BenchmarkFieldArrayDiveFailure-8 2000000 878 ns/op 412 B/op 16 allocs/op | |
78 | BenchmarkFieldArrayDiveFailureParallel-8 5000000 274 ns/op 413 B/op 16 allocs/op | |
79 | BenchmarkFieldMapDiveSuccess-8 1000000 1279 ns/op 432 B/op 18 allocs/op | |
80 | BenchmarkFieldMapDiveSuccessParallel-8 5000000 401 ns/op 432 B/op 18 allocs/op | |
81 | BenchmarkFieldMapDiveFailure-8 1000000 1060 ns/op 512 B/op 16 allocs/op | |
82 | BenchmarkFieldMapDiveFailureParallel-8 5000000 334 ns/op 512 B/op 16 allocs/op | |
83 | BenchmarkFieldMapDiveWithKeysSuccess-8 1000000 1462 ns/op 480 B/op 21 allocs/op | |
84 | BenchmarkFieldMapDiveWithKeysSuccessParallel-8 3000000 463 ns/op 480 B/op 21 allocs/op | |
85 | BenchmarkFieldMapDiveWithKeysFailure-8 1000000 1414 ns/op 721 B/op 21 allocs/op | |
86 | BenchmarkFieldMapDiveWithKeysFailureParallel-8 3000000 446 ns/op 721 B/op 21 allocs/op | |
87 | BenchmarkFieldCustomTypeSuccess-8 10000000 211 ns/op 32 B/op 2 allocs/op | |
88 | BenchmarkFieldCustomTypeSuccessParallel-8 20000000 65.9 ns/op 32 B/op 2 allocs/op | |
89 | BenchmarkFieldCustomTypeFailure-8 5000000 270 ns/op 208 B/op 4 allocs/op | |
90 | BenchmarkFieldCustomTypeFailureParallel-8 20000000 93.3 ns/op 208 B/op 4 allocs/op | |
91 | BenchmarkFieldOrTagSuccess-8 2000000 729 ns/op 16 B/op 1 allocs/op | |
92 | BenchmarkFieldOrTagSuccessParallel-8 5000000 367 ns/op 16 B/op 1 allocs/op | |
93 | BenchmarkFieldOrTagFailure-8 3000000 472 ns/op 224 B/op 5 allocs/op | |
94 | BenchmarkFieldOrTagFailureParallel-8 5000000 373 ns/op 224 B/op 5 allocs/op | |
95 | BenchmarkStructLevelValidationSuccess-8 10000000 201 ns/op 32 B/op 2 allocs/op | |
96 | BenchmarkStructLevelValidationSuccessParallel-8 20000000 66.3 ns/op 32 B/op 2 allocs/op | |
97 | BenchmarkStructLevelValidationFailure-8 3000000 468 ns/op 304 B/op 8 allocs/op | |
98 | BenchmarkStructLevelValidationFailureParallel-8 10000000 172 ns/op 304 B/op 8 allocs/op | |
99 | BenchmarkStructSimpleCustomTypeSuccess-8 5000000 376 ns/op 32 B/op 2 allocs/op | |
100 | BenchmarkStructSimpleCustomTypeSuccessParallel-8 20000000 126 ns/op 32 B/op 2 allocs/op | |
101 | BenchmarkStructSimpleCustomTypeFailure-8 2000000 646 ns/op 424 B/op 9 allocs/op | |
102 | BenchmarkStructSimpleCustomTypeFailureParallel-8 10000000 240 ns/op 440 B/op 10 allocs/op | |
103 | BenchmarkStructFilteredSuccess-8 3000000 582 ns/op 288 B/op 9 allocs/op | |
104 | BenchmarkStructFilteredSuccessParallel-8 10000000 198 ns/op 288 B/op 9 allocs/op | |
105 | BenchmarkStructFilteredFailure-8 3000000 447 ns/op 256 B/op 7 allocs/op | |
106 | BenchmarkStructFilteredFailureParallel-8 10000000 156 ns/op 256 B/op 7 allocs/op | |
107 | BenchmarkStructPartialSuccess-8 3000000 536 ns/op 256 B/op 6 allocs/op | |
108 | BenchmarkStructPartialSuccessParallel-8 10000000 175 ns/op 256 B/op 6 allocs/op | |
109 | BenchmarkStructPartialFailure-8 2000000 738 ns/op 480 B/op 11 allocs/op | |
110 | BenchmarkStructPartialFailureParallel-8 5000000 256 ns/op 480 B/op 11 allocs/op | |
111 | BenchmarkStructExceptSuccess-8 2000000 835 ns/op 496 B/op 12 allocs/op | |
103 | 112 | BenchmarkStructExceptSuccessParallel-8 10000000 163 ns/op 240 B/op 5 allocs/op |
104 | BenchmarkStructExceptFailure-8 2000000 734 ns/op 464 B/op 10 allocs/op | |
105 | BenchmarkStructExceptFailureParallel-8 5000000 259 ns/op 464 B/op 10 allocs/op | |
106 | BenchmarkStructSimpleCrossFieldSuccess-8 3000000 432 ns/op 72 B/op 3 allocs/op | |
107 | BenchmarkStructSimpleCrossFieldSuccessParallel-8 10000000 129 ns/op 72 B/op 3 allocs/op | |
108 | BenchmarkStructSimpleCrossFieldFailure-8 2000000 671 ns/op 304 B/op 8 allocs/op | |
109 | BenchmarkStructSimpleCrossFieldFailureParallel-8 10000000 229 ns/op 304 B/op 8 allocs/op | |
110 | BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 2000000 628 ns/op 80 B/op 4 allocs/op | |
111 | BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 10000000 182 ns/op 80 B/op 4 allocs/op | |
112 | BenchmarkStructSimpleCrossStructCrossFieldFailure-8 2000000 872 ns/op 320 B/op 9 allocs/op | |
113 | BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 5000000 267 ns/op 320 B/op 9 allocs/op | |
114 | BenchmarkStructSimpleSuccess-8 5000000 274 ns/op 0 B/op 0 allocs/op | |
115 | BenchmarkStructSimpleSuccessParallel-8 20000000 79.0 ns/op 0 B/op 0 allocs/op | |
116 | BenchmarkStructSimpleFailure-8 2000000 647 ns/op 424 B/op 9 allocs/op | |
117 | BenchmarkStructSimpleFailureParallel-8 10000000 224 ns/op 424 B/op 9 allocs/op | |
118 | BenchmarkStructComplexSuccess-8 1000000 1557 ns/op 128 B/op 8 allocs/op | |
119 | BenchmarkStructComplexSuccessParallel-8 3000000 473 ns/op 128 B/op 8 allocs/op | |
120 | BenchmarkStructComplexFailure-8 300000 4373 ns/op 3041 B/op 53 allocs/op | |
121 | BenchmarkStructComplexFailureParallel-8 1000000 1554 ns/op 3041 B/op 53 allocs/op | |
113 | BenchmarkStructExceptFailure-8 2000000 682 ns/op 464 B/op 10 allocs/op | |
114 | BenchmarkStructExceptFailureParallel-8 10000000 244 ns/op 464 B/op 10 allocs/op | |
115 | BenchmarkStructSimpleCrossFieldSuccess-8 5000000 392 ns/op 72 B/op 3 allocs/op | |
116 | BenchmarkStructSimpleCrossFieldSuccessParallel-8 20000000 126 ns/op 72 B/op 3 allocs/op | |
117 | BenchmarkStructSimpleCrossFieldFailure-8 2000000 611 ns/op 304 B/op 8 allocs/op | |
118 | BenchmarkStructSimpleCrossFieldFailureParallel-8 10000000 214 ns/op 304 B/op 8 allocs/op | |
119 | BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 3000000 567 ns/op 80 B/op 4 allocs/op | |
120 | BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 10000000 177 ns/op 80 B/op 4 allocs/op | |
121 | BenchmarkStructSimpleCrossStructCrossFieldFailure-8 2000000 807 ns/op 320 B/op 9 allocs/op | |
122 | BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 5000000 268 ns/op 320 B/op 9 allocs/op | |
123 | BenchmarkStructSimpleSuccess-8 5000000 256 ns/op 0 B/op 0 allocs/op | |
124 | BenchmarkStructSimpleSuccessParallel-8 20000000 76.3 ns/op 0 B/op 0 allocs/op | |
125 | BenchmarkStructSimpleFailure-8 2000000 625 ns/op 424 B/op 9 allocs/op | |
126 | BenchmarkStructSimpleFailureParallel-8 10000000 219 ns/op 424 B/op 9 allocs/op | |
127 | BenchmarkStructComplexSuccess-8 1000000 1431 ns/op 128 B/op 8 allocs/op | |
128 | BenchmarkStructComplexSuccessParallel-8 3000000 427 ns/op 128 B/op 8 allocs/op | |
129 | BenchmarkStructComplexFailure-8 300000 4065 ns/op 3041 B/op 53 allocs/op | |
130 | BenchmarkStructComplexFailureParallel-8 1000000 1478 ns/op 3041 B/op 53 allocs/op | |
122 | 131 | ``` |
123 | 132 | |
124 | 133 | Complementary Software |
0 | package main | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | ||
5 | "gopkg.in/go-playground/validator.v9" | |
6 | ) | |
7 | ||
8 | // Test ... | |
9 | type Test struct { | |
10 | Array []string `validate:"required,gt=0,dive,required"` | |
11 | Map map[string]string `validate:"required,gt=0,dive,keys,keymax,endkeys,required,max=1000"` | |
12 | } | |
13 | ||
14 | // use a single instance of Validate, it caches struct info | |
15 | var validate *validator.Validate | |
16 | ||
17 | func main() { | |
18 | ||
19 | validate = validator.New() | |
20 | ||
21 | // registering alias so we can see the differences between | |
22 | // map key, value validation errors | |
23 | validate.RegisterAlias("keymax", "max=10") | |
24 | ||
25 | var test Test | |
26 | ||
27 | val(test) | |
28 | ||
29 | test.Array = []string{""} | |
30 | test.Map = map[string]string{"test > than 10": ""} | |
31 | val(test) | |
32 | } | |
33 | ||
34 | func val(test Test) { | |
35 | fmt.Println("testing") | |
36 | err := validate.Struct(test) | |
37 | fmt.Println(err) | |
38 | } |
31 | 31 | var ( |
32 | 32 | restrictedTags = map[string]struct{}{ |
33 | 33 | diveTag: {}, |
34 | keysTag: {}, | |
35 | endKeysTag: {}, | |
34 | 36 | structOnlyTag: {}, |
35 | 37 | omitempty: {}, |
36 | 38 | skipValidationTag: {}, |
58 | 58 | }) |
59 | 59 | } |
60 | 60 | |
61 | func BenchmarkFieldDiveSuccess(b *testing.B) { | |
61 | func BenchmarkFieldArrayDiveSuccess(b *testing.B) { | |
62 | 62 | |
63 | 63 | validate := New() |
64 | 64 | |
71 | 71 | } |
72 | 72 | } |
73 | 73 | |
74 | func BenchmarkFieldDiveSuccessParallel(b *testing.B) { | |
74 | func BenchmarkFieldArrayDiveSuccessParallel(b *testing.B) { | |
75 | 75 | |
76 | 76 | validate := New() |
77 | 77 | |
85 | 85 | }) |
86 | 86 | } |
87 | 87 | |
88 | func BenchmarkFieldDiveFailure(b *testing.B) { | |
88 | func BenchmarkFieldArrayDiveFailure(b *testing.B) { | |
89 | 89 | |
90 | 90 | validate := New() |
91 | 91 | |
97 | 97 | } |
98 | 98 | } |
99 | 99 | |
100 | func BenchmarkFieldDiveFailureParallel(b *testing.B) { | |
100 | func BenchmarkFieldArrayDiveFailureParallel(b *testing.B) { | |
101 | 101 | |
102 | 102 | validate := New() |
103 | 103 | |
107 | 107 | b.RunParallel(func(pb *testing.PB) { |
108 | 108 | for pb.Next() { |
109 | 109 | validate.Var(m, "required,dive,required") |
110 | } | |
111 | }) | |
112 | } | |
113 | ||
114 | func BenchmarkFieldMapDiveSuccess(b *testing.B) { | |
115 | ||
116 | validate := New() | |
117 | ||
118 | m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"} | |
119 | ||
120 | b.ResetTimer() | |
121 | ||
122 | for n := 0; n < b.N; n++ { | |
123 | validate.Var(m, "required,dive,required") | |
124 | } | |
125 | } | |
126 | ||
127 | func BenchmarkFieldMapDiveSuccessParallel(b *testing.B) { | |
128 | ||
129 | validate := New() | |
130 | ||
131 | m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"} | |
132 | ||
133 | b.ResetTimer() | |
134 | b.RunParallel(func(pb *testing.PB) { | |
135 | for pb.Next() { | |
136 | validate.Var(m, "required,dive,required") | |
137 | } | |
138 | }) | |
139 | } | |
140 | ||
141 | func BenchmarkFieldMapDiveFailure(b *testing.B) { | |
142 | ||
143 | validate := New() | |
144 | ||
145 | m := map[string]string{"": "", "val3": "val3"} | |
146 | ||
147 | b.ResetTimer() | |
148 | for n := 0; n < b.N; n++ { | |
149 | validate.Var(m, "required,dive,required") | |
150 | } | |
151 | } | |
152 | ||
153 | func BenchmarkFieldMapDiveFailureParallel(b *testing.B) { | |
154 | ||
155 | validate := New() | |
156 | ||
157 | m := map[string]string{"": "", "val3": "val3"} | |
158 | ||
159 | b.ResetTimer() | |
160 | b.RunParallel(func(pb *testing.PB) { | |
161 | for pb.Next() { | |
162 | validate.Var(m, "required,dive,required") | |
163 | } | |
164 | }) | |
165 | } | |
166 | ||
167 | func BenchmarkFieldMapDiveWithKeysSuccess(b *testing.B) { | |
168 | ||
169 | validate := New() | |
170 | ||
171 | m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"} | |
172 | ||
173 | b.ResetTimer() | |
174 | ||
175 | for n := 0; n < b.N; n++ { | |
176 | validate.Var(m, "required,dive,keys,required,endkeys,required") | |
177 | } | |
178 | } | |
179 | ||
180 | func BenchmarkFieldMapDiveWithKeysSuccessParallel(b *testing.B) { | |
181 | ||
182 | validate := New() | |
183 | ||
184 | m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"} | |
185 | ||
186 | b.ResetTimer() | |
187 | b.RunParallel(func(pb *testing.PB) { | |
188 | for pb.Next() { | |
189 | validate.Var(m, "required,dive,keys,required,endkeys,required") | |
190 | } | |
191 | }) | |
192 | } | |
193 | ||
194 | func BenchmarkFieldMapDiveWithKeysFailure(b *testing.B) { | |
195 | ||
196 | validate := New() | |
197 | ||
198 | m := map[string]string{"": "", "val3": "val3"} | |
199 | ||
200 | b.ResetTimer() | |
201 | for n := 0; n < b.N; n++ { | |
202 | validate.Var(m, "required,dive,keys,required,endkeys,required") | |
203 | } | |
204 | } | |
205 | ||
206 | func BenchmarkFieldMapDiveWithKeysFailureParallel(b *testing.B) { | |
207 | ||
208 | validate := New() | |
209 | ||
210 | m := map[string]string{"": "", "val3": "val3"} | |
211 | ||
212 | b.ResetTimer() | |
213 | b.RunParallel(func(pb *testing.PB) { | |
214 | for pb.Next() { | |
215 | validate.Var(m, "required,dive,keys,required,endkeys,required") | |
110 | 216 | } |
111 | 217 | }) |
112 | 218 | } |
17 | 17 | typeStructOnly |
18 | 18 | typeDive |
19 | 19 | typeOr |
20 | typeKeys | |
21 | typeEndKeys | |
20 | 22 | ) |
21 | 23 | |
22 | 24 | const ( |
23 | 25 | invalidValidation = "Invalid validation tag on field '%s'" |
24 | 26 | undefinedValidation = "Undefined validation function '%s' on field '%s'" |
27 | keysTagNotDefined = "'" + endKeysTag + "' tag encountered without a corresponding '" + keysTag + "' tag" | |
25 | 28 | ) |
26 | 29 | |
27 | 30 | type structCache struct { |
87 | 90 | aliasTag string |
88 | 91 | actualAliasTag string |
89 | 92 | param string |
93 | typeof tagType | |
94 | keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation | |
95 | next *cTag | |
96 | hasTag bool | |
90 | 97 | hasAlias bool |
91 | typeof tagType | |
92 | hasTag bool | |
93 | 98 | fn FuncCtx |
94 | next *cTag | |
95 | 99 | } |
96 | 100 | |
97 | 101 | func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct { |
184 | 188 | |
185 | 189 | // check map for alias and process new tags, otherwise process as usual |
186 | 190 | if tagsVal, found := v.aliases[t]; found { |
187 | ||
188 | 191 | if i == 0 { |
189 | 192 | firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true) |
190 | 193 | } else { |
196 | 199 | continue |
197 | 200 | } |
198 | 201 | |
202 | var prevTag tagType | |
203 | ||
199 | 204 | if i == 0 { |
200 | 205 | current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} |
201 | 206 | firstCtag = current |
202 | 207 | } else { |
208 | prevTag = current.typeof | |
203 | 209 | current.next = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} |
204 | 210 | current = current.next |
205 | 211 | } |
209 | 215 | case diveTag: |
210 | 216 | current.typeof = typeDive |
211 | 217 | continue |
218 | ||
219 | case keysTag: | |
220 | current.typeof = typeKeys | |
221 | ||
222 | if i == 0 || prevTag != typeDive { | |
223 | panic(fmt.Sprintf("'%s' tag must be immediately preceeded by the '%s' tag", keysTag, diveTag)) | |
224 | } | |
225 | ||
226 | current.typeof = typeKeys | |
227 | ||
228 | // need to pass along only keys tag | |
229 | // need to increment i to skip over the keys tags | |
230 | b := make([]byte, 0, 64) | |
231 | ||
232 | i++ | |
233 | ||
234 | for ; i < len(tags); i++ { | |
235 | ||
236 | b = append(b, tags[i]...) | |
237 | b = append(b, ',') | |
238 | ||
239 | if tags[i] == endKeysTag { | |
240 | break | |
241 | } | |
242 | } | |
243 | ||
244 | current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false) | |
245 | continue | |
246 | ||
247 | case endKeysTag: | |
248 | current.typeof = typeEndKeys | |
249 | ||
250 | // if there are more in tags then there was no keysTag defined | |
251 | // and an error should be thrown | |
252 | if i != len(tags)-1 { | |
253 | panic(keysTagNotDefined) | |
254 | } | |
255 | return | |
212 | 256 | |
213 | 257 | case omitempty: |
214 | 258 | current.typeof = typeOmitEmpty |
192 | 192 | This tells the validator to dive into a slice, array or map and validate that |
193 | 193 | level of the slice, array or map with the validation tags that follow. |
194 | 194 | Multidimensional nesting is also supported, each level you wish to dive will |
195 | require another dive tag. | |
195 | require another dive tag. dive has some sub-tags, 'keys' & 'endkeys', please see | |
196 | the Keys & EndKeys section just below. | |
196 | 197 | |
197 | 198 | Usage: dive |
198 | 199 | |
209 | 210 | // gt=0 will be applied to [] |
210 | 211 | // []string will be spared validation |
211 | 212 | // required will be applied to string |
213 | ||
214 | Keys & EndKeys | |
215 | ||
216 | These are to be used together directly after the dive tag and tells the validator | |
217 | that anything between 'keys' and 'endkeys' applies to the keys of a map and not the | |
218 | values; think of it like the 'dive' tag, but for map keys instead of values. | |
219 | Multidimensional nesting is also supported, each level you wish to validate will | |
220 | require another 'keys' and 'endkeys' tag. These tags are only valid for maps. | |
221 | ||
222 | Usage: dive,keys,othertagvalidation(s),endkeys,valuevalidationtags | |
223 | ||
224 | Example #1 | |
225 | ||
226 | map[string]string with validation tag "gt=0,dive,keys,eg=1|eq=2,endkeys,required" | |
227 | // gt=0 will be applied to the map itself | |
228 | // eg=1|eq=2 will be applied to the map keys | |
229 | // required will be applied to map values | |
230 | ||
231 | Example #2 | |
232 | ||
233 | map[[2]string]string with validation tag "gt=0,dive,keys,dive,eq=1|eq=2,endkeys,required" | |
234 | // gt=0 will be applied to the map itself | |
235 | // eg=1|eq=2 will be applied to each array element in the the map keys | |
236 | // required will be applied to map values | |
212 | 237 | |
213 | 238 | Required |
214 | 239 |
259 | 259 | ct = ct.next |
260 | 260 | continue |
261 | 261 | |
262 | case typeEndKeys: | |
263 | return | |
264 | ||
262 | 265 | case typeDive: |
263 | 266 | |
264 | 267 | ct = ct.next |
293 | 296 | |
294 | 297 | reusableCF.altName = string(v.misc) |
295 | 298 | } |
296 | ||
297 | 299 | v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct) |
298 | 300 | } |
299 | 301 | |
324 | 326 | reusableCF.altName = string(v.misc) |
325 | 327 | } |
326 | 328 | |
327 | v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct) | |
329 | if ct != nil && ct.typeof == typeKeys && ct.keys != nil { | |
330 | v.traverseField(ctx, parent, key, ns, structNs, reusableCF, ct.keys) | |
331 | // can be nil when just keys being validated | |
332 | if ct.next != nil { | |
333 | v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct.next) | |
334 | } | |
335 | } else { | |
336 | v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct) | |
337 | } | |
328 | 338 | } |
329 | 339 | |
330 | 340 | default: |
24 | 24 | isdefault = "isdefault" |
25 | 25 | skipValidationTag = "-" |
26 | 26 | diveTag = "dive" |
27 | keysTag = "keys" | |
28 | endKeysTag = "endkeys" | |
27 | 29 | requiredTag = "required" |
28 | 30 | namespaceSeparator = "." |
29 | 31 | leftBracket = "[" |
11 | 11 | "testing" |
12 | 12 | "time" |
13 | 13 | |
14 | . "gopkg.in/go-playground/assert.v1" | |
15 | ||
16 | 14 | "github.com/go-playground/locales/en" |
17 | 15 | "github.com/go-playground/locales/fr" |
18 | 16 | "github.com/go-playground/locales/nl" |
19 | 17 | ut "github.com/go-playground/universal-translator" |
18 | . "gopkg.in/go-playground/assert.v1" | |
20 | 19 | ) |
21 | 20 | |
22 | 21 | // NOTES: |
125 | 124 | EqualSkip(t, 2, fe.Field(), field) |
126 | 125 | EqualSkip(t, 2, fe.StructField(), structField) |
127 | 126 | EqualSkip(t, 2, fe.Tag(), expectedTag) |
127 | } | |
128 | ||
129 | func AssertDeepError(t *testing.T, err error, nsKey, structNsKey, field, structField, expectedTag, actualTag string) { | |
130 | errs := err.(ValidationErrors) | |
131 | ||
132 | found := false | |
133 | var fe FieldError | |
134 | ||
135 | for i := 0; i < len(errs); i++ { | |
136 | if errs[i].Namespace() == nsKey && errs[i].StructNamespace() == structNsKey && errs[i].Tag() == expectedTag && errs[i].ActualTag() == actualTag { | |
137 | found = true | |
138 | fe = errs[i] | |
139 | break | |
140 | } | |
141 | } | |
142 | ||
143 | EqualSkip(t, 2, found, true) | |
144 | NotEqualSkip(t, 2, fe, nil) | |
145 | EqualSkip(t, 2, fe.Field(), field) | |
146 | EqualSkip(t, 2, fe.StructField(), structField) | |
128 | 147 | } |
129 | 148 | |
130 | 149 | func getError(err error, nsKey, structNsKey string) FieldError { |
7312 | 7331 | } |
7313 | 7332 | PanicMatches(t, func() { validate.Var(1.0, "unique") }, "Bad field type float64") |
7314 | 7333 | } |
7334 | ||
7335 | func TestKeys(t *testing.T) { | |
7336 | ||
7337 | type Test struct { | |
7338 | Test1 map[string]string `validate:"gt=0,dive,keys,eq=testkey,endkeys,eq=testval" json:"test1"` | |
7339 | Test2 map[int]int `validate:"gt=0,dive,keys,eq=3,endkeys,eq=4" json:"test2"` | |
7340 | Test3 map[int]int `validate:"gt=0,dive,keys,eq=3,endkeys" json:"test3"` | |
7341 | } | |
7342 | ||
7343 | var tst Test | |
7344 | ||
7345 | validate := New() | |
7346 | err := validate.Struct(tst) | |
7347 | NotEqual(t, err, nil) | |
7348 | Equal(t, len(err.(ValidationErrors)), 3) | |
7349 | AssertError(t, err.(ValidationErrors), "Test.Test1", "Test.Test1", "Test1", "Test1", "gt") | |
7350 | AssertError(t, err.(ValidationErrors), "Test.Test2", "Test.Test2", "Test2", "Test2", "gt") | |
7351 | AssertError(t, err.(ValidationErrors), "Test.Test3", "Test.Test3", "Test3", "Test3", "gt") | |
7352 | ||
7353 | tst.Test1 = map[string]string{ | |
7354 | "testkey": "testval", | |
7355 | } | |
7356 | ||
7357 | tst.Test2 = map[int]int{ | |
7358 | 3: 4, | |
7359 | } | |
7360 | ||
7361 | tst.Test3 = map[int]int{ | |
7362 | 3: 4, | |
7363 | } | |
7364 | ||
7365 | err = validate.Struct(tst) | |
7366 | Equal(t, err, nil) | |
7367 | ||
7368 | tst.Test1["badtestkey"] = "badtestvalue" | |
7369 | tst.Test2[10] = 11 | |
7370 | ||
7371 | err = validate.Struct(tst) | |
7372 | NotEqual(t, err, nil) | |
7373 | ||
7374 | errs := err.(ValidationErrors) | |
7375 | ||
7376 | Equal(t, len(errs), 4) | |
7377 | ||
7378 | AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7379 | AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7380 | AssertDeepError(t, errs, "Test.Test2[10]", "Test.Test2[10]", "Test2[10]", "Test2[10]", "eq", "eq") | |
7381 | AssertDeepError(t, errs, "Test.Test2[10]", "Test.Test2[10]", "Test2[10]", "Test2[10]", "eq", "eq") | |
7382 | ||
7383 | type Test2 struct { | |
7384 | NestedKeys map[[1]string]string `validate:"gt=0,dive,keys,dive,eq=innertestkey,endkeys,eq=outertestval"` | |
7385 | } | |
7386 | ||
7387 | var tst2 Test2 | |
7388 | ||
7389 | err = validate.Struct(tst2) | |
7390 | NotEqual(t, err, nil) | |
7391 | Equal(t, len(err.(ValidationErrors)), 1) | |
7392 | AssertError(t, err.(ValidationErrors), "Test2.NestedKeys", "Test2.NestedKeys", "NestedKeys", "NestedKeys", "gt") | |
7393 | ||
7394 | tst2.NestedKeys = map[[1]string]string{ | |
7395 | [1]string{"innertestkey"}: "outertestval", | |
7396 | } | |
7397 | ||
7398 | err = validate.Struct(tst2) | |
7399 | Equal(t, err, nil) | |
7400 | ||
7401 | tst2.NestedKeys[[1]string{"badtestkey"}] = "badtestvalue" | |
7402 | ||
7403 | err = validate.Struct(tst2) | |
7404 | NotEqual(t, err, nil) | |
7405 | ||
7406 | errs = err.(ValidationErrors) | |
7407 | ||
7408 | Equal(t, len(errs), 2) | |
7409 | AssertDeepError(t, errs, "Test2.NestedKeys[[badtestkey]][0]", "Test2.NestedKeys[[badtestkey]][0]", "NestedKeys[[badtestkey]][0]", "NestedKeys[[badtestkey]][0]", "eq", "eq") | |
7410 | AssertDeepError(t, errs, "Test2.NestedKeys[[badtestkey]]", "Test2.NestedKeys[[badtestkey]]", "NestedKeys[[badtestkey]]", "NestedKeys[[badtestkey]]", "eq", "eq") | |
7411 | ||
7412 | // test bad tag definitions | |
7413 | ||
7414 | PanicMatches(t, func() { validate.Var(map[string]string{"key": "val"}, "endkeys,dive,eq=val") }, "'endkeys' tag encountered without a corresponding 'keys' tag") | |
7415 | PanicMatches(t, func() { validate.Var(1, "keys,eq=1,endkeys") }, "'keys' tag must be immediately preceeded by the 'dive' tag") | |
7416 | ||
7417 | // test custom tag name | |
7418 | validate = New() | |
7419 | validate.RegisterTagNameFunc(func(fld reflect.StructField) string { | |
7420 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] | |
7421 | ||
7422 | if name == "-" { | |
7423 | return "" | |
7424 | } | |
7425 | return name | |
7426 | }) | |
7427 | ||
7428 | err = validate.Struct(tst) | |
7429 | NotEqual(t, err, nil) | |
7430 | ||
7431 | errs = err.(ValidationErrors) | |
7432 | ||
7433 | Equal(t, len(errs), 4) | |
7434 | ||
7435 | AssertDeepError(t, errs, "Test.test1[badtestkey]", "Test.Test1[badtestkey]", "test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7436 | AssertDeepError(t, errs, "Test.test1[badtestkey]", "Test.Test1[badtestkey]", "test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7437 | AssertDeepError(t, errs, "Test.test2[10]", "Test.Test2[10]", "test2[10]", "Test2[10]", "eq", "eq") | |
7438 | AssertDeepError(t, errs, "Test.test2[10]", "Test.Test2[10]", "test2[10]", "Test2[10]", "eq", "eq") | |
7439 | } | |
7440 | ||
7441 | // Thanks @adrian-sgn specific test for your specific scenario | |
7442 | func TestKeysCustomValidation(t *testing.T) { | |
7443 | ||
7444 | type LangCode string | |
7445 | type Label map[LangCode]string | |
7446 | ||
7447 | type TestMapStructPtr struct { | |
7448 | Label Label `validate:"dive,keys,lang_code,endkeys,required"` | |
7449 | } | |
7450 | ||
7451 | validate := New() | |
7452 | validate.RegisterValidation("lang_code", func(fl FieldLevel) bool { | |
7453 | validLangCodes := map[LangCode]struct{}{ | |
7454 | "en": {}, | |
7455 | "es": {}, | |
7456 | "pt": {}, | |
7457 | } | |
7458 | ||
7459 | _, ok := validLangCodes[fl.Field().Interface().(LangCode)] | |
7460 | return ok | |
7461 | }) | |
7462 | ||
7463 | label := Label{ | |
7464 | "en": "Good morning!", | |
7465 | "pt": "", | |
7466 | "es": "¡Buenos días!", | |
7467 | "xx": "Bad key", | |
7468 | "xxx": "", | |
7469 | } | |
7470 | ||
7471 | err := validate.Struct(TestMapStructPtr{label}) | |
7472 | NotEqual(t, err, nil) | |
7473 | ||
7474 | errs := err.(ValidationErrors) | |
7475 | Equal(t, len(errs), 4) | |
7476 | ||
7477 | AssertDeepError(t, errs, "TestMapStructPtr.Label[xx]", "TestMapStructPtr.Label[xx]", "Label[xx]", "Label[xx]", "lang_code", "lang_code") | |
7478 | AssertDeepError(t, errs, "TestMapStructPtr.Label[pt]", "TestMapStructPtr.Label[pt]", "Label[pt]", "Label[pt]", "required", "required") | |
7479 | AssertDeepError(t, errs, "TestMapStructPtr.Label[xxx]", "TestMapStructPtr.Label[xxx]", "Label[xxx]", "Label[xxx]", "lang_code", "lang_code") | |
7480 | AssertDeepError(t, errs, "TestMapStructPtr.Label[xxx]", "TestMapStructPtr.Label[xxx]", "Label[xxx]", "Label[xxx]", "required", "required") | |
7481 | ||
7482 | // find specific error | |
7483 | ||
7484 | var e FieldError | |
7485 | for _, e = range errs { | |
7486 | if e.Namespace() == "TestMapStructPtr.Label[xxx]" { | |
7487 | break | |
7488 | } | |
7489 | } | |
7490 | ||
7491 | Equal(t, e.Param(), "") | |
7492 | Equal(t, e.Value().(LangCode), LangCode("xxx")) | |
7493 | ||
7494 | for _, e = range errs { | |
7495 | if e.Namespace() == "TestMapStructPtr.Label[xxx]" && e.Tag() == "required" { | |
7496 | break | |
7497 | } | |
7498 | } | |
7499 | ||
7500 | Equal(t, e.Param(), "") | |
7501 | Equal(t, e.Value().(string), "") | |
7502 | } | |
7503 | ||
7504 | func TestKeyOrs(t *testing.T) { | |
7505 | ||
7506 | type Test struct { | |
7507 | Test1 map[string]string `validate:"gt=0,dive,keys,eq=testkey|eq=testkeyok,endkeys,eq=testval" json:"test1"` | |
7508 | } | |
7509 | ||
7510 | var tst Test | |
7511 | ||
7512 | validate := New() | |
7513 | err := validate.Struct(tst) | |
7514 | NotEqual(t, err, nil) | |
7515 | Equal(t, len(err.(ValidationErrors)), 1) | |
7516 | AssertError(t, err.(ValidationErrors), "Test.Test1", "Test.Test1", "Test1", "Test1", "gt") | |
7517 | ||
7518 | tst.Test1 = map[string]string{ | |
7519 | "testkey": "testval", | |
7520 | } | |
7521 | ||
7522 | err = validate.Struct(tst) | |
7523 | Equal(t, err, nil) | |
7524 | ||
7525 | tst.Test1["badtestkey"] = "badtestval" | |
7526 | ||
7527 | err = validate.Struct(tst) | |
7528 | NotEqual(t, err, nil) | |
7529 | ||
7530 | errs := err.(ValidationErrors) | |
7531 | ||
7532 | Equal(t, len(errs), 2) | |
7533 | ||
7534 | AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq=testkey|eq=testkeyok", "eq=testkey|eq=testkeyok") | |
7535 | AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7536 | ||
7537 | validate.RegisterAlias("okkey", "eq=testkey|eq=testkeyok") | |
7538 | ||
7539 | type Test2 struct { | |
7540 | Test1 map[string]string `validate:"gt=0,dive,keys,okkey,endkeys,eq=testval" json:"test1"` | |
7541 | } | |
7542 | ||
7543 | var tst2 Test2 | |
7544 | ||
7545 | err = validate.Struct(tst2) | |
7546 | NotEqual(t, err, nil) | |
7547 | Equal(t, len(err.(ValidationErrors)), 1) | |
7548 | AssertError(t, err.(ValidationErrors), "Test2.Test1", "Test2.Test1", "Test1", "Test1", "gt") | |
7549 | ||
7550 | tst2.Test1 = map[string]string{ | |
7551 | "testkey": "testval", | |
7552 | } | |
7553 | ||
7554 | err = validate.Struct(tst2) | |
7555 | Equal(t, err, nil) | |
7556 | ||
7557 | tst2.Test1["badtestkey"] = "badtestval" | |
7558 | ||
7559 | err = validate.Struct(tst2) | |
7560 | NotEqual(t, err, nil) | |
7561 | ||
7562 | errs = err.(ValidationErrors) | |
7563 | ||
7564 | Equal(t, len(errs), 2) | |
7565 | ||
7566 | AssertDeepError(t, errs, "Test2.Test1[badtestkey]", "Test2.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "okkey", "eq=testkey|eq=testkeyok") | |
7567 | AssertDeepError(t, errs, "Test2.Test1[badtestkey]", "Test2.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq") | |
7568 | } |