Description
Summary
Provide a Pythonic interface for creating matching rules.
Motivation
The Pact specification supports matching rules for most aspects of an interaction. These matching rules ensure that the content fit some more customisable constraints, as opposed to a literal value.
Some example of matching rules including matching a particular regex pattern, or ensuring a number is constrained to a range, or simple asserting that an entry is a floating point.
At present, functions such as with_body
or with_matching_rules
only take strings or byte arrays which are passed as-is to the underlying FFI. As a result, while matchers are technically supported, the end user is responsible for writing out the matching rules in a JSON string. This is clearly undesirable.
Proposed Implementation
General Considerations
The proposed changes would apply to all functions that can support matching rules, whether it be the body, headers, metadata, etc. The specific might change slightly based on context, but the general idea should remain the same.
There should also be consistency between using with_body(...)
with a matching rule, and with_matching_rules(...)
. That is, a user should be able to straightforward refactor from one to the other. Similarly, adapting a rule from with_body
to with_header
should be equally straightforward.
Literal Conversion
At present, with_body
consumes a string or a byte array. A dictionary for example is not supported and the end-user must wrap is with a json.dumps
to serialise the data into a JSON string. If someone wants to match a value literally, they should be able to pass the value directly to with_body
and the serialisation should be handled automatically.
There should be support for:
- All base types
- Strings and bytes would need to be handled specifically so as to ensure that their literal value is matched (even if the underlying Pact library is able to parse it).
- All collections to be mapped to the appropriate JSON object or array
- Furthermore, the following should be considered:
- datetime objects to ISO8601 strings
- decimal objects (to what?)
- pathlib objects to strings
- enum objects to strings or integers (or both?)
- dataclasses to JSON objects
Pydantic Support
Values which are subclasses of Pydantic's BaseModel
should be serialised to JSON objects. Support for Pydantic should be opt-in so as to not introduce a new dependency.
Custom Serialisation
Lastly, we should consider whether to support arbitrary classes. For example, it might be useful to inspect a value for any of the following methods:
to_json
/as_json
/json
to_dict
/as_dict
/dict
It might also be worth standardising a __pact__
method which can be used to provide a custom serialisation.
Matching Rules Constructor
When a matching rule is required, we should expose a set of functions which can be used to create and compose rules.
I would suggest the following, but I am open to suggestions:
- Expose a number of simple functions which can be used to create a matching rule on the fly:
regex
,type
,min
,max
,timestamp
,time
,date
, etc.- I have an open question around the use of
like
andeach_like
from the Pact JS. The term 'like' might be a bit ambiguous I wonder if there is a better name.
- Have these functions return a
MatchingRule
object.- It should be transparently supported within the
with_body
function and friends. - It should overload the
&
and|
operators to allow for logical and and or. This would allow for the creation of more complex rules such astype(42) & min(0) & max(100)
.
- It should be transparently supported within the
To avoid polluting the namespace, it might be best to introduce a best practice of importing the module with an alias:
import pact.v3.matchers as match
example = {
"name": match.type("Alice"),
"age:" match.type(42) & match.min(0),
match.regex(r"address_\d+"): match.type(dict) | match.type(str)
}
References
Below are some references as to how Pact JS handles matching rules, and how the Rust library handles them internally.
Rust Library
The main logic for parsing an arbitrary JSON value into a MatchingRule
is in the pact_models
library (rules_from_json
and MatchingRule::from_json
)
fn rules_from_json(attributes: &Map<String, Value>) -> anyhow::Result<Vec<Either<MatchingRule, MatchingReference>>> {
match attributes.get("rules") {
Some(rules) => match rules {
Value::Array(rules) => {
let rules = rules.iter()
.map(|rule| MatchingRule::from_json(rule));
if let Some(err) = rules.clone().find(|rule| rule.is_err()) {
Err(anyhow!("Matching rule configuration is not correct - {}", err.unwrap_err()))
} else {
Ok(rules.map(|rule| Either::Left(rule.unwrap())).collect())
}
}
_ => Err(anyhow!("EachKey matcher config is not valid. Was expected an array but got {}", rules))
}
None => Ok(vec![])
}
}
pub fn MatchingRule::from_json(value: &Value) -> anyhow::Result<MatchingRule> {
match value {
Value::Object(m) => match m.get("match").or_else(|| m.get("pact:matcher:type")) {
Some(match_val) => {
let val = json_to_string(match_val);
MatchingRule::create(val.as_str(), value)
}
None => if let Some(val) = m.get("regex") {
Ok(MatchingRule::Regex(json_to_string(val)))
} else if let Some(val) = json_to_num(m.get("min").cloned()) {
Ok(MatchingRule::MinType(val))
} else if let Some(val) = json_to_num(m.get("max").cloned()) {
Ok(MatchingRule::MaxType(val))
} else if let Some(val) = m.get("timestamp") {
Ok(MatchingRule::Timestamp(json_to_string(val)))
} else if let Some(val) = m.get("time") {
Ok(MatchingRule::Time(json_to_string(val)))
} else if let Some(val) = m.get("date") {
Ok(MatchingRule::Date(json_to_string(val)))
} else {
Err(anyhow!("Matching rule missing 'match' field and unable to guess its type"))
}
},
_ => Err(anyhow!("Matching rule JSON is not an Object")),
}
}
Pact JS
Pact JS has quite a nice API for creating matching rules. The like
and eachLike
functions are particularly useful.
const example = {
name: 'Alice', // Literal match
age: Matchers.integer(42), // Type match
email: Matchers.email(), // Built-in regex
address: like({ // Nested object
street: '123 Main St',
city: 'Springfield'
}),
}
Metadata
Metadata
Assignees
Labels
Type
Projects
Status