Skip to content

Commit 7be0ff6

Browse files
committed
feat: add 'and' matcher
The `AndMatcher` allows for matchers to be combined. This is achieved in practice through the overloaded `&` operator. Signed-off-by: JP-Ellis <josh@jpellis.me>
1 parent 4bf428c commit 7be0ff6

File tree

1 file changed

+99
-9
lines changed

1 file changed

+99
-9
lines changed

src/pact/match/matcher.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,43 @@ def to_matching_rule(self) -> dict[str, Any]:
7171
The matcher as a matching rule.
7272
"""
7373

74+
def has_value(self) -> bool:
75+
"""
76+
Check if the matcher has a value.
77+
78+
If a value is present, it _must_ be accessible via the `value`
79+
attribute.
80+
81+
Returns:
82+
True if the matcher has a value, otherwise False.
83+
"""
84+
return not isinstance(getattr(self, "value", UNSET), Unset)
85+
86+
def __and__(self, other: object) -> AndMatcher[Any]:
87+
"""
88+
Combine two matchers using a logical AND.
89+
90+
This allows for combining multiple matchers into a single matcher that
91+
requires all conditions to be met.
92+
93+
Only a single example value is supported when combining matchers. The
94+
first value found will be used.
95+
96+
Args:
97+
other:
98+
The other matcher to combine with.
99+
100+
Returns:
101+
An `AndMatcher` that combines both matchers.
102+
"""
103+
if isinstance(self, AndMatcher) and isinstance(other, AbstractMatcher):
104+
return AndMatcher(*self._matchers, other) # type: ignore[attr-defined]
105+
if isinstance(other, AndMatcher):
106+
return AndMatcher(self, *other._matchers) # type: ignore[attr-defined]
107+
if isinstance(other, AbstractMatcher):
108+
return AndMatcher(self, other)
109+
return NotImplemented
110+
74111

75112
class GenericMatcher(AbstractMatcher[_T_co]):
76113
"""
@@ -134,15 +171,6 @@ def __init__(
134171
chain((extra_fields or {}).items(), kwargs.items())
135172
)
136173

137-
def has_value(self) -> bool:
138-
"""
139-
Check if the matcher has a value.
140-
141-
Returns:
142-
True if the matcher has a value, otherwise False.
143-
"""
144-
return not isinstance(self.value, Unset)
145-
146174
def to_integration_json(self) -> dict[str, Any]:
147175
"""
148176
Convert the matcher to an integration JSON object.
@@ -316,6 +344,68 @@ def to_matching_rule(self) -> dict[str, Any]:
316344
return self._matcher.to_matching_rule()
317345

318346

347+
class AndMatcher(AbstractMatcher[_T_co]):
348+
"""
349+
And matcher.
350+
351+
A matcher that combines multiple matchers using a logical AND.
352+
"""
353+
354+
def __init__(
355+
self,
356+
*matchers: AbstractMatcher[Any],
357+
value: _T_co | Unset = UNSET,
358+
) -> None:
359+
"""
360+
Initialize the matcher.
361+
362+
It is best practice to provide a value. This may be set when creating
363+
the `AndMatcher`, or it may be inferred from one of the constituent
364+
matchers. In the latter case, the value from the first matcher that has
365+
a value will be used.
366+
367+
Args:
368+
matchers:
369+
List of matchers to combine.
370+
371+
value:
372+
Example value to match against. If not provided, the value
373+
from the first matcher that has a value will be used.
374+
"""
375+
self._matchers = matchers
376+
self._value: _T_co | Unset = value
377+
378+
if isinstance(self._value, Unset):
379+
for matcher in matchers:
380+
if matcher.has_value():
381+
# If `has_value` is true, `value` must be present
382+
self._value = matcher.value # type: ignore[attr-defined]
383+
break
384+
385+
def to_integration_json(self) -> dict[str, Any]:
386+
"""
387+
Convert the matcher to an integration JSON object.
388+
389+
See
390+
[`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json]
391+
for more information.
392+
"""
393+
return {"pact:matcher:type": [m.to_integration_json() for m in self._matchers]}
394+
395+
def to_matching_rule(self) -> dict[str, Any]:
396+
"""
397+
Convert the matcher to a matching rule.
398+
399+
See
400+
[`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule]
401+
for more information.
402+
"""
403+
return {
404+
"combine": "AND",
405+
"matchers": [m.to_matching_rule() for m in self._matchers],
406+
}
407+
408+
319409
class MatchingRuleJSONEncoder(JSONEncoder):
320410
"""
321411
JSON encoder class for matching rules.

0 commit comments

Comments
 (0)