@@ -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
75112class 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+
319409class MatchingRuleJSONEncoder (JSONEncoder ):
320410 """
321411 JSON encoder class for matching rules.
0 commit comments