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  )