# Copyright 2023 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Implementation of matchers.""" def _match_custom(desc, func): """Wrap an arbitrary function up as a Matcher. Method: Matcher.new `Matcher` struct attributes: * `desc`: ([`str`]) a human-friendly description * `match`: (callable) accepts 1 positional arg (the value to match) and returns [`bool`] (`True` if it matched, `False` if not). Args: desc: ([`str`]) a human-friendly string describing what is matched. func: (callable) accepts 1 positional arg (the value to match) and returns [`bool`] (`True` if it matched, `False` if not). Returns: [`Matcher`] (see above). """ return struct(desc = desc, match = func) def _match_equals_wrapper(value): """Match that a value equals `value`, but use `value` as the `desc`. This is a helper so that simple equality comparisons can re-use predicate based APIs. Args: value: object, the value that must be equal to. Returns: [`Matcher`] (see `_match_custom()`), whose description is `value`. """ return _match_custom(value, lambda other: other == value) def _match_file_basename_contains(substr): """Match that a a `File.basename` string contains a substring. Args: substr: ([`str`]) the substring to match. Returns: [`Matcher`] (see `_match_custom()`). """ return struct( desc = "".format(substr), match = lambda f: substr in f.basename, ) def _match_file_path_matches(pattern): """Match that a `File.path` string matches a glob-style pattern. Args: pattern: ([`str`]) the pattern to match. "*" can be used to denote "match anything". Returns: [`Matcher`] (see `_match_custom`). """ parts = pattern.split("*") return struct( desc = "".format(pattern), match = lambda f: _match_parts_in_order(f.path, parts), ) def _match_file_basename_equals(value): """Match that a `File.basename` string equals `value`. Args: value: ([`str`]) the basename to match. Returns: [`Matcher`] instance """ return struct( desc = "".format(value), match = lambda f: f.basename == value, ) def _match_file_extension_in(values): """Match that a `File.extension` string is any of `values`. See also: `file_path_matches` for matching extensions that have multiple parts, e.g. `*.tar.gz` or `*.so.*`. Args: values: ([`list`] of [`str`]) the extensions to match. Returns: [`Matcher`] instance """ return struct( desc = "".format(repr(values)), match = lambda f: f.extension in values, ) def _match_is_in(values): """Match that the to-be-matched value is in a collection of other values. This is equivalent to: `to_be_matched in values`. See `_match_contains` for the reversed operation. Args: values: The collection that the value must be within. Returns: [`Matcher`] (see `_match_custom()`). """ return struct( desc = "".format(repr(values)), match = lambda v: v in values, ) def _match_never(desc): """A matcher that never matches. This is mostly useful for testing, as it allows preventing any match while providing a custom description. Args: desc: ([`str`]) human-friendly string. Returns: [`Matcher`] (see `_match_custom`). """ return struct( desc = desc, match = lambda value: False, ) def _match_contains(contained): """Match that `contained` is within the to-be-matched value. This is equivalent to: `contained in to_be_matched`. See `_match_is_in` for the reversed operation. Args: contained: the value that to-be-matched value must contain. Returns: [`Matcher`] (see `_match_custom`). """ return struct( desc = "".format(contained), match = lambda value: contained in value, ) def _match_str_endswith(suffix): """Match that a string contains another string. Args: suffix: ([`str`]) the suffix that must be present Returns: [`Matcher`] (see `_match_custom`). """ return struct( desc = "".format(suffix), match = lambda value: value.endswith(suffix), ) def _match_str_matches(pattern): """Match that a string matches a glob-style pattern. Args: pattern: ([`str`]) the pattern to match. `*` can be used to denote "match anything". There is an implicit `*` at the start and end of the pattern. Returns: [`Matcher`] object. """ parts = pattern.split("*") return struct( desc = "".format(pattern), match = lambda value: _match_parts_in_order(value, parts), ) def _match_str_startswith(prefix): """Match that a string contains another string. Args: prefix: ([`str`]) the prefix that must be present Returns: [`Matcher`] (see `_match_custom`). """ return struct( desc = "".format(prefix), match = lambda value: value.startswith(prefix), ) def _match_parts_in_order(string, parts): start = 0 for part in parts: start = string.find(part, start) if start == -1: return False return True def _is_matcher(obj): return hasattr(obj, "desc") and hasattr(obj, "match") # For the definition of a `Matcher` object, see `_match_custom`. matching = struct( # keep sorted start contains = _match_contains, custom = _match_custom, equals_wrapper = _match_equals_wrapper, file_basename_contains = _match_file_basename_contains, file_basename_equals = _match_file_basename_equals, file_path_matches = _match_file_path_matches, file_extension_in = _match_file_extension_in, is_in = _match_is_in, never = _match_never, str_endswith = _match_str_endswith, str_matches = _match_str_matches, str_startswith = _match_str_startswith, is_matcher = _is_matcher, # keep sorted end )