|
| 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}. |
0 commit comments