Skip to content

Commit 8957af5

Browse files
author
Dmitry Mandrika
committed
init
0 parents  commit 8957af5

File tree

6 files changed

+1244
-0
lines changed

6 files changed

+1244
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Dmitriy
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 all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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 THE
21+
SOFTWARE.

README.txt

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
Package validator implements value validations based on struct tags.
2+
3+
In code it is often necessary to validate that a given value is valid before
4+
using it for something. A typical example might be something like this.
5+
6+
if age < 18 {
7+
return error.New("age cannot be under 18")
8+
}
9+
10+
This is a simple enough example, but it can get significantly more complex,
11+
especially when dealing with structs.
12+
13+
l := len(strings.Trim(s.Username))
14+
if l < 3 || l > 40 || !regexp.MatchString("^[a-zA-Z]$", s.Username) || s.Age < 18 || s.Password {
15+
return errors.New("Invalid request")
16+
}
17+
18+
You get the idea. Package validator allows one to define valid values as
19+
struct tags when defining a new struct type.
20+
21+
type NewUserRequest struct {
22+
Username string `validate:"min=3,max=40,regexp=^[a-zA-Z]$"`
23+
Name string `validate:"nonzero"`
24+
Age int `validate:"min=18"`
25+
Password string `validate:"min=8"`
26+
}
27+
28+
Then validating a variable of type NewUserRequest becomes trivial.
29+
30+
nur := NewUserRequest{Username: "something", ...}
31+
if valid, _ := validator.Validate(nur); valid {
32+
// do something
33+
}
34+
35+
Builtin validator functions
36+
37+
Here is the list of validator functions builtin in the package.
38+
39+
max
40+
For numeric numbers, max will simply make sure that the value is
41+
equal to the parameter given. For strings, it checks that
42+
the string length is exactly that number of characters. For slices,
43+
arrays, and maps, validates the number of items. (Usage: len=10)
44+
45+
max
46+
For numeric numbers, max will simply make sure that the value is
47+
lesser or equal to the parameter given. For strings, it checks that
48+
the string length is at most that number of characters. For slices,
49+
arrays, and maps, validates the number of items. (Usage: max=10)
50+
51+
min
52+
For numeric numbers, min will simply make sure that the value is
53+
greater or equal to the parameter given. For strings, it checks that
54+
the string length is at least that number of characters. For slices,
55+
arrays, and maps, validates the number of items. (Usage: min=10)
56+
57+
nonzero
58+
This validates that the value is not zero. The appropriate zero value
59+
is given by the Go spec (e.g. for int it's 0, for string it's "", for
60+
pointers is nil, etc.) Usage: nonzero
61+
62+
regexp
63+
Only valid for string types, it will validate that the value matches
64+
the regular expression provided as parameter. (Usage: regexp=^a.*b$)
65+
66+
in
67+
For string, int, float. Validates that the value is presented in the
68+
whitelist. (Usage: in='str1,str2,str3')
69+
Note: any string containing commas must be single-quoted to prevent
70+
parsing failures.
71+
72+
type
73+
Checks if the value is valid for defined type(one of: base64, timestamp).
74+
(Usage: type=base64)
75+
76+
77+
Note that there are no tests to prevent conflicting validator parameters. For
78+
instance, these fields will never be valid.
79+
80+
...
81+
A int `validate:"max=0,min=1"`
82+
B string `validate:"len=10,regexp=^$"
83+
...
84+
85+
Custom validation functions
86+
87+
It is possible to define custom validation functions by using SetValidationFunc.
88+
First, one needs to create a validation function.
89+
90+
// Very simple validation func
91+
func notZZ(v interface{}, param string) error {
92+
st := reflect.ValueOf(v)
93+
if st.Kind() != reflect.String {
94+
return validate.ErrUnsupported
95+
}
96+
if st.String() == "ZZ" {
97+
return errors.New("value cannot be ZZ")
98+
}
99+
return nil
100+
}
101+
102+
Then one needs to add it to the list of validation funcs and give it a "tag" name.
103+
104+
validate.SetValidationFunc("notzz", notZZ)
105+
106+
Then it is possible to use the notzz validation tag. This will print
107+
"Field A error: value cannot be ZZ"
108+
109+
type T struct {
110+
A string `validate:"nonzero,notzz"`
111+
}
112+
t := T{"ZZ"}
113+
if valid, errs := validator.Validate(t); !valid {
114+
fmt.Printf("Field A error: %s\n", errs["A"][0])
115+
}
116+
117+
To use parameters, it is very similar.
118+
119+
// Very simple validator with parameter
120+
func notSomething(v interface{}, param string) error {
121+
st := reflect.ValueOf(v)
122+
if st.Kind() != reflect.String {
123+
return validate.ErrUnsupported
124+
}
125+
if st.String() == param {
126+
return errors.New("value cannot be " + param)
127+
}
128+
return nil
129+
}
130+
131+
And then the code below should print "Field A error: value cannot be ABC".
132+
133+
validator.SetValidationFunc("notsomething", notSomething)
134+
type T struct {
135+
A string `validate:"notsomething=ABC"`
136+
}
137+
t := T{"ABC"}
138+
if valid, errs := validator.Validate(t); !valid {
139+
fmt.Printf("Field A error: %s\n", errs["A"][0])
140+
}
141+
142+
As well, it is possible to overwrite builtin validation functions.
143+
144+
validate.SetValidationFunc("min", myMinFunc)
145+
146+
And you can delete a validation function by setting it to nil.
147+
148+
validate.SetValidationFunc("notzz", nil)
149+
validate.SetValidationFunc("nonzero", nil)
150+
151+
Using a non-existing validation func in a field tag will always return
152+
false and with error validate.ErrUnknownTag.
153+
154+
Finally, package validator also provides a helper function that can be used
155+
to validate simple variables/values.
156+
157+
// valid: true, errs: []
158+
valid, errs = validator.Valid(42, "min=10, max=50")
159+
160+
// valid: false, errs: [validate.ErrZeroValue]
161+
valid, errs = validator.Valid(nil, "nonzero")
162+
163+
// valid: false, errs: [validate.ErrMin,validate.ErrMax]
164+
valid, errs = validator.Valid("hi", "nonzero,min=3,max=2")
165+
166+
Custom tag name
167+
168+
In case there is a reason why one would not wish to use tag 'validate' (maybe due to
169+
a conflict with a different package), it is possible to tell the package to use
170+
a different tag.
171+
172+
validator.SetTag("valid")
173+
174+
Then.
175+
176+
Type T struct {
177+
A int `valid:"min=8, max=10"`
178+
B string `valid:"nonzero"`
179+
}
180+
181+
SetTag is permanent. The new tag name will be used until it is again changed
182+
with a new call to SetTag. A way to temporarily use a different tag exists.
183+
184+
validator.WithTag("foo").Validate(t)
185+
validator.WithTag("bar").Validate(t)
186+
// But this will go back to using 'validate'
187+
validator.Validate(t)
188+
189+
Multiple validators
190+
191+
You may often need to have a different set of validation
192+
rules for different situations. In all the examples above,
193+
we only used the default validator but you could create a
194+
new one and set specific rules for it.
195+
196+
For instance, you might use the same struct to decode incoming JSON for a REST API
197+
but your needs will change when you're using it to, say, create a new instance
198+
in storage vs. when you need to change something.
199+
200+
type User struct {
201+
Username string `validate:"nonzero"`
202+
Name string `validate:"nonzero"`
203+
Age int `validate:"nonzero"`
204+
Password string `validate:"nonzero"`
205+
}
206+
207+
Maybe when creating a new user, you need to make sure all values in the struct are filled,
208+
but then you use the same struct to handle incoming requests to, say, change the password,
209+
in which case you only need the Username and the Password and don't care for the others.
210+
You might use two different validators.
211+
212+
type User struct {
213+
Username string `creating:"nonzero" chgpw:"nonzero"`
214+
Name string `creating:"nonzero"`
215+
Age int `creating:"nonzero"`
216+
Password string `creating:"nonzero" chgpw:"nonzero"`
217+
}
218+
219+
var (
220+
creationValidator = validator.NewValidator()
221+
chgPwValidator = validator.NewValidator()
222+
)
223+
224+
func init() {
225+
creationValidator.SetTag("creating")
226+
chgPwValidator.SetTag("chgpw")
227+
}
228+
229+
...
230+
231+
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
232+
var u User
233+
json.NewDecoder(r.Body).Decode(&user)
234+
if valid, _ := creationValidator.Validate(user); !valid {
235+
// the request did not include all of the User
236+
// struct fields, so send a http.StatusBadRequest
237+
// back or something
238+
}
239+
// create the new user
240+
}
241+
242+
func SetNewUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
243+
var u User
244+
json.NewDecoder(r.Body).Decode(&user)
245+
if valid, _ := chgPwValidator.Validate(user); !valid {
246+
// the request did not Username and Password,
247+
// so send a http.StatusBadRequest
248+
// back or something
249+
}
250+
// save the new password
251+
}
252+
253+
It is also possible to do all of that using only the default validator as long
254+
as SetTag is always called before calling validator.Validate() or you chain the
255+
with WithTag().
256+
257+
========================
258+
type User struct {
259+
Firstname string `validate:"attr=firstname,min=3,msg_min=errors.form.too_small,max=15,msg_max=errors.form.too_big,regexp=^[a-zA-Z]$,msg_regexp=My custom message"`
260+
Lastname string `validate:"attr=lastname,min=3,msg_min=errors.form.too_small,max=15,msg_max=errors.form.too_big,regexp=^[a-zA-Z]$,msg_regexp=my custom message"`
261+
}
262+
263+
Результат валидации при некорректных данных будет map, где поле ключа соответствует названию атрибута (задается как "attr" в тэге), а значение - первой ошибке из списка правил валидации, если их несколько для одного поля.
264+
265+
map[string]error{
266+
"firstname" : "errors.form.too_small",
267+
"lastname" : "my custom message",
268+
}
269+
270+
Добавить возможность использования кастомных сообщений об ошибках. Они должны задаваться параметром тега "msg_%rule_name%", где %rule_name% - правило валидации.
271+
272+
Необходимые правила валидации:
273+
274+
"empty" - для любого объекта (для цифровых значений 0)
275+
"min", "max" - для string, slice длина, для числовых значений само значение
276+
"in" - значение входит в slice типа поля, к которому относится, к примеру validate:"attr=sex,in=male,female" , для числовых значений: validate:"attr=option,in=1,9,42"
277+
"compare" - значение соответствует заданному согласно типу validate:"attr=agree_terms,compare=true"
278+
"type" - значение соответствует некоторому типу, пока только timestamp и base64 validate:"attr=created_at,type=timestamp"
279+
280+
Для правил min, max должны обрабатываться плейсхолдеры {min} {max} при генерации ошибок. msg_min=The value should be more than {min}.

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/censync/go-validator
2+
3+
go 1.14

0 commit comments

Comments
 (0)