go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/validate.star (about) 1 # Copyright 2018 The LUCI Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """Generic value validators.""" 16 17 load("@stdlib//internal/lucicfg.star", "lucicfg") 18 load("@stdlib//internal/re.star", "re") 19 load("@stdlib//internal/time.star", "time") 20 21 def _string(attr, val, *, regexp = None, allow_empty = False, default = None, required = True): 22 """Validates that the value is a string and returns it. 23 24 Args: 25 attr: field name with this value, for error messages. 26 val: a value to validate. 27 regexp: a regular expression to check 'val' against. 28 allow_empty: if True, accept empty string as valid. 29 default: a value to use if 'val' is None, ignored if required is True. 30 required: if False, allow 'val' to be None, return 'default' in this case. 31 32 Returns: 33 The validated string or None if required is False and default is None. 34 """ 35 if val == None: 36 if required: 37 fail("missing required field %r" % attr) 38 if default == None: 39 return None 40 val = default 41 42 if type(val) != "string": 43 fail("bad %r: got %s, want string" % (attr, type(val))) 44 if not allow_empty and not val: 45 fail("bad %r: must not be empty" % (attr,)) 46 if regexp and not re.submatches(regexp, val): 47 fail("bad %r: %r should match %r" % (attr, val, regexp)) 48 49 return val 50 51 def _hostname(attr, val, *, default = None, required = True): 52 """Validates that the value is a string RFC 1123 hostname and returns it. 53 54 Args: 55 attr: field name with this value, for error messages. 56 val: a value to validate. 57 default: a value to use if 'val' is None, ignored if required is True. 58 required: if False, allow 'val' to be None, return 'default' in this case. 59 60 Returns: 61 The validated hostname or None if required is False and default is None. 62 """ 63 if val == None: 64 if required: 65 fail("missing required field %r" % attr) 66 if default == None: 67 return None 68 val = default 69 70 if type(val) != "string": 71 fail("bad %r: got %s, want string" % (attr, type(val))) 72 if not val: 73 fail("bad %r: must not be empty" % (attr,)) 74 75 hostname_regexp = r"^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$" 76 if not re.submatches(hostname_regexp, val): 77 fail("bad %r: %r is not valid RFC1123 hostname" % (attr, val)) 78 79 return val 80 81 def _int(attr, val, *, min = None, max = None, default = None, required = True): 82 """Validates that the value is an integer and returns it. 83 84 Args: 85 attr: field name with this value, for error messages. 86 val: a value to validate. 87 min: minimal allowed value (inclusive) or None for unbounded. 88 max: maximal allowed value (inclusive) or None for unbounded. 89 default: a value to use if 'val' is None, ignored if required is True. 90 required: if False, allow 'val' to be None, return 'default' in this case. 91 92 Returns: 93 The validated int or None if required is False and default is None. 94 """ 95 if val == None: 96 if required: 97 fail("missing required field %r" % attr) 98 if default == None: 99 return None 100 val = default 101 102 if type(val) != "int": 103 fail("bad %r: got %s, want int" % (attr, type(val))) 104 105 if min != None and val < min: 106 fail("bad %r: %s should be >= %s" % (attr, val, min)) 107 if max != None and val > max: 108 fail("bad %r: %s should be <= %s" % (attr, val, max)) 109 110 return val 111 112 def _float(attr, val, *, min = None, max = None, default = None, required = True): 113 """Validates that the value is a float or integer and returns it as float. 114 115 Args: 116 attr: field name with this value, for error messages. 117 val: a value to validate. 118 min: minimal allowed value (inclusive) or None for unbounded. 119 max: maximal allowed value (inclusive) or None for unbounded. 120 default: a value to use if 'val' is None, ignored if required is True. 121 required: if False, allow 'val' to be None, return 'default' in this case. 122 123 Returns: 124 The validated float or None if required is False and default is None. 125 """ 126 if val == None: 127 if required: 128 fail("missing required field %r" % attr) 129 if default == None: 130 return None 131 val = default 132 133 if type(val) == "int": 134 val = float(val) 135 elif type(val) != "float": 136 fail("bad %r: got %s, want float or int" % (attr, type(val))) 137 138 if min != None and val < min: 139 fail("bad %r: %s should be >= %s" % (attr, val, min)) 140 if max != None and val > max: 141 fail("bad %r: %s should be <= %s" % (attr, val, max)) 142 143 return val 144 145 def _bool(attr, val, *, default = None, required = True): 146 """Validates that the value can be converted to a boolean. 147 148 Zero values other than None (0, "", [], etc) are treated as False. None 149 indicates "use default". If required is False and val is None, returns None 150 (indicating no value was passed). 151 152 Args: 153 attr: field name with this value, for error messages. 154 val: a value to validate. 155 default: a value to use if 'val' is None, ignored if required is True. 156 required: if False, allow 'val' to be None, return 'default' in this case. 157 158 Returns: 159 The boolean or None if required is False and default is None. 160 """ 161 if val == None: 162 if required: 163 fail("missing required field %r" % attr) 164 if default == None: 165 return None 166 val = default 167 return bool(val) 168 169 def _duration(attr, val, *, precision = time.second, min = time.zero, max = None, default = None, required = True): 170 """Validates that the value is a duration specified at the given precision. 171 172 For example, if 'precision' is time.second, will validate that the given 173 duration has a whole number of seconds. Fails if truncating the duration to 174 the requested precision loses information. 175 176 Args: 177 attr: field name with this value, for error messages. 178 val: a value to validate. 179 precision: a time unit to divide 'val' by to get the output. 180 min: minimal allowed duration (inclusive) or None for unbounded. 181 max: maximal allowed duration (inclusive) or None for unbounded. 182 default: a value to use if 'val' is None, ignored if required is True. 183 required: if False, allow 'val' to be None, return 'default' in this case. 184 185 Returns: 186 The validated duration or None if required is False and default is None. 187 """ 188 if val == None: 189 if required: 190 fail("missing required field %r" % attr) 191 if default == None: 192 return None 193 val = default 194 195 if type(val) != "duration": 196 fail("bad %r: got %s, want duration" % (attr, type(val))) 197 198 if min != None and val < min: 199 fail("bad %r: %s should be >= %s" % (attr, val, min)) 200 if max != None and val > max: 201 fail("bad %r: %s should be <= %s" % (attr, val, max)) 202 203 if time.truncate(val, precision) != val: 204 fail(( 205 "bad %r: losing precision when truncating %s to %s units, " + 206 "use time.truncate(...) to acknowledge" 207 ) % (attr, val, precision)) 208 209 return val 210 211 def _email(attr, val, *, default = None, required = True): 212 """Validates that the value is a string RFC 2822 hostname and returns it. 213 214 Args: 215 attr: field name with this value, for error messages. 216 val: a value to validate. 217 default: a value to use if 'val' is None, ignored if required is True. 218 required: if False, allow 'val' to be None, return 'default' in this case. 219 220 Returns: 221 The validated email or None if required is False and default is None. 222 """ 223 if val == None: 224 if required: 225 fail("missing required field %r" % attr) 226 if default == None: 227 return None 228 val = default 229 230 if type(val) != "string": 231 fail("bad %r: got %s, want string" % (attr, type(val))) 232 if not val: 233 fail("bad %r: must not be empty" % (attr,)) 234 235 email_regexp = r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" 236 if not re.submatches(email_regexp, val.lower()): 237 fail("bad %r: %r is not a valid RFC 2822 email" % (attr, val)) 238 239 return val 240 241 def _list(attr, val, *, required = False): 242 """Validates that the value is a list and returns it. 243 244 None is treated as an empty list. 245 246 Args: 247 attr: field name with this value, for error messages. 248 val: a value to validate. 249 required: if False, allow 'val' to be None or empty, return empty list in 250 this case. 251 252 Returns: 253 The validated list. 254 """ 255 if val == None: 256 val = [] 257 258 if type(val) != "list": 259 fail("bad %r: got %s, want list" % (attr, type(val))) 260 261 if required and not val: 262 fail("missing required field %r" % attr) 263 264 return val 265 266 def _str_dict(attr, val, *, required = False): 267 """Validates that the value is a dict with non-empty string keys. 268 269 None is treated as an empty dict. 270 271 Args: 272 attr: field name with this value, for error messages. 273 val: a value to validate. 274 required: if False, allow 'val' to be None or empty, return empty dict in 275 this case. 276 277 Returns: 278 The validated dict. 279 """ 280 if val == None: 281 val = {} 282 283 if type(val) != "dict": 284 fail("bad %r: got %s, want dict" % (attr, type(val))) 285 286 if required and not val: 287 fail("missing required field %r" % attr) 288 289 for k in val: 290 if type(k) != "string": 291 fail("bad %r: got %s key, want string" % (attr, type(k))) 292 if not k: 293 fail("bad %r: got empty key" % attr) 294 295 return val 296 297 def _struct(attr, val, sym, *, default = None, required = True): 298 """Validates that the value is a struct of the given flavor and returns it. 299 300 Args: 301 attr: field name with this value, for error messages. 302 val: a value to validate. 303 sym: a name of the constructor that produced the struct. 304 default: a value to use if 'val' is None, ignored if required is True. 305 required: if False, allow 'val' to be None, return 'default' in this case. 306 307 Returns: 308 The validated struct or None if required is False and default is None. 309 """ 310 if val == None: 311 if required: 312 fail("missing required field %r" % attr) 313 if default == None: 314 return None 315 val = default 316 317 tp = __native__.ctor(val) or type(val) # ctor(...) return None for non-structs 318 if tp != sym: 319 fail("bad %r: got %s, want %s" % (attr, tp, sym)) 320 321 return val 322 323 def _type(attr, val, prototype, *, default = None, required = True): 324 """Validates the value has the same type as `prototype` or is None. 325 326 Useful when checking types of protobuf messages. 327 328 Args: 329 attr: field name with this value, for error messages. 330 val: a value to validate. 331 prototype: a prototype value to compare val's type against. 332 default: a value to use if `val` is None, ignored if required is True. 333 required: if False, allow `val` to be None, return `default` in this case. 334 335 Returns: 336 `val` on success or None if required is False and default is None. 337 """ 338 if val == None: 339 if required: 340 fail("missing required field %r" % attr) 341 if default == None: 342 return None 343 val = default 344 345 if type(val) != type(prototype): 346 fail("bad %r: got %s, want %s" % (attr, type(val), type(prototype))) 347 348 return val 349 350 def _repo_url(attr, val, *, required = True): 351 """Validates that the value is `https://...` repository URL and returns it. 352 353 Additionally verifies that `val` doesn't end with `.git`. 354 355 Args: 356 attr: name of the var for error messages. Required. 357 val: a value to validate. Required. 358 required: if False, allow `val` to be None, return None in this case. 359 360 Returns: 361 Validate `val` or None if it is None and `required` is False. 362 """ 363 val = validate.string(attr, val, regexp = r"https://.+", required = required) 364 if val and val.endswith(".git"): 365 fail('bad %r: %r should not end with ".git"' % (attr, val)) 366 return val 367 368 def _relative_path(attr, val, *, allow_dots = False, base = None, required = True, default = None): 369 """Validates that the value is a string with relative path. 370 371 Optionally adds it to some base path and returns the cleaned resulting path. 372 373 Args: 374 attr: name of the var for error messages. Required. 375 val: a value to validate. Required. 376 allow_dots: if True, allow `../` as a prefix in the resulting path. 377 Default is False. 378 base: if given, apply the relative path to this base path and returns the 379 result. 380 default: a value to use if 'val' is None, ignored if required is True. 381 required: if False, allow 'val' to be None, return `default` in this case. 382 383 Returns: 384 Validated, cleaned and (if `base` is given) rebased path. 385 """ 386 val = validate.string(attr, val, required = required, default = default) 387 if val == None: 388 return None 389 base = validate.string("base", base, required = False) 390 clean, err = __native__.clean_relative_path(base or "", val, allow_dots) 391 if err: 392 fail("bad %r: %s" % (attr, err)) 393 return clean 394 395 def _regex_list(attr, val, *, required = False): 396 """Validates that the value is a valid regex parameter. 397 398 Strings are valid, and are returned unchanged. Lists of strings are valid, 399 and are combined into a single regex that matches any of the regexes in 400 the list. 401 402 None is treated as an empty string. 403 404 Args: 405 attr: field name with this value, for error messages. 406 val: a value to validate. 407 required: if False, allow 'val' to be None or empty, return empty string 408 in this case. 409 410 Returns: 411 The validated regex. 412 """ 413 if val == None: 414 val = "" 415 416 if required and not val: 417 fail("missing required field %r" % attr) 418 419 if type(val) == "string": 420 valid, err = __native__.is_valid_regex(val) 421 if not valid: 422 fail("bad %r: %s" % (attr, err)) 423 return val 424 if type(val) == "list": 425 # buildifier: disable=string-iteration 426 for s in val: 427 if type(s) != "string": 428 fail("bad %r: got list element of type %s, want string" % (attr, type(s))) 429 valid, err = __native__.is_valid_regex(s) 430 if not valid: 431 fail("bad %r: %s" % (attr, err)) 432 return "|".join(val) 433 434 fail("bad %r: got %s, want string or list" % (attr, type(val))) 435 436 def _str_list(attr, val, *, required = False): 437 """Validates that the value is a list of strings. 438 439 None is treated as an empty list. 440 441 Args: 442 attr: field name with this value, for error messages. 443 val: a value to validate. 444 required: if False, allow 'val' to be None or empty, return empty list in 445 this case. 446 447 Returns: 448 The validated list. 449 """ 450 if val == None: 451 val = [] 452 453 if required and not val: 454 fail("missing required field %r" % attr) 455 456 if type(val) == "list": 457 for s in val: 458 if type(s) != "string": 459 fail("bad %r: got list element of type %s, want string" % (attr, type(s))) 460 return val 461 462 fail("bad %r: got %s, want list of strings" % (attr, type(val))) 463 464 def _var_with_validator(attr, validator, **kwargs): 465 """Returns a lucicfg.var that validates the value via a validator callback. 466 467 Args: 468 attr: name of the var for error messages. Required. 469 validator: a callback(attr, value, **kwargs), e.g. `validate.string`. 470 Required. 471 **kwargs: keyword arguments to pass to `validator`. 472 473 Returns: 474 lucicfg.var(...). 475 """ 476 return lucicfg.var(validator = lambda value: validator(attr, value, **kwargs)) 477 478 def _vars_with_validators(vars): 479 """Accepts dict `{attr -> validator}`, returns dict `{attr -> lucicfg.var}`. 480 481 Basically applies validate.var_with_validator(...) to each item of the dict. 482 483 Args: 484 vars: a dict with string keys and callable values, matching the signature 485 of `validator` in validate.var_with_validator(...). Required. 486 487 Returns: 488 Dict with string keys and lucicfg.var(...) values. 489 """ 490 return {attr: _var_with_validator(attr, validator) for attr, validator in vars.items()} 491 492 validate = struct( 493 string = _string, 494 int = _int, 495 float = _float, 496 bool = _bool, 497 duration = _duration, 498 email = _email, 499 list = _list, 500 str_dict = _str_dict, 501 struct = _struct, 502 type = _type, 503 repo_url = _repo_url, 504 hostname = _hostname, 505 relative_path = _relative_path, 506 regex_list = _regex_list, 507 str_list = _str_list, 508 var_with_validator = _var_with_validator, 509 vars_with_validators = _vars_with_validators, 510 )