This library implements the core matching logic required for matching HTTP requests and responses. It is based on the V2 pact specification.
To use it, add it to your dependencies in your cargo manifest and add an extern crate definition for it.
toml
[dependencies]
pact_matching = "0.2.0"
rust
extern crate pact_matching;
This crate provides two functions: match_request
and match_response
. These functions take an expected and actual request or response
model from the models
module, and return a vector of mismatches.
To compare any incoming request, it first needs to be converted to a models::Request
and then can be compared. Same for
any response.
The Pact
struct in the models
module has methods to read and write pact JSON files. It supports all the specification
versions up to V2, but will converted a V1 and V1.1 spec file to a V2 format.
V2 specification matching is supported for both JSON and XML bodies, headers, query strings and request paths.
To understand the basic rules of matching, see Matching Gotchas. For example test cases for matching, see the Pact Specification Project, version 2.
By default, Pact will use string equality matching following Postel's Law. This means that for an actual value to match an expected one, they both must consist of the same sequence of characters. For collections (basically Maps and Lists), they must have the same elements that match in the same sequence, with cases where the additional elements in an actual Map are ignored.
Matching rules can be defined for both request and response elements based on a pseudo JSON-Path syntax.
For the most part, matching involves matching request and response bodies in JSON or XML format. Other formats will either have their own matching rules, or will follow the JSON one.
Bodies consist of Objects (Maps of Key-Value pairs), Arrays (Lists) and values (Strings, Numbers, true, false, null).
Body matching rules are prefixed with $.body
.
The following method is used to determine if two bodies match:
Postel's law governs if we allow unexpected keys or not.
Bodies consist of a root element, Elements (Lists with children), Attributes (Maps) and values (Strings).
Body matching rules are prefixed with $.body
.
The following method is used to determine if two bodies match:
Start by comparing the root element.
Then, if there are no mismatches:
Attributes are treated as a map of key-value pairs.
Then, for each expected key and value pair:
Postel's law governs if we allow unexpected keys or not. Note for matching paths, attribute names are prefixed with an @
.
Then, for each expected and actual element pair, compare them using the rules for comparing elements.
Text nodes are combined into a single string and then compared as values.
#text
), default to that
matcherPaths are matched by the following:
$.path
, default to that matcher.Query strings are parsed into a Map of keys mapped to lists of values. Key value pairs can be in any order, but when the same key appears more than once the values are compared in the order they appear in the query string.
For matching header values:
$.header.<HEADER_KEY>
, default to that matcherRequest headers are matched by excluding the cookie header.
If the list of expected cookies contains all the actual cookies, the cookies match.
Status codes are compared as integer values.
The actual and expected methods are compared as case-insensitive strings.
Pact supports extending the matching rules on each type of object (Request or Response) with a matchingRules
element in the pact file.
This is a map of JSON path strings to a matcher. When an item is being compared, if there is an entry in the matching
rules that corresponds to the path to the item, the comparison will be delegated to the defined matcher. Note that the
matching rules cascade, so a rule can be specified on a value and will apply to all children of that value.
Pact does not support the full JSON path expressions, only ones that match the following rules:
$
), representing the root..
), except array indices which use square brackets ([]
).$.body
or $.header
).*
) can be used to match all keys of a map or all items of an array (one level only).So the expression $.body.item1.level[2].id
will match the highlighted item in the following body:
js
{
"item1": {
"level": [
{
"id": 100
},
{
"id": 101
},
{
"id": 102 // <---- $.body.item1.level[2].id
},
{
"id": 103
}
]
}
}
while $.body.*.level[*].id
will match all the ids of all the levels for all items.
Due to the star notation, there can be multiple matcher paths defined that correspond to an item. The first, most specific expression is selected by assigning weightings to each path element and taking the product of the weightings. The matcher with the path with the largest weighting is used.
$
) is assigned the value 2.*
) that matches a property or array index is assigned the value 1.So for the body with highlighted item:
js
{
"item1": {
"level": [
{
"id": 100
},
{
"id": 101
},
{
"id": 102 // <--- Item under consideration
},
{
"id": 103
}
]
}
}
The expressions will have the following weightings:
| expression | weighting calculation | weighting | |------------|-----------------------|-----------| | $ | $(2) | 2 | | $.body | $(2).body(2) | 4 | | $.body.item1 | $(2).body(2).item1(2) | 8 | | $.body.item2 | $(2).body(2).item2(0) | 0 | | $.header.item1 | $(2).header(0).item1(2) | 0 | | $.body.item1.level | $(2).body(2).item1(2).level(2) | 16 | | $.body.item1.level[1] | $(2).body(2).item1(2).level(2)[1(2)] | 32 | | $.body.item1.level[1].id | $(2).body(2).item1(2).level(2)[1(2)].id(2) | 64 | | $.body.item1.level[1].name | $(2).body(2).item1(2).level(2)[1(2)].name(0) | 0 | | $.body.item1.level[2] | $(2).body(2).item1(2).level(2)[2(0)] | 0 | | $.body.item1.level[2].id | $(2).body(2).item1(2).level(2)[2(0)].id(2) | 0 | | $.body.item1.level[].id | $(2).body(2).item1(2).level(2)[(1)].id(2) | 32 | | $.body.*.level[*].id | $(2).body(2).(1).level(2)[(1)].id(2) | 8 |
So for the item with id 102, the matcher with path $.body.item1.level[1].id
and weighting 64 will be selected.
The following matchers are supported:
| matcher | example configuration | description |
|---------|-----------------------|-------------|
| Equality | { "match": "equality" }
| This is the default matcher, and relies on the equals operator |
| Regex | { "match": "regex", "regex": "\\d+" }
| This executes a regular expression match against the string representation of a values. |
| Type | { "match": "type" }
| This executes a type based match against the values, that is, they are equal if they are the same type. |
| MinType | { "match": "type", "min": 2 }
| This executes a type based match against the values, that is, they are equal if they are the same type. In addition, if the values represent a collection, the length of the actual value is compared against the minimum. |
| MaxType | { "match": "type", "max": 10 }
| This executes a type based match against the values, that is, they are equal if they are the same type. In addition, if the values represent a collection, the length of the actual value is compared against the maximum. |