xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/collection_subject.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
1*d6050574SRomain Jobredeaux# Copyright 2023 The Bazel Authors. All rights reserved.
2*d6050574SRomain Jobredeaux#
3*d6050574SRomain Jobredeaux# Licensed under the Apache License, Version 2.0 (the "License");
4*d6050574SRomain Jobredeaux# you may not use this file except in compliance with the License.
5*d6050574SRomain Jobredeaux# You may obtain a copy of the License at
6*d6050574SRomain Jobredeaux#
7*d6050574SRomain Jobredeaux#     http://www.apache.org/licenses/LICENSE-2.0
8*d6050574SRomain Jobredeaux#
9*d6050574SRomain Jobredeaux# Unless required by applicable law or agreed to in writing, software
10*d6050574SRomain Jobredeaux# distributed under the License is distributed on an "AS IS" BASIS,
11*d6050574SRomain Jobredeaux# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*d6050574SRomain Jobredeaux# See the License for the specific language governing permissions and
13*d6050574SRomain Jobredeaux# limitations under the License.
14*d6050574SRomain Jobredeaux
15*d6050574SRomain Jobredeaux"""# CollectionSubject"""
16*d6050574SRomain Jobredeaux
17*d6050574SRomain Jobredeauxload(
18*d6050574SRomain Jobredeaux    ":check_util.bzl",
19*d6050574SRomain Jobredeaux    "check_contains_at_least_predicates",
20*d6050574SRomain Jobredeaux    "check_contains_exactly",
21*d6050574SRomain Jobredeaux    "check_contains_exactly_predicates",
22*d6050574SRomain Jobredeaux    "check_contains_none_of",
23*d6050574SRomain Jobredeaux    "check_contains_predicate",
24*d6050574SRomain Jobredeaux    "check_not_contains_predicate",
25*d6050574SRomain Jobredeaux)
26*d6050574SRomain Jobredeauxload(
27*d6050574SRomain Jobredeaux    ":failure_messages.bzl",
28*d6050574SRomain Jobredeaux    "format_actual_collection",
29*d6050574SRomain Jobredeaux    "format_problem_expected_exactly",
30*d6050574SRomain Jobredeaux    "format_problem_matched_out_of_order",
31*d6050574SRomain Jobredeaux    "format_problem_missing_required_values",
32*d6050574SRomain Jobredeaux    "format_problem_predicates_did_not_match",
33*d6050574SRomain Jobredeaux    "format_problem_unexpected_values",
34*d6050574SRomain Jobredeaux)
35*d6050574SRomain Jobredeauxload(":int_subject.bzl", "IntSubject")
36*d6050574SRomain Jobredeauxload(":matching.bzl", "matching")
37*d6050574SRomain Jobredeauxload(":truth_common.bzl", "to_list")
38*d6050574SRomain Jobredeauxload(":util.bzl", "get_function_name")
39*d6050574SRomain Jobredeaux
40*d6050574SRomain Jobredeauxdef _identity(v):
41*d6050574SRomain Jobredeaux    return v
42*d6050574SRomain Jobredeaux
43*d6050574SRomain Jobredeauxdef _always_true(v):
44*d6050574SRomain Jobredeaux    _ = v  # @unused
45*d6050574SRomain Jobredeaux    return True
46*d6050574SRomain Jobredeaux
47*d6050574SRomain Jobredeauxdef _collection_subject_new(
48*d6050574SRomain Jobredeaux        values,
49*d6050574SRomain Jobredeaux        meta,
50*d6050574SRomain Jobredeaux        container_name = "values",
51*d6050574SRomain Jobredeaux        sortable = True,
52*d6050574SRomain Jobredeaux        element_plural_name = "elements"):
53*d6050574SRomain Jobredeaux    """Creates a "CollectionSubject" struct.
54*d6050574SRomain Jobredeaux
55*d6050574SRomain Jobredeaux    Method: CollectionSubject.new
56*d6050574SRomain Jobredeaux
57*d6050574SRomain Jobredeaux    Public Attributes:
58*d6050574SRomain Jobredeaux    * `actual`: The wrapped collection.
59*d6050574SRomain Jobredeaux
60*d6050574SRomain Jobredeaux    Args:
61*d6050574SRomain Jobredeaux        values: ([`collection`]) the values to assert against.
62*d6050574SRomain Jobredeaux        meta: ([`ExpectMeta`]) the metadata about the call chain.
63*d6050574SRomain Jobredeaux        container_name: ([`str`]) conceptual name of the container.
64*d6050574SRomain Jobredeaux        sortable: ([`bool`]) True if output should be sorted for display, False if not.
65*d6050574SRomain Jobredeaux        element_plural_name: ([`str`]) the plural word for the values in the container.
66*d6050574SRomain Jobredeaux
67*d6050574SRomain Jobredeaux    Returns:
68*d6050574SRomain Jobredeaux        [`CollectionSubject`].
69*d6050574SRomain Jobredeaux    """
70*d6050574SRomain Jobredeaux
71*d6050574SRomain Jobredeaux    # buildifier: disable=uninitialized
72*d6050574SRomain Jobredeaux    public = struct(
73*d6050574SRomain Jobredeaux        # keep sorted start
74*d6050574SRomain Jobredeaux        actual = values,
75*d6050574SRomain Jobredeaux        contains = lambda *a, **k: _collection_subject_contains(self, *a, **k),
76*d6050574SRomain Jobredeaux        contains_at_least = lambda *a, **k: _collection_subject_contains_at_least(self, *a, **k),
77*d6050574SRomain Jobredeaux        contains_at_least_predicates = lambda *a, **k: _collection_subject_contains_at_least_predicates(self, *a, **k),
78*d6050574SRomain Jobredeaux        contains_exactly = lambda *a, **k: _collection_subject_contains_exactly(self, *a, **k),
79*d6050574SRomain Jobredeaux        contains_exactly_predicates = lambda *a, **k: _collection_subject_contains_exactly_predicates(self, *a, **k),
80*d6050574SRomain Jobredeaux        contains_none_of = lambda *a, **k: _collection_subject_contains_none_of(self, *a, **k),
81*d6050574SRomain Jobredeaux        contains_predicate = lambda *a, **k: _collection_subject_contains_predicate(self, *a, **k),
82*d6050574SRomain Jobredeaux        has_size = lambda *a, **k: _collection_subject_has_size(self, *a, **k),
83*d6050574SRomain Jobredeaux        not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k),
84*d6050574SRomain Jobredeaux        not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k),
85*d6050574SRomain Jobredeaux        offset = lambda *a, **k: _collection_subject_offset(self, *a, **k),
86*d6050574SRomain Jobredeaux        transform = lambda *a, **k: _collection_subject_transform(self, *a, **k),
87*d6050574SRomain Jobredeaux        # keep sorted end
88*d6050574SRomain Jobredeaux    )
89*d6050574SRomain Jobredeaux    self = struct(
90*d6050574SRomain Jobredeaux        actual = values,
91*d6050574SRomain Jobredeaux        meta = meta,
92*d6050574SRomain Jobredeaux        element_plural_name = element_plural_name,
93*d6050574SRomain Jobredeaux        container_name = container_name,
94*d6050574SRomain Jobredeaux        sortable = sortable,
95*d6050574SRomain Jobredeaux        contains_predicate = public.contains_predicate,
96*d6050574SRomain Jobredeaux        contains_at_least_predicates = public.contains_at_least_predicates,
97*d6050574SRomain Jobredeaux    )
98*d6050574SRomain Jobredeaux    return public
99*d6050574SRomain Jobredeaux
100*d6050574SRomain Jobredeauxdef _collection_subject_has_size(self, expected):
101*d6050574SRomain Jobredeaux    """Asserts that `expected` is the size of the collection.
102*d6050574SRomain Jobredeaux
103*d6050574SRomain Jobredeaux    Method: CollectionSubject.has_size
104*d6050574SRomain Jobredeaux
105*d6050574SRomain Jobredeaux    Args:
106*d6050574SRomain Jobredeaux        self: implicitly added.
107*d6050574SRomain Jobredeaux        expected: ([`int`]) the expected size of the collection.
108*d6050574SRomain Jobredeaux    """
109*d6050574SRomain Jobredeaux    return IntSubject.new(
110*d6050574SRomain Jobredeaux        len(self.actual),
111*d6050574SRomain Jobredeaux        meta = self.meta.derive("size()"),
112*d6050574SRomain Jobredeaux    ).equals(expected)
113*d6050574SRomain Jobredeaux
114*d6050574SRomain Jobredeauxdef _collection_subject_contains(self, expected):
115*d6050574SRomain Jobredeaux    """Asserts that `expected` is within the collection.
116*d6050574SRomain Jobredeaux
117*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains
118*d6050574SRomain Jobredeaux
119*d6050574SRomain Jobredeaux    Args:
120*d6050574SRomain Jobredeaux        self: implicitly added.
121*d6050574SRomain Jobredeaux        expected: ([`str`]) the value that must be present.
122*d6050574SRomain Jobredeaux    """
123*d6050574SRomain Jobredeaux    matcher = matching.equals_wrapper(expected)
124*d6050574SRomain Jobredeaux    return self.contains_predicate(matcher)
125*d6050574SRomain Jobredeaux
126*d6050574SRomain Jobredeauxdef _collection_subject_contains_exactly(self, expected):
127*d6050574SRomain Jobredeaux    """Check that a collection contains exactly the given elements.
128*d6050574SRomain Jobredeaux
129*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_exactly
130*d6050574SRomain Jobredeaux
131*d6050574SRomain Jobredeaux    * Multiplicity is respected.
132*d6050574SRomain Jobredeaux    * The collection must contain all the values, no more or less.
133*d6050574SRomain Jobredeaux    * Checking that the order of matches is the same as the passed-in matchers
134*d6050574SRomain Jobredeaux      order can be done by call `in_order()`.
135*d6050574SRomain Jobredeaux
136*d6050574SRomain Jobredeaux    The collection must contain all the values and no more. Multiplicity of
137*d6050574SRomain Jobredeaux    values is respected. Checking that the order of matches is the same as the
138*d6050574SRomain Jobredeaux    passed-in matchers order can done by calling `in_order()`.
139*d6050574SRomain Jobredeaux
140*d6050574SRomain Jobredeaux    Args:
141*d6050574SRomain Jobredeaux        self: implicitly added.
142*d6050574SRomain Jobredeaux        expected: ([`list`]) values that must exist.
143*d6050574SRomain Jobredeaux
144*d6050574SRomain Jobredeaux    Returns:
145*d6050574SRomain Jobredeaux        [`Ordered`] (see `_ordered_incorrectly_new`).
146*d6050574SRomain Jobredeaux    """
147*d6050574SRomain Jobredeaux    expected = to_list(expected)
148*d6050574SRomain Jobredeaux    return check_contains_exactly(
149*d6050574SRomain Jobredeaux        actual_container = self.actual,
150*d6050574SRomain Jobredeaux        expect_contains = expected,
151*d6050574SRomain Jobredeaux        meta = self.meta,
152*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
153*d6050574SRomain Jobredeaux            self.actual,
154*d6050574SRomain Jobredeaux            name = self.container_name,
155*d6050574SRomain Jobredeaux            sort = False,  # Don't sort; this might be rendered by the in_order() error.
156*d6050574SRomain Jobredeaux        ),
157*d6050574SRomain Jobredeaux        format_expected = lambda: format_problem_expected_exactly(
158*d6050574SRomain Jobredeaux            expected,
159*d6050574SRomain Jobredeaux            sort = False,  # Don't sort; this might be rendered by the in_order() error.
160*d6050574SRomain Jobredeaux        ),
161*d6050574SRomain Jobredeaux        format_missing = lambda missing: format_problem_missing_required_values(
162*d6050574SRomain Jobredeaux            missing,
163*d6050574SRomain Jobredeaux            sort = self.sortable,
164*d6050574SRomain Jobredeaux        ),
165*d6050574SRomain Jobredeaux        format_unexpected = lambda unexpected: format_problem_unexpected_values(
166*d6050574SRomain Jobredeaux            unexpected,
167*d6050574SRomain Jobredeaux            sort = self.sortable,
168*d6050574SRomain Jobredeaux        ),
169*d6050574SRomain Jobredeaux        format_out_of_order = format_problem_matched_out_of_order,
170*d6050574SRomain Jobredeaux    )
171*d6050574SRomain Jobredeaux
172*d6050574SRomain Jobredeauxdef _collection_subject_contains_exactly_predicates(self, expected):
173*d6050574SRomain Jobredeaux    """Check that the values correspond 1:1 to the predicates.
174*d6050574SRomain Jobredeaux
175*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_exactly_predicates
176*d6050574SRomain Jobredeaux
177*d6050574SRomain Jobredeaux    * There must be a 1:1 correspondence between the container values and the
178*d6050574SRomain Jobredeaux      predicates.
179*d6050574SRomain Jobredeaux    * Multiplicity is respected (i.e., if the same predicate occurs twice, then
180*d6050574SRomain Jobredeaux      two distinct elements must match).
181*d6050574SRomain Jobredeaux    * Matching occurs in first-seen order. That is, a predicate will "consume"
182*d6050574SRomain Jobredeaux      the first value in `actual_container` it matches.
183*d6050574SRomain Jobredeaux    * The collection must match all the predicates, no more or less.
184*d6050574SRomain Jobredeaux    * Checking that the order of matches is the same as the passed-in matchers
185*d6050574SRomain Jobredeaux      order can be done by call `in_order()`.
186*d6050574SRomain Jobredeaux
187*d6050574SRomain Jobredeaux    Note that confusing results may occur if predicates with overlapping
188*d6050574SRomain Jobredeaux    match conditions are used. For example, given:
189*d6050574SRomain Jobredeaux      actual=["a", "ab", "abc"],
190*d6050574SRomain Jobredeaux      predicates=[<contains a>, <contains b>, <equals a>]
191*d6050574SRomain Jobredeaux
192*d6050574SRomain Jobredeaux    Then the result will be they aren't equal: the first two predicates
193*d6050574SRomain Jobredeaux    consume "a" and "ab", leaving only "abc" for the <equals a> predicate
194*d6050574SRomain Jobredeaux    to match against, which fails.
195*d6050574SRomain Jobredeaux
196*d6050574SRomain Jobredeaux    Args:
197*d6050574SRomain Jobredeaux        self: implicitly added.
198*d6050574SRomain Jobredeaux        expected: ([`list`] of [`Matcher`]) that must match.
199*d6050574SRomain Jobredeaux
200*d6050574SRomain Jobredeaux    Returns:
201*d6050574SRomain Jobredeaux        [`Ordered`] (see `_ordered_incorrectly_new`).
202*d6050574SRomain Jobredeaux    """
203*d6050574SRomain Jobredeaux    expected = to_list(expected)
204*d6050574SRomain Jobredeaux    return check_contains_exactly_predicates(
205*d6050574SRomain Jobredeaux        actual_container = self.actual,
206*d6050574SRomain Jobredeaux        expect_contains = expected,
207*d6050574SRomain Jobredeaux        meta = self.meta,
208*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
209*d6050574SRomain Jobredeaux            self.actual,
210*d6050574SRomain Jobredeaux            name = self.container_name,
211*d6050574SRomain Jobredeaux            sort = False,  # Don't sort; this might be rendered by the in_order() error.
212*d6050574SRomain Jobredeaux        ),
213*d6050574SRomain Jobredeaux        format_expected = lambda: format_problem_expected_exactly(
214*d6050574SRomain Jobredeaux            [e.desc for e in expected],
215*d6050574SRomain Jobredeaux            sort = False,  # Don't sort; this might be rendered by the in_order() error.
216*d6050574SRomain Jobredeaux        ),
217*d6050574SRomain Jobredeaux        format_missing = lambda missing: format_problem_missing_required_values(
218*d6050574SRomain Jobredeaux            [m.desc for m in missing],
219*d6050574SRomain Jobredeaux            sort = self.sortable,
220*d6050574SRomain Jobredeaux        ),
221*d6050574SRomain Jobredeaux        format_unexpected = lambda unexpected: format_problem_unexpected_values(
222*d6050574SRomain Jobredeaux            unexpected,
223*d6050574SRomain Jobredeaux            sort = self.sortable,
224*d6050574SRomain Jobredeaux        ),
225*d6050574SRomain Jobredeaux        format_out_of_order = format_problem_matched_out_of_order,
226*d6050574SRomain Jobredeaux    )
227*d6050574SRomain Jobredeaux
228*d6050574SRomain Jobredeauxdef _collection_subject_contains_none_of(self, values):
229*d6050574SRomain Jobredeaux    """Asserts the collection contains none of `values`.
230*d6050574SRomain Jobredeaux
231*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_none_of
232*d6050574SRomain Jobredeaux
233*d6050574SRomain Jobredeaux    Args:
234*d6050574SRomain Jobredeaux        self: implicitly added
235*d6050574SRomain Jobredeaux        values: ([`collection`]) values of which none of are allowed to exist.
236*d6050574SRomain Jobredeaux    """
237*d6050574SRomain Jobredeaux    check_contains_none_of(
238*d6050574SRomain Jobredeaux        collection = self.actual,
239*d6050574SRomain Jobredeaux        none_of = values,
240*d6050574SRomain Jobredeaux        meta = self.meta,
241*d6050574SRomain Jobredeaux        sort = self.sortable,
242*d6050574SRomain Jobredeaux    )
243*d6050574SRomain Jobredeaux
244*d6050574SRomain Jobredeauxdef _collection_subject_contains_predicate(self, matcher):
245*d6050574SRomain Jobredeaux    """Asserts that `matcher` matches at least one value.
246*d6050574SRomain Jobredeaux
247*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_predicate
248*d6050574SRomain Jobredeaux
249*d6050574SRomain Jobredeaux    Args:
250*d6050574SRomain Jobredeaux        self: implicitly added.
251*d6050574SRomain Jobredeaux        matcher: ([`Matcher`]) (see `matchers` struct).
252*d6050574SRomain Jobredeaux    """
253*d6050574SRomain Jobredeaux    check_contains_predicate(
254*d6050574SRomain Jobredeaux        self.actual,
255*d6050574SRomain Jobredeaux        matcher = matcher,
256*d6050574SRomain Jobredeaux        format_problem = "expected to contain: {}".format(matcher.desc),
257*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
258*d6050574SRomain Jobredeaux            self.actual,
259*d6050574SRomain Jobredeaux            name = self.container_name,
260*d6050574SRomain Jobredeaux            sort = self.sortable,
261*d6050574SRomain Jobredeaux        ),
262*d6050574SRomain Jobredeaux        meta = self.meta,
263*d6050574SRomain Jobredeaux    )
264*d6050574SRomain Jobredeaux
265*d6050574SRomain Jobredeauxdef _collection_subject_contains_at_least(self, expect_contains):
266*d6050574SRomain Jobredeaux    """Assert that the collection is a subset of the given predicates.
267*d6050574SRomain Jobredeaux
268*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_at_least
269*d6050574SRomain Jobredeaux
270*d6050574SRomain Jobredeaux    The collection must contain all the values. It can contain extra elements.
271*d6050574SRomain Jobredeaux    The multiplicity of values is respected. Checking that the relative order
272*d6050574SRomain Jobredeaux    of matches is the same as the passed-in expected values order can done by
273*d6050574SRomain Jobredeaux    calling `in_order()`.
274*d6050574SRomain Jobredeaux
275*d6050574SRomain Jobredeaux    Args:
276*d6050574SRomain Jobredeaux        self: implicitly added.
277*d6050574SRomain Jobredeaux        expect_contains: ([`list`]) values that must be in the collection.
278*d6050574SRomain Jobredeaux
279*d6050574SRomain Jobredeaux    Returns:
280*d6050574SRomain Jobredeaux        [`Ordered`] (see `_ordered_incorrectly_new`).
281*d6050574SRomain Jobredeaux    """
282*d6050574SRomain Jobredeaux    matchers = [
283*d6050574SRomain Jobredeaux        matching.equals_wrapper(expected)
284*d6050574SRomain Jobredeaux        for expected in to_list(expect_contains)
285*d6050574SRomain Jobredeaux    ]
286*d6050574SRomain Jobredeaux    return self.contains_at_least_predicates(matchers)
287*d6050574SRomain Jobredeaux
288*d6050574SRomain Jobredeauxdef _collection_subject_contains_at_least_predicates(self, matchers):
289*d6050574SRomain Jobredeaux    """Assert that the collection is a subset of the given predicates.
290*d6050574SRomain Jobredeaux
291*d6050574SRomain Jobredeaux    Method: CollectionSubject.contains_at_least_predicates
292*d6050574SRomain Jobredeaux
293*d6050574SRomain Jobredeaux    The collection must match all the predicates. It can contain extra elements.
294*d6050574SRomain Jobredeaux    The multiplicity of matchers is respected. Checking that the relative order
295*d6050574SRomain Jobredeaux    of matches is the same as the passed-in matchers order can done by calling
296*d6050574SRomain Jobredeaux    `in_order()`.
297*d6050574SRomain Jobredeaux
298*d6050574SRomain Jobredeaux    Args:
299*d6050574SRomain Jobredeaux        self: implicitly added.
300*d6050574SRomain Jobredeaux        matchers: ([`list`] of [`Matcher`]) (see `matchers` struct).
301*d6050574SRomain Jobredeaux
302*d6050574SRomain Jobredeaux    Returns:
303*d6050574SRomain Jobredeaux        [`Ordered`] (see `_ordered_incorrectly_new`).
304*d6050574SRomain Jobredeaux    """
305*d6050574SRomain Jobredeaux    ordered = check_contains_at_least_predicates(
306*d6050574SRomain Jobredeaux        self.actual,
307*d6050574SRomain Jobredeaux        matchers,
308*d6050574SRomain Jobredeaux        format_missing = lambda missing: format_problem_predicates_did_not_match(
309*d6050574SRomain Jobredeaux            missing,
310*d6050574SRomain Jobredeaux            element_plural_name = self.element_plural_name,
311*d6050574SRomain Jobredeaux            container_name = self.container_name,
312*d6050574SRomain Jobredeaux        ),
313*d6050574SRomain Jobredeaux        format_out_of_order = format_problem_matched_out_of_order,
314*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
315*d6050574SRomain Jobredeaux            self.actual,
316*d6050574SRomain Jobredeaux            name = self.container_name,
317*d6050574SRomain Jobredeaux            sort = self.sortable,
318*d6050574SRomain Jobredeaux        ),
319*d6050574SRomain Jobredeaux        meta = self.meta,
320*d6050574SRomain Jobredeaux    )
321*d6050574SRomain Jobredeaux    return ordered
322*d6050574SRomain Jobredeaux
323*d6050574SRomain Jobredeauxdef _collection_subject_not_contains(self, value):
324*d6050574SRomain Jobredeaux    check_not_contains_predicate(
325*d6050574SRomain Jobredeaux        self.actual,
326*d6050574SRomain Jobredeaux        matcher = matching.equals_wrapper(value),
327*d6050574SRomain Jobredeaux        meta = self.meta,
328*d6050574SRomain Jobredeaux        sort = self.sortable,
329*d6050574SRomain Jobredeaux    )
330*d6050574SRomain Jobredeaux
331*d6050574SRomain Jobredeauxdef _collection_subject_not_contains_predicate(self, matcher):
332*d6050574SRomain Jobredeaux    """Asserts that `matcher` matches no values in the collection.
333*d6050574SRomain Jobredeaux
334*d6050574SRomain Jobredeaux    Method: CollectionSubject.not_contains_predicate
335*d6050574SRomain Jobredeaux
336*d6050574SRomain Jobredeaux    Args:
337*d6050574SRomain Jobredeaux        self: implicitly added.
338*d6050574SRomain Jobredeaux        matcher: [`Matcher`] object (see `matchers` struct).
339*d6050574SRomain Jobredeaux    """
340*d6050574SRomain Jobredeaux    check_not_contains_predicate(
341*d6050574SRomain Jobredeaux        self.actual,
342*d6050574SRomain Jobredeaux        matcher = matcher,
343*d6050574SRomain Jobredeaux        meta = self.meta,
344*d6050574SRomain Jobredeaux        sort = self.sortable,
345*d6050574SRomain Jobredeaux    )
346*d6050574SRomain Jobredeaux
347*d6050574SRomain Jobredeauxdef _collection_subject_offset(self, offset, factory):
348*d6050574SRomain Jobredeaux    """Fetches an element from the collection as a subject.
349*d6050574SRomain Jobredeaux
350*d6050574SRomain Jobredeaux    Args:
351*d6050574SRomain Jobredeaux        self: implicitly added.
352*d6050574SRomain Jobredeaux        offset: ([`int`]) the offset to fetch
353*d6050574SRomain Jobredeaux        factory: ([`callable`]). The factory function to use to create
354*d6050574SRomain Jobredeaux            the subject for the offset's value. It must have the following
355*d6050574SRomain Jobredeaux            signature: `def factory(value, *, meta)`.
356*d6050574SRomain Jobredeaux
357*d6050574SRomain Jobredeaux    Returns:
358*d6050574SRomain Jobredeaux        Object created by `factory`.
359*d6050574SRomain Jobredeaux    """
360*d6050574SRomain Jobredeaux    value = self.actual[offset]
361*d6050574SRomain Jobredeaux    return factory(
362*d6050574SRomain Jobredeaux        value,
363*d6050574SRomain Jobredeaux        meta = self.meta.derive("offset({})".format(offset)),
364*d6050574SRomain Jobredeaux    )
365*d6050574SRomain Jobredeaux
366*d6050574SRomain Jobredeauxdef _collection_subject_transform(
367*d6050574SRomain Jobredeaux        self,
368*d6050574SRomain Jobredeaux        desc = None,
369*d6050574SRomain Jobredeaux        *,
370*d6050574SRomain Jobredeaux        map_each = None,
371*d6050574SRomain Jobredeaux        loop = None,
372*d6050574SRomain Jobredeaux        filter = None):
373*d6050574SRomain Jobredeaux    """Transforms a collections's value and returns another CollectionSubject.
374*d6050574SRomain Jobredeaux
375*d6050574SRomain Jobredeaux    This is equivalent to applying a list comprehension over the collection values,
376*d6050574SRomain Jobredeaux    but takes care of propagating context information and wrapping the value
377*d6050574SRomain Jobredeaux    in a `CollectionSubject`.
378*d6050574SRomain Jobredeaux
379*d6050574SRomain Jobredeaux    `transform(map_each=M, loop=L, filter=F)` is equivalent to
380*d6050574SRomain Jobredeaux    `[M(v) for v in L(collection) if F(v)]`.
381*d6050574SRomain Jobredeaux
382*d6050574SRomain Jobredeaux    Args:
383*d6050574SRomain Jobredeaux        self: implicitly added.
384*d6050574SRomain Jobredeaux        desc: (optional [`str`]) a human-friendly description of the transform
385*d6050574SRomain Jobredeaux            for use in error messages. Required when a description can't be
386*d6050574SRomain Jobredeaux            inferred from the other args. The description can be inferred if the
387*d6050574SRomain Jobredeaux            filter arg is a named function (non-lambda) or Matcher object.
388*d6050574SRomain Jobredeaux        map_each: (optional [`callable`]) function to transform an element in
389*d6050574SRomain Jobredeaux            the collection. It takes one positional arg, the loop's
390*d6050574SRomain Jobredeaux            current iteration value, and its return value will be the element's
391*d6050574SRomain Jobredeaux            new value. If not specified, the values from the loop iteration are
392*d6050574SRomain Jobredeaux            returned unchanged.
393*d6050574SRomain Jobredeaux        loop: (optional [`callable`]) function to produce values from the
394*d6050574SRomain Jobredeaux            original collection and whose values are iterated over. It takes one
395*d6050574SRomain Jobredeaux            positional arg, which is the original collection. If not specified,
396*d6050574SRomain Jobredeaux            the original collection values are iterated over.
397*d6050574SRomain Jobredeaux        filter: (optional [`callable`]) function that decides what values are
398*d6050574SRomain Jobredeaux            passed onto `map_each` for inclusion in the final result. It takes
399*d6050574SRomain Jobredeaux            one positional arg, the value to match (which is the current
400*d6050574SRomain Jobredeaux            iteration value before `map_each` is applied), and returns a bool
401*d6050574SRomain Jobredeaux            (True if the value should be included in the result, False if it
402*d6050574SRomain Jobredeaux            should be skipped).
403*d6050574SRomain Jobredeaux
404*d6050574SRomain Jobredeaux    Returns:
405*d6050574SRomain Jobredeaux        [`CollectionSubject`] of the transformed values.
406*d6050574SRomain Jobredeaux    """
407*d6050574SRomain Jobredeaux    if not desc:
408*d6050574SRomain Jobredeaux        if map_each or loop:
409*d6050574SRomain Jobredeaux            fail("description required when map_each or loop used")
410*d6050574SRomain Jobredeaux
411*d6050574SRomain Jobredeaux        if matching.is_matcher(filter):
412*d6050574SRomain Jobredeaux            desc = "filter=" + filter.desc
413*d6050574SRomain Jobredeaux        else:
414*d6050574SRomain Jobredeaux            func_name = get_function_name(filter)
415*d6050574SRomain Jobredeaux            if func_name == "lambda":
416*d6050574SRomain Jobredeaux                fail("description required: description cannot be " +
417*d6050574SRomain Jobredeaux                     "inferred from lambdas. Explicitly specify the " +
418*d6050574SRomain Jobredeaux                     "description, use a named function for the filter, " +
419*d6050574SRomain Jobredeaux                     "or use a Matcher for the filter.")
420*d6050574SRomain Jobredeaux            else:
421*d6050574SRomain Jobredeaux                desc = "filter={}(...)".format(func_name)
422*d6050574SRomain Jobredeaux
423*d6050574SRomain Jobredeaux    map_each = map_each or _identity
424*d6050574SRomain Jobredeaux    loop = loop or _identity
425*d6050574SRomain Jobredeaux
426*d6050574SRomain Jobredeaux    if filter:
427*d6050574SRomain Jobredeaux        if matching.is_matcher(filter):
428*d6050574SRomain Jobredeaux            filter_func = filter.match
429*d6050574SRomain Jobredeaux        else:
430*d6050574SRomain Jobredeaux            filter_func = filter
431*d6050574SRomain Jobredeaux    else:
432*d6050574SRomain Jobredeaux        filter_func = _always_true
433*d6050574SRomain Jobredeaux
434*d6050574SRomain Jobredeaux    new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)]
435*d6050574SRomain Jobredeaux
436*d6050574SRomain Jobredeaux    return _collection_subject_new(
437*d6050574SRomain Jobredeaux        new_values,
438*d6050574SRomain Jobredeaux        meta = self.meta.derive(
439*d6050574SRomain Jobredeaux            "transform()",
440*d6050574SRomain Jobredeaux            details = ["transform: {}".format(desc)],
441*d6050574SRomain Jobredeaux        ),
442*d6050574SRomain Jobredeaux        container_name = self.container_name,
443*d6050574SRomain Jobredeaux        sortable = self.sortable,
444*d6050574SRomain Jobredeaux        element_plural_name = self.element_plural_name,
445*d6050574SRomain Jobredeaux    )
446*d6050574SRomain Jobredeaux
447*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs.
448*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions
449*d6050574SRomain JobredeauxCollectionSubject = struct(
450*d6050574SRomain Jobredeaux    # keep sorted start
451*d6050574SRomain Jobredeaux    contains = _collection_subject_contains,
452*d6050574SRomain Jobredeaux    contains_at_least = _collection_subject_contains_at_least,
453*d6050574SRomain Jobredeaux    contains_at_least_predicates = _collection_subject_contains_at_least_predicates,
454*d6050574SRomain Jobredeaux    contains_exactly = _collection_subject_contains_exactly,
455*d6050574SRomain Jobredeaux    contains_exactly_predicates = _collection_subject_contains_exactly_predicates,
456*d6050574SRomain Jobredeaux    contains_none_of = _collection_subject_contains_none_of,
457*d6050574SRomain Jobredeaux    contains_predicate = _collection_subject_contains_predicate,
458*d6050574SRomain Jobredeaux    has_size = _collection_subject_has_size,
459*d6050574SRomain Jobredeaux    new = _collection_subject_new,
460*d6050574SRomain Jobredeaux    not_contains_predicate = _collection_subject_not_contains_predicate,
461*d6050574SRomain Jobredeaux    offset = _collection_subject_offset,
462*d6050574SRomain Jobredeaux    transform = _collection_subject_transform,
463*d6050574SRomain Jobredeaux    # keep sorted end
464*d6050574SRomain Jobredeaux)
465