Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 99 additions & 9 deletions src/pact/match/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,43 @@ def to_matching_rule(self) -> dict[str, Any]:
The matcher as a matching rule.
"""

def has_value(self) -> bool:
"""
Check if the matcher has a value.

If a value is present, it _must_ be accessible via the `value`
attribute.

Returns:
True if the matcher has a value, otherwise False.
"""
return not isinstance(getattr(self, "value", UNSET), Unset)

def __and__(self, other: object) -> AndMatcher[Any]:
"""
Combine two matchers using a logical AND.

This allows for combining multiple matchers into a single matcher that
requires all conditions to be met.

Only a single example value is supported when combining matchers. The
first value found will be used.

Args:
other:
The other matcher to combine with.

Returns:
An `AndMatcher` that combines both matchers.
"""
if isinstance(self, AndMatcher) and isinstance(other, AbstractMatcher):
return AndMatcher(*self._matchers, other) # type: ignore[attr-defined]
if isinstance(other, AndMatcher):
return AndMatcher(self, *other._matchers) # type: ignore[attr-defined]
if isinstance(other, AbstractMatcher):
return AndMatcher(self, other)
return NotImplemented


class GenericMatcher(AbstractMatcher[_T_co]):
"""
Expand Down Expand Up @@ -134,15 +171,6 @@ def __init__(
chain((extra_fields or {}).items(), kwargs.items())
)

def has_value(self) -> bool:
"""
Check if the matcher has a value.

Returns:
True if the matcher has a value, otherwise False.
"""
return not isinstance(self.value, Unset)

def to_integration_json(self) -> dict[str, Any]:
"""
Convert the matcher to an integration JSON object.
Expand Down Expand Up @@ -316,6 +344,68 @@ def to_matching_rule(self) -> dict[str, Any]:
return self._matcher.to_matching_rule()


class AndMatcher(AbstractMatcher[_T_co]):
"""
And matcher.

A matcher that combines multiple matchers using a logical AND.
"""

def __init__(
self,
*matchers: AbstractMatcher[Any],
value: _T_co | Unset = UNSET,
) -> None:
"""
Initialize the matcher.

It is best practice to provide a value. This may be set when creating
the `AndMatcher`, or it may be inferred from one of the constituent
matchers. In the latter case, the value from the first matcher that has
a value will be used.

Args:
matchers:
List of matchers to combine.

value:
Example value to match against. If not provided, the value
from the first matcher that has a value will be used.
"""
self._matchers = matchers
self._value: _T_co | Unset = value

if isinstance(self._value, Unset):
for matcher in matchers:
if matcher.has_value():
# If `has_value` is true, `value` must be present
self._value = matcher.value # type: ignore[attr-defined]
break

def to_integration_json(self) -> dict[str, Any]:
"""
Convert the matcher to an integration JSON object.

See
[`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json]
for more information.
"""
return {"pact:matcher:type": [m.to_integration_json() for m in self._matchers]}

def to_matching_rule(self) -> dict[str, Any]:
"""
Convert the matcher to a matching rule.

See
[`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule]
for more information.
"""
return {
"combine": "AND",
"matchers": [m.to_matching_rule() for m in self._matchers],
}


class MatchingRuleJSONEncoder(JSONEncoder):
"""
JSON encoder class for matching rules.
Expand Down
Loading