Skip to main content

pact_matching/
lib.rs

1//! The `pact_matching` crate provides the core logic to performing matching on HTTP requests
2//! and responses. It implements the [V3 Pact specification](https://github.com/pact-foundation/pact-specification/tree/version-3)
3//! and [V4 Pact specification](https://github.com/pact-foundation/pact-specification/tree/version-4).
4//!
5//! ## To use it
6//!
7//! To use it, add it to your dependencies in your cargo manifest.
8//!
9//! This crate provides three functions: [`match_request`](fn.match_request.html), [`match_response`](fn.match_response.html)
10//! and [`match_message`](fn.match_message.html).
11//! These functions take an expected and actual request, response or message
12//! model from the [`models`)(models/index.html) module, and return a vector of mismatches.
13//!
14//! To compare any incoming request, it first needs to be converted to a [`models::Request`](models/struct.Request.html) and then can be compared. Same for
15//! any response.
16//!
17//! ## Crate features
18//! All features are enabled by default.
19//!
20//! * `datetime`: Enables support of date and time expressions and generators. This will add the `chronos` crate as a dependency.
21//! * `xml`: Enables support for parsing XML documents. This feature will add the `sxd-document` crate as a dependency.
22//! * `plugins`: Enables support for using plugins. This feature will add the `pact-plugin-driver` crate as a dependency.
23//! * `multipart`: Enables support for MIME multipart bodies. This feature will add the `multer` crate as a dependency.
24//!
25//! ## Reading and writing Pact files
26//!
27//! The [`Pact`](models/struct.Pact.html) struct in the [`models`)(models/index.html) module has methods to read and write pact JSON files. It supports all the specification
28//! versions up to V4, but will convert a V1, V1.1 or V2 spec file to a V3 format.
29//!
30//! ## Matching request and response parts
31//!
32//! V3 specification matching is supported for both JSON and XML bodies, headers, query strings and request paths.
33//!
34//! To understand the basic rules of matching, see [Matching Gotchas](https://docs.pact.io/getting_started/matching/gotchas).
35//! For example test cases for matching, see the [Pact Specification Project, version 3](https://github.com/bethesque/pact-specification/tree/version-3).
36//!
37//! By default, Pact will use string equality matching following Postel's Law. This means
38//! that for an actual value to match an expected one, they both must consist of the same
39//! sequence of characters. For collections (basically Maps and Lists), they must have the
40//! same elements that match in the same sequence, with cases where the additional elements
41//! in an actual Map are ignored.
42//!
43//! Matching rules can be defined for both request and response elements based on a pseudo JSON-Path
44//! syntax.
45//!
46//! ### Matching Bodies
47//!
48//! For the most part, matching involves matching request and response bodies in JSON or XML format.
49//! Other formats will either have their own matching rules, or will follow the JSON one.
50//!
51//! #### JSON body matching rules
52//!
53//! Bodies consist of Objects (Maps of Key-Value pairs), Arrays (Lists) and values (Strings, Numbers, true, false, null).
54//! Body matching rules are prefixed with `$`.
55//!
56//! The following method is used to determine if two bodies match:
57//!
58//! 1. If both the actual body and expected body are empty, the bodies match.
59//! 2. If the actual body is non-empty, and the expected body empty, the bodies match.
60//! 3. If the actual body is empty, and the expected body non-empty, the bodies don't match.
61//! 4. Otherwise do a comparison on the contents of the bodies.
62//!
63//! ##### For the body contents comparison:
64//!
65//! 1. If the actual and expected values are both Objects, compare as Maps.
66//! 2. If the actual and expected values are both Arrays, compare as Lists.
67//! 3. If the expected value is an Object, and the actual is not, they don't match.
68//! 4. If the expected value is an Array, and the actual is not, they don't match.
69//! 5. Otherwise, compare the values
70//!
71//! ##### For comparing Maps
72//!
73//! 1. If the actual map is non-empty while the expected is empty, they don't match.
74//! 2. If we allow unexpected keys, and the number of expected keys is greater than the actual keys,
75//! they don't match.
76//! 3. If we don't allow unexpected keys, and the expected and actual maps don't have the
77//! same number of keys, they don't match.
78//! 4. Otherwise, for each expected key and value pair:
79//!     1. if the actual map contains the key, compare the values
80//!     2. otherwise they don't match
81//!
82//! Postel's law governs if we allow unexpected keys or not.
83//!
84//! ##### For comparing lists
85//!
86//! 1. If there is a body matcher defined that matches the path to the list, default
87//! to that matcher and then compare the list contents.
88//! 2. If the expected list is empty and the actual one is not, the lists don't match.
89//! 3. Otherwise
90//!     1. compare the list sizes
91//!     2. compare the list contents
92//!
93//! ###### For comparing list contents
94//!
95//! 1. For each value in the expected list:
96//!     1. If the index of the value is less than the actual list's size, compare the value
97//!        with the actual value at the same index using the method for comparing values.
98//!     2. Otherwise the value doesn't match
99//!
100//! ##### For comparing values
101//!
102//! 1. If there is a matcher defined that matches the path to the value, default to that
103//! matcher
104//! 2. Otherwise compare the values using equality.
105//!
106//! #### XML body matching rules
107//!
108//! Bodies consist of a root element, Elements (Lists with children), Attributes (Maps) and values (Strings).
109//! Body matching rules are prefixed with `$`.
110//!
111//! The following method is used to determine if two bodies match:
112//!
113//! 1. If both the actual body and expected body are empty, the bodies match.
114//! 2. If the actual body is non-empty, and the expected body empty, the bodies match.
115//! 3. If the actual body is empty, and the expected body non-empty, the bodies don't match.
116//! 4. Otherwise do a comparison on the contents of the bodies.
117//!
118//! ##### For the body contents comparison:
119//!
120//! Start by comparing the root element.
121//!
122//! ##### For comparing elements
123//!
124//! 1. If there is a body matcher defined that matches the path to the element, default
125//! to that matcher on the elements name or children.
126//! 2. Otherwise the elements match if they have the same name.
127//!
128//! Then, if there are no mismatches:
129//!
130//! 1. compare the attributes of the element
131//! 2. compare the child elements
132//! 3. compare the text nodes
133//!
134//! ##### For comparing attributes
135//!
136//! Attributes are treated as a map of key-value pairs.
137//!
138//! 1. If the actual map is non-empty while the expected is empty, they don't match.
139//! 2. If we allow unexpected keys, and the number of expected keys is greater than the actual keys,
140//! they don't match.
141//! 3. If we don't allow unexpected keys, and the expected and actual maps don't have the
142//! same number of keys, they don't match.
143//!
144//! Then, for each expected key and value pair:
145//!
146//! 1. if the actual map contains the key, compare the values
147//! 2. otherwise they don't match
148//!
149//! Postel's law governs if we allow unexpected keys or not. Note for matching paths, attribute names are prefixed with an `@`.
150//!
151//! ###### For comparing child elements
152//!
153//! 1. If there is a matcher defined for the path to the child elements, then pad out the expected child elements to have the
154//! same size as the actual child elements.
155//! 2. Otherwise
156//!     1. If the actual children is non-empty while the expected is empty, they don't match.
157//!     2. If we allow unexpected keys, and the number of expected children is greater than the actual children,
158//!     they don't match.
159//!     3. If we don't allow unexpected keys, and the expected and actual children don't have the
160//!     same number of elements, they don't match.
161//!
162//! Then, for each expected and actual element pair, compare them using the rules for comparing elements.
163//!
164//! ##### For comparing text nodes
165//!
166//! Text nodes are combined into a single string and then compared as values.
167//!
168//! 1. If there is a matcher defined that matches the path to the text node (text node paths end with `#text`), default to that
169//! matcher
170//! 2. Otherwise compare the text using equality.
171//!
172//!
173//! ##### For comparing values
174//!
175//! 1. If there is a matcher defined that matches the path to the value, default to that
176//! matcher
177//! 2. Otherwise compare the values using equality.
178//!
179//! ### Matching Paths
180//!
181//! Paths are matched by the following:
182//!
183//! 1. If there is a matcher defined for `path`, default to that matcher.
184//! 2. Otherwise paths are compared as Strings
185//!
186//! ### Matching Queries
187//!
188//! 1. If the actual and expected query strings are empty, they match.
189//! 2. If the actual is not empty while the expected is, they don't match.
190//! 3. If the actual is empty while the expected is not, they don't match.
191//! 4. Otherwise convert both into a Map of keys mapped to a list values, and compare those.
192//!
193//! #### Matching Query Maps
194//!
195//! Query strings are parsed into a Map of keys mapped to lists of values. Key value
196//! pairs can be in any order, but when the same key appears more than once the values
197//! are compared in the order they appear in the query string.
198//!
199//! ### Matching Headers
200//!
201//! 1. Do a case-insensitive sort of the headers by keys
202//! 2. For each expected header in the sorted list:
203//!     1. If the actual headers contain that key, compare the header values
204//!     2. Otherwise the header does not match
205//!
206//! For matching header values:
207//!
208//! 1. If there is a matcher defined for `header.<HEADER_KEY>`, default to that matcher
209//! 2. Otherwise strip all whitespace after commas and compare the resulting strings.
210//!
211//! #### Matching Request Headers
212//!
213//! Request headers are matched by excluding the cookie header.
214//!
215//! #### Matching Request cookies
216//!
217//! If the list of expected cookies contains all the actual cookies, the cookies match.
218//!
219//! ### Matching Status Codes
220//!
221//! Status codes are compared as integer values.
222//!
223//! ### Matching HTTP Methods
224//!
225//! The actual and expected methods are compared as case-insensitive strings.
226//!
227//! ## Matching Rules
228//!
229//! Pact supports extending the matching rules on each type of object (Request or Response) with a `matchingRules` element in the pact file.
230//! 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
231//! rules that corresponds to the path to the item, the comparison will be delegated to the defined matcher. Note that the
232//! matching rules cascade, so a rule can be specified on a value and will apply to all children of that value.
233//!
234//! ## Matcher Path expressions
235//!
236//! Pact does not support the full JSON path expressions, only ones that match the following rules:
237//!
238//! 1. All paths start with a dollar (`$`), representing the root.
239//! 2. All path elements are separated by periods (`.`), except array indices which use square brackets (`[]`).
240//! 3. Path elements represent keys.
241//! 4. A star (`*`) can be used to match all keys of a map or all items of an array (one level only).
242//!
243//! So the expression `$.item1.level[2].id` will match the highlighted item in the following body:
244//!
245//! ```js,ignore
246//! {
247//!   "item1": {
248//!     "level": [
249//!       {
250//!         "id": 100
251//!       },
252//!       {
253//!         "id": 101
254//!       },
255//!       {
256//!         "id": 102 // <---- $.item1.level[2].id
257//!       },
258//!       {
259//!         "id": 103
260//!       }
261//!     ]
262//!   }
263//! }
264//! ```
265//!
266//! while `$.*.level[*].id` will match all the ids of all the levels for all items.
267//!
268//! ### Matcher selection algorithm
269//!
270//! Due to the star notation, there can be multiple matcher paths defined that correspond to an item. The first, most
271//! specific expression is selected by assigning weightings to each path element and taking the product of the weightings.
272//! The matcher with the path with the largest weighting is used.
273//!
274//! * The root node (`$`) is assigned the value 2.
275//! * Any path element that does not match is assigned the value 0.
276//! * Any property name that matches a path element is assigned the value 2.
277//! * Any array index that matches a path element is assigned the value 2.
278//! * Any star (`*`) that matches a property or array index is assigned the value 1.
279//! * Everything else is assigned the value 0.
280//!
281//! So for the body with highlighted item:
282//!
283//! ```js,ignore
284//! {
285//!   "item1": {
286//!     "level": [
287//!       {
288//!         "id": 100
289//!       },
290//!       {
291//!         "id": 101
292//!       },
293//!       {
294//!         "id": 102 // <--- Item under consideration
295//!       },
296//!       {
297//!         "id": 103
298//!       }
299//!     ]
300//!   }
301//! }
302//! ```
303//!
304//! The expressions will have the following weightings:
305//!
306//! | expression | weighting calculation | weighting |
307//! |------------|-----------------------|-----------|
308//! | $ | $(2) | 2 |
309//! | $.item1 | $(2).item1(2) | 4 |
310//! | $.item2 | $(2).item2(0) | 0 |
311//! | $.item1.level | $(2).item1(2).level(2) | 8 |
312//! | $.item1.level\[1\] | $(2).item1(2).level(2)\[1(2)\] | 16 |
313//! | $.item1.level\[1\].id | $(2).item1(2).level(2)\[1(2)\].id(2) | 32 |
314//! | $.item1.level\[1\].name | $(2).item1(2).level(2)\[1(2)\].name(0) | 0 |
315//! | $.item1.level\[2\] | $(2).item1(2).level(2)\[2(0)\] | 0 |
316//! | $.item1.level\[2\].id | $(2).item1(2).level(2)\[2(0)\].id(2) | 0 |
317//! | $.item1.level\[*\].id | $(2).item1(2).level(2)\[*(1)\].id(2) | 16 |
318//! | $.\*.level\[\*\].id | $(2).*(1).level(2)\[*(1)\].id(2) | 8 |
319//!
320//! So for the item with id 102, the matcher with path `$.item1.level\[1\].id` and weighting 32 will be selected.
321//!
322//! ## Supported matchers
323//!
324//! The following matchers are supported:
325//!
326//! | matcher | Spec Version | example configuration | description |
327//! |---------|--------------|-----------------------|-------------|
328//! | Equality | V1 | `{ "match": "equality" }` | This is the default matcher, and relies on the equals operator |
329//! | Regex | V2 | `{ "match": "regex", "regex": "\\d+" }` | This executes a regular expression match against the string representation of a values. |
330//! | Type | V2 | `{ "match": "type" }` | This executes a type based match against the values, that is, they are equal if they are the same type. |
331//! | MinType | V2 | `{ "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. |
332//! | MaxType | V2 | `{ "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. |
333//! | MinMaxType | V2 | `{ "match": "type", "max": 10, "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 and maximum. |
334//! | Include | V3 | `{ "match": "include", "value": "substr" }` | This checks if the string representation of a value contains the substring. |
335//! | Integer | V3 | `{ "match": "integer" }` | This checks if the type of the value is an integer. |
336//! | Decimal | V3 | `{ "match": "decimal" }` | This checks if the type of the value is a number with decimal places. |
337//! | Number | V3 | `{ "match": "number" }` | This checks if the type of the value is a number. |
338//! | Timestamp | V3 | `{ "match": "datetime", "format": "yyyy-MM-dd HH:ss:mm" }` | Matches the string representation of a value against the datetime format |
339//! | Time  | V3 | `{ "match": "time", "format": "HH:ss:mm" }` | Matches the string representation of a value against the time format |
340//! | Date  | V3 | `{ "match": "date", "format": "yyyy-MM-dd" }` | Matches the string representation of a value against the date format |
341//! | Null  | V3 | `{ "match": "null" }` | Match if the value is a null value (this is content specific, for JSON will match a JSON null) |
342//! | Boolean  | V3 | `{ "match": "boolean" }` | Match if the value is a boolean value (booleans and the string values `true` and `false`) |
343//! | ContentType  | V3 | `{ "match": "contentType", "value": "image/jpeg" }` | Match binary data by its content type (magic file check) |
344//! | Values  | V3 | `{ "match": "values" }` | Match the values in a map, ignoring the keys |
345//! | ArrayContains | V4 | `{ "match": "arrayContains", "variants": [...] }` | Checks if all the variants are present in an array. |
346//! | StatusCode | V4 | `{ "match": "statusCode", "status": "success" }` | Matches the response status code. |
347//! | NotEmpty | V4 | `{ "match": "notEmpty" }` | Value must be present and not empty (not null or the empty string) |
348//! | Semver | V4 | `{ "match": "semver" }` | Value must be valid based on the semver specification |
349//! | Semver | V4 | `{ "match": "semver" }` | Value must be valid based on the semver specification |
350//! | EachKey | V4 | `{ "match": "eachKey", "rules": [{"match": "regex", "regex": "\\$(\\.\\w+)+"}], "value": "$.test.one" }` | Allows defining matching rules to apply to the keys in a map |
351//! | EachValue | V4 | `{ "match": "eachValue", "rules": [{"match": "regex", "regex": "\\$(\\.\\w+)+"}], "value": "$.test.one" }` | Allows defining matching rules to apply to the values in a collection. For maps, delgates to the Values matcher. |
352
353#![warn(missing_docs)]
354
355use std::collections::{BTreeSet, HashMap, HashSet};
356use std::fmt::{Debug, Display};
357use std::fmt::Formatter;
358use std::hash::Hash;
359use std::panic::RefUnwindSafe;
360use std::str;
361use std::str::from_utf8;
362
363use ansi_term::*;
364use ansi_term::Colour::*;
365use anyhow::anyhow;
366use bytes::Bytes;
367use itertools::{Either, Itertools};
368use lazy_static::*;
369use maplit::{hashmap, hashset};
370#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use pact_plugin_driver::catalogue_manager::find_content_matcher;
371#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use pact_plugin_driver::plugin_models::PluginInteractionConfig;
372use serde_json::{json, Value};
373#[allow(unused_imports)] use tracing::{debug, error, info, instrument, trace, warn};
374
375use pact_models::bodies::OptionalBody;
376use pact_models::content_types::ContentType;
377use pact_models::generators::{apply_generators, GenerateValue, GeneratorCategory, GeneratorTestMode, VariantMatcher};
378use pact_models::http_parts::HttpPart;
379use pact_models::interaction::Interaction;
380use pact_models::json_utils::json_to_string;
381use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleList};
382use pact_models::pact::Pact;
383use pact_models::PactSpecification;
384use pact_models::path_exp::DocPath;
385use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
386use pact_models::v4::message_parts::MessageContents;
387use pact_models::v4::sync_message::SynchronousMessage;
388
389use crate::engine::{
390  body_mismatches,
391  build_message_plan,
392  build_request_plan,
393  build_response_plan,
394  execute_message_plan,
395  execute_request_plan,
396  execute_response_plan,
397  ExecutionPlan,
398  header_mismatches,
399  metadata_mismatches,
400  method_mismatch,
401  path_mismatch,
402  query_mismatches
403};
404use crate::engine::context::{MatchingConfiguration, PlanMatchingContext};
405use crate::generators::bodies::generators_process_body;
406use crate::generators::DefaultVariantMatcher;
407use crate::headers::{match_header_value, match_headers};
408#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use crate::json::match_json;
409use crate::matchingrules::{DisplayForMismatch, DoMatch, match_values};
410#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use crate::plugin_support::{InteractionPart, setup_plugin_config};
411use crate::query::match_query_maps;
412
413/// Simple macro to convert a string slice to a `String` struct.
414#[macro_export]
415macro_rules! s {
416    ($e:expr) => ($e.to_string())
417}
418
419/// Version of the library
420pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
421
422pub mod json;
423pub mod matchingrules;
424#[cfg(not(target_family = "wasm"))] pub mod metrics;
425pub mod generators;
426pub mod engine;
427
428#[cfg(feature = "xml")] mod xml;
429pub mod binary_utils;
430pub mod headers;
431pub mod query;
432pub mod form_urlencoded;
433mod field_rules;
434use crate::field_rules::FieldMatchScope;
435#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] mod plugin_support;
436#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] mod core_capabilities;
437
438#[cfg(not(feature = "plugins"))]
439#[derive(Clone, Debug, PartialEq)]
440/// Stub for when plugins feature is not enabled
441pub struct PluginInteractionConfig {}
442
443/// Context used to apply matching logic
444pub trait MatchingContext: Debug {
445  /// If there is a matcher defined at the path in this context
446  fn matcher_is_defined(&self, path: &DocPath) -> bool;
447
448  /// Selected the best matcher from the context for the given path
449  fn select_best_matcher(&self, path: &DocPath) -> RuleList;
450
451  /// If there is a type matcher defined at the path in this context
452  fn type_matcher_defined(&self, path: &DocPath) -> bool;
453
454  /// If there is a values matcher defined at the path in this context
455  fn values_matcher_defined(&self, path: &DocPath) -> bool;
456
457  /// If a matcher defined at the path (ignoring parents)
458  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool;
459
460  /// Matches the keys of the expected and actual maps
461  fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>>;
462
463  /// Returns the plugin configuration associated with the context
464  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
465  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig>;
466
467  /// Returns the matching rules for the matching context
468  fn matchers(&self) -> &MatchingRuleCategory;
469
470  /// Configuration to apply when matching with the context
471  fn config(&self) -> DiffConfig;
472
473  /// Clones the current context with the provided matching rules
474  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync>;
475}
476
477#[derive(Debug, Clone)]
478/// Core implementation of a matching context
479pub struct CoreMatchingContext {
480  /// Matching rules that apply when matching with the context
481  pub matchers: MatchingRuleCategory,
482  /// Configuration to apply when matching with the context
483  pub config: DiffConfig,
484  /// Specification version to apply when matching with the context
485  pub matching_spec: PactSpecification,
486  /// Any plugin configuration available for the interaction
487  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
488  pub plugin_configuration: HashMap<String, PluginInteractionConfig>
489}
490
491impl CoreMatchingContext {
492  /// Creates a new context with the given config and matching rules
493  pub fn new(
494    config: DiffConfig,
495    matchers: &MatchingRuleCategory,
496    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
497    plugin_configuration: &HashMap<String, PluginInteractionConfig>
498  ) -> Self {
499    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
500    {
501      CoreMatchingContext {
502        matchers: matchers.clone(),
503        config,
504        plugin_configuration: plugin_configuration.clone(),
505        ..CoreMatchingContext::default()
506      }
507    }
508
509    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
510    {
511      CoreMatchingContext {
512        matchers: matchers.clone(),
513        config,
514        ..CoreMatchingContext::default()
515      }
516    }
517  }
518
519  /// Creates a new empty context with the given config
520  pub fn with_config(config: DiffConfig) -> Self {
521    CoreMatchingContext {
522      config,
523      .. CoreMatchingContext::default()
524    }
525  }
526
527  fn matchers_for_exact_path(&self, path: &DocPath) -> MatchingRuleCategory {
528    match self.matchers.name {
529      Category::HEADER | Category::QUERY => self.matchers.filter(|&(val, _)| {
530        path.len() == 1 && path.first_field() == val.first_field()
531      }),
532      Category::BODY => self.matchers.filter(|&(val, _)| {
533        let p = path.to_vec();
534        let p_slice = p.iter().map(|p| p.as_str()).collect_vec();
535        val.matches_path_exactly(p_slice.as_slice())
536      }),
537      _ => self.matchers.filter(|_| false)
538    }
539  }
540
541  #[allow(dead_code)]
542  pub(crate) fn clone_from(context: &(dyn MatchingContext + Send + Sync)) -> Self {
543    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
544    {
545      CoreMatchingContext {
546        matchers: context.matchers().clone(),
547        config: context.config().clone(),
548        plugin_configuration: context.plugin_configuration().clone(),
549        .. CoreMatchingContext::default()
550      }
551    }
552
553    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
554    {
555      CoreMatchingContext {
556        matchers: context.matchers().clone(),
557        config: context.config().clone(),
558        .. CoreMatchingContext::default()
559      }
560    }
561  }
562}
563
564impl Default for CoreMatchingContext {
565  fn default() -> Self {
566    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
567    {
568      CoreMatchingContext {
569        matchers: Default::default(),
570        config: DiffConfig::AllowUnexpectedKeys,
571        matching_spec: PactSpecification::V3,
572        plugin_configuration: Default::default()
573      }
574    }
575
576    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
577    {
578      CoreMatchingContext {
579        matchers: Default::default(),
580        config: DiffConfig::AllowUnexpectedKeys,
581        matching_spec: PactSpecification::V3
582      }
583    }
584  }
585}
586
587impl MatchingContext for CoreMatchingContext {
588  #[instrument(level = "trace", ret, skip_all, fields(path, matchers = ?self.matchers))]
589  fn matcher_is_defined(&self, path: &DocPath) -> bool {
590    let path = path.to_vec();
591    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
592    self.matchers.matcher_is_defined(path_slice.as_slice())
593  }
594
595  fn select_best_matcher(&self, path: &DocPath) -> RuleList {
596    let path = path.to_vec();
597    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
598    self.matchers.select_best_matcher(path_slice.as_slice())
599  }
600
601  fn type_matcher_defined(&self, path: &DocPath) -> bool {
602    let path = path.to_vec();
603    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
604    self.matchers.resolve_matchers_for_path(path_slice.as_slice()).type_matcher_defined()
605  }
606
607  fn values_matcher_defined(&self, path: &DocPath) -> bool {
608    self.matchers_for_exact_path(path).values_matcher_defined()
609  }
610
611  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
612    let actual = self.matchers_for_exact_path(path);
613    if matchers.is_empty() {
614      actual.is_not_empty()
615    } else {
616      actual.as_rule_list().rules.iter().any(|r| matchers.contains(r.name().as_str()))
617    }
618  }
619
620  fn match_keys(
621    &self,
622    path: &DocPath,
623    expected: &BTreeSet<String>,
624    actual: &BTreeSet<String>
625  ) -> Result<(), Vec<CommonMismatch>> {
626    let mut expected_keys = expected.iter().cloned().collect::<Vec<String>>();
627    expected_keys.sort();
628    let mut actual_keys = actual.iter().cloned().collect::<Vec<String>>();
629    actual_keys.sort();
630    let missing_keys: Vec<String> = expected.iter().filter(|key| !actual.contains(*key)).cloned().collect();
631    let mut result = vec![];
632
633    if !self.direct_matcher_defined(path, &hashset! { "values", "each-value", "each-key" }) {
634      match self.config {
635        DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
636          result.push(CommonMismatch {
637            path: path.to_string(),
638            expected: expected.for_mismatch(),
639            actual: actual.for_mismatch(),
640            description: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
641          });
642        }
643        DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
644          result.push(CommonMismatch {
645            path: path.to_string(),
646            expected: expected.for_mismatch(),
647            actual: actual.for_mismatch(),
648            description: format!("Expected a Map with keys [{}] but received one with keys [{}]",
649                              expected_keys.join(", "), actual_keys.join(", ")),
650          });
651        }
652        _ => {}
653      }
654    }
655
656    if self.direct_matcher_defined(path, &Default::default()) {
657      let matchers = self.select_best_matcher(path);
658      for matcher in matchers.rules {
659        match matcher {
660          MatchingRule::EachKey(definition) => {
661            for sub_matcher in definition.rules {
662              match sub_matcher {
663                Either::Left(rule) => {
664                  for key in &actual_keys {
665                    let key_path = path.join(key);
666                    if let Err(err) = rule.match_value("", key.as_str(), false, false) {
667                      result.push(CommonMismatch {
668                        path: key_path.to_string(),
669                        expected: "".to_string(),
670                        actual: key.clone(),
671                        description: err.to_string(),
672                      });
673                    }
674                  }
675                }
676                Either::Right(name) => {
677                  result.push(CommonMismatch {
678                    path: path.to_string(),
679                    expected: expected.for_mismatch(),
680                    actual: actual.for_mismatch(),
681                    description: format!("Expected a matching rule, found an unresolved reference '{}'",
682                      name.name),
683                  });
684                }
685              }
686            }
687          }
688          _ => {}
689        }
690      }
691    }
692
693    if result.is_empty() {
694      Ok(())
695    } else {
696      Err(result)
697    }
698  }
699
700  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
701  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
702    &self.plugin_configuration
703  }
704
705  fn matchers(&self) -> &MatchingRuleCategory {
706    &self.matchers
707  }
708
709  fn config(&self) -> DiffConfig {
710    self.config
711  }
712
713  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
714    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
715    {
716      Box::new(CoreMatchingContext {
717        matchers: matchers.clone(),
718        config: self.config.clone(),
719        matching_spec: self.matching_spec,
720        plugin_configuration: self.plugin_configuration.clone()
721      })
722    }
723
724    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
725    {
726      Box::new(CoreMatchingContext {
727        matchers: matchers.clone(),
728        config: self.config.clone(),
729        matching_spec: self.matching_spec
730      })
731    }
732  }
733}
734
735#[derive(Debug, Clone, Default)]
736/// Matching context for headers. Keys will be applied in a case-insensitive manor
737pub struct HeaderMatchingContext {
738  inner_context: CoreMatchingContext
739}
740
741impl HeaderMatchingContext {
742  /// Wraps a MatchingContext, downcasing all the matching path keys
743  pub fn new(context: &(dyn MatchingContext + Send + Sync)) -> Self {
744    let matchers = context.matchers();
745
746    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
747    {
748      HeaderMatchingContext {
749        inner_context: CoreMatchingContext::new(
750          context.config(),
751          &MatchingRuleCategory {
752            name: matchers.name.clone(),
753            rules: matchers.rules.iter()
754              .map(|(path, rules)| {
755                (path.to_lower_case(), rules.clone())
756              })
757              .collect()
758          },
759          &context.plugin_configuration()
760        )
761      }
762    }
763
764    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
765    {
766      HeaderMatchingContext {
767        inner_context: CoreMatchingContext::new(
768          context.config(),
769          &MatchingRuleCategory {
770            name: matchers.name.clone(),
771            rules: matchers.rules.iter()
772              .map(|(path, rules)| {
773                (path.to_lower_case(), rules.clone())
774              })
775              .collect()
776          }
777        )
778      }
779    }
780  }
781}
782
783impl MatchingContext for HeaderMatchingContext {
784  fn matcher_is_defined(&self, path: &DocPath) -> bool {
785    self.inner_context.matcher_is_defined(path)
786  }
787
788  fn select_best_matcher(&self, path: &DocPath) -> RuleList {
789    self.inner_context.select_best_matcher(path)
790  }
791
792  fn type_matcher_defined(&self, path: &DocPath) -> bool {
793    self.inner_context.type_matcher_defined(path)
794  }
795
796  fn values_matcher_defined(&self, path: &DocPath) -> bool {
797    self.inner_context.values_matcher_defined(path)
798  }
799
800  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
801    self.inner_context.direct_matcher_defined(path, matchers)
802  }
803
804  fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
805    self.inner_context.match_keys(path, expected, actual)
806  }
807
808  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
809  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
810    self.inner_context.plugin_configuration()
811  }
812
813  fn matchers(&self) -> &MatchingRuleCategory {
814    self.inner_context.matchers()
815  }
816
817  fn config(&self) -> DiffConfig {
818    self.inner_context.config()
819  }
820
821  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
822    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
823    {
824      Box::new(HeaderMatchingContext::new(
825        &CoreMatchingContext {
826          matchers: matchers.clone(),
827          config: self.inner_context.config.clone(),
828          matching_spec: self.inner_context.matching_spec,
829          plugin_configuration: self.inner_context.plugin_configuration.clone()
830        }
831      ))
832    }
833
834    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
835    {
836      Box::new(HeaderMatchingContext::new(
837        &CoreMatchingContext {
838          matchers: matchers.clone(),
839          config: self.inner_context.config.clone(),
840          matching_spec: self.inner_context.matching_spec
841        }
842      ))
843    }
844  }
845}
846
847lazy_static! {
848  static ref BODY_MATCHERS: [
849    (fn(content_type: &ContentType) -> bool,
850    fn(expected: &(dyn HttpPart + Send + Sync), actual: &(dyn HttpPart + Send + Sync), context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>>); 5]
851     = [
852      (|content_type| { content_type.is_json() }, json::match_json),
853      (|content_type| { content_type.is_xml() }, match_xml),
854      (|content_type| { content_type.main_type == "multipart" }, binary_utils::match_mime_multipart),
855      (|content_type| { content_type.base_type() == "application/x-www-form-urlencoded" }, form_urlencoded::match_form_urlencoded),
856      (|content_type| { content_type.is_binary() || content_type.base_type() == "application/octet-stream" }, binary_utils::match_octet_stream)
857  ];
858}
859
860fn match_xml(
861  expected: &(dyn HttpPart + Send + Sync),
862  actual: &(dyn HttpPart + Send + Sync),
863  context: &(dyn MatchingContext + Send + Sync)
864) -> Result<(), Vec<Mismatch>> {
865  #[cfg(feature = "xml")]
866  {
867    xml::match_xml(expected, actual, context)
868  }
869  #[cfg(not(feature = "xml"))]
870  {
871    warn!("Matching XML documents requires the xml feature to be enabled");
872    match_text(&expected.body().value(), &actual.body().value(), context)
873  }
874}
875
876/// Store common mismatch information so it can be converted to different type of mismatches
877#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
878pub struct CommonMismatch {
879  /// path expression to where the mismatch occurred
880  pub path: String,
881  /// expected value (as a string)
882  expected: String,
883  /// actual value (as a string)
884  actual: String,
885  /// Description of the mismatch
886  description: String
887}
888
889impl CommonMismatch {
890  /// Convert common mismatch to body mismatch
891  pub fn to_body_mismatch(&self) -> Mismatch {
892    Mismatch::BodyMismatch {
893      path: self.path.clone(),
894      expected: Some(self.expected.clone().into()),
895      actual: Some(self.actual.clone().into()),
896      mismatch: self.description.clone()
897    }
898  }
899
900  /// Convert common mismatch to query mismatch
901  pub fn to_query_mismatch(&self) -> Mismatch {
902    Mismatch::QueryMismatch {
903      parameter: self.path.clone(),
904      expected: self.expected.clone(),
905      actual: self.actual.clone(),
906      mismatch: self.description.clone()
907    }
908  }
909
910  /// Convert common mismatch to header mismatch
911  pub fn to_header_mismatch(&self) -> Mismatch {
912    Mismatch::HeaderMismatch {
913      key: self.path.clone(),
914      expected: self.expected.clone().into(),
915      actual: self.actual.clone().into(),
916      mismatch: self.description.clone()
917    }
918  }
919}
920
921impl Display for CommonMismatch {
922  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
923    write!(f, "{}", self.description)
924  }
925}
926
927impl PartialEq for CommonMismatch {
928  fn eq(&self, other: &CommonMismatch) -> bool {
929    self.path == other.path && self.expected == other.expected && self.actual == other.actual
930  }
931}
932
933impl From<Mismatch> for CommonMismatch {
934  fn from(value: Mismatch) -> Self {
935    match value {
936      Mismatch::MethodMismatch { expected, actual , mismatch} => CommonMismatch {
937        path: "".to_string(),
938        expected: expected.clone(),
939        actual: actual.clone(),
940        description: mismatch.clone()
941      },
942      Mismatch::PathMismatch { expected, actual, mismatch } => CommonMismatch {
943        path: "".to_string(),
944        expected: expected.clone(),
945        actual: actual.clone(),
946        description: mismatch.clone()
947      },
948      Mismatch::StatusMismatch { expected, actual, mismatch } => CommonMismatch {
949        path: "".to_string(),
950        expected: expected.to_string(),
951        actual: actual.to_string(),
952        description: mismatch.clone()
953      },
954      Mismatch::QueryMismatch { parameter, expected, actual, mismatch } => CommonMismatch {
955        path: parameter.clone(),
956        expected: expected.clone(),
957        actual: actual.clone(),
958        description: mismatch.clone()
959      },
960      Mismatch::HeaderMismatch { key, expected, actual, mismatch } => CommonMismatch {
961        path: key.clone(),
962        expected: expected.clone(),
963        actual: actual.clone(),
964        description: mismatch.clone()
965      },
966      Mismatch::BodyTypeMismatch { expected, actual, mismatch, .. } => CommonMismatch {
967        path: "".to_string(),
968        expected: expected.clone(),
969        actual: actual.clone(),
970        description: mismatch.clone()
971      },
972      Mismatch::BodyMismatch { path, expected, actual, mismatch } => CommonMismatch {
973        path: path.clone(),
974        expected: String::from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(),
975        actual: String::from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(),
976        description: mismatch.clone()
977      },
978      Mismatch::MetadataMismatch { key, expected, actual, mismatch } => CommonMismatch {
979        path: key.clone(),
980        expected: expected.clone(),
981        actual: actual.clone(),
982        description: mismatch.clone()
983      }
984    }
985  }
986}
987
988/// Enum that defines the different types of mismatches that can occur.
989#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
990pub enum Mismatch {
991    /// Request Method mismatch
992    MethodMismatch {
993      /// Expected request method
994      expected: String,
995      /// Actual request method
996      actual: String,
997      /// description of the mismatch
998      mismatch: String
999    },
1000    /// Request Path mismatch
1001    PathMismatch {
1002        /// expected request path
1003        expected: String,
1004        /// actual request path
1005        actual: String,
1006        /// description of the mismatch
1007        mismatch: String
1008    },
1009    /// Response status mismatch
1010    StatusMismatch {
1011        /// expected response status
1012      expected: u16,
1013      /// actual response status
1014      actual: u16,
1015      /// description of the mismatch
1016      mismatch: String
1017    },
1018    /// Request query mismatch
1019    QueryMismatch {
1020        /// query parameter name
1021        parameter: String,
1022        /// expected value
1023        expected: String,
1024        /// actual value
1025        actual: String,
1026        /// description of the mismatch
1027        mismatch: String
1028    },
1029    /// Header mismatch
1030    HeaderMismatch {
1031        /// header key
1032        key: String,
1033        /// expected value
1034        expected: String,
1035        /// actual value
1036        actual: String,
1037        /// description of the mismatch
1038        mismatch: String
1039    },
1040    /// Mismatch in the content type of the body
1041    BodyTypeMismatch {
1042      /// expected content type of the body
1043      expected: String,
1044      /// actual content type of the body
1045      actual: String,
1046      /// description of the mismatch
1047      mismatch: String,
1048      /// expected value
1049      expected_body: Option<Bytes>,
1050      /// actual value
1051      actual_body: Option<Bytes>
1052    },
1053    /// Body element mismatch
1054    BodyMismatch {
1055      /// path expression to where the mismatch occurred
1056      path: String,
1057      /// expected value
1058      expected: Option<Bytes>,
1059      /// actual value
1060      actual: Option<Bytes>,
1061      /// description of the mismatch
1062      mismatch: String
1063    },
1064    /// Message metadata mismatch
1065    MetadataMismatch {
1066      /// key
1067      key: String,
1068      /// expected value
1069      expected: String,
1070      /// actual value
1071      actual: String,
1072      /// description of the mismatch
1073      mismatch: String
1074    }
1075}
1076
1077impl Mismatch {
1078  /// Converts the mismatch to a `Value` struct.
1079  pub fn to_json(&self) -> serde_json::Value {
1080    match self {
1081      Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => {
1082        json!({
1083          "type" : "MethodMismatch",
1084          "expected" : e,
1085          "actual" : a,
1086          "mismatch" : m
1087        })
1088      },
1089      Mismatch::PathMismatch { expected: e, actual: a, mismatch: m } => {
1090        json!({
1091          "type" : "PathMismatch",
1092          "expected" : e,
1093          "actual" : a,
1094          "mismatch" : m
1095        })
1096      },
1097      Mismatch::StatusMismatch { expected: e, actual: a, mismatch: m } => {
1098        json!({
1099          "type" : "StatusMismatch",
1100          "expected" : e,
1101          "actual" : a,
1102          "mismatch": m
1103        })
1104      },
1105      Mismatch::QueryMismatch { parameter: p, expected: e, actual: a, mismatch: m } => {
1106        json!({
1107          "type" : "QueryMismatch",
1108          "parameter" : p,
1109          "expected" : e,
1110          "actual" : a,
1111          "mismatch" : m
1112        })
1113      },
1114      Mismatch::HeaderMismatch { key: k, expected: e, actual: a, mismatch: m } => {
1115        json!({
1116          "type" : "HeaderMismatch",
1117          "key" : k,
1118          "expected" : e,
1119          "actual" : a,
1120          "mismatch" : m
1121        })
1122      },
1123      Mismatch::BodyTypeMismatch {
1124        expected,
1125        actual,
1126        mismatch,
1127        expected_body,
1128        actual_body
1129      } => {
1130        json!({
1131          "type" : "BodyTypeMismatch",
1132          "expected" : expected,
1133          "actual" : actual,
1134          "mismatch" : mismatch,
1135          "expectedBody": match expected_body {
1136            Some(v) => serde_json::Value::String(str::from_utf8(v)
1137              .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1138            None => serde_json::Value::Null
1139          },
1140          "actualBody": match actual_body {
1141            Some(v) => serde_json::Value::String(str::from_utf8(v)
1142              .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1143            None => serde_json::Value::Null
1144          }
1145        })
1146      },
1147      Mismatch::BodyMismatch { path, expected, actual, mismatch } => {
1148        json!({
1149          "type" : "BodyMismatch",
1150          "path" : path,
1151          "expected" : match expected {
1152            Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1153            None => serde_json::Value::Null
1154          },
1155          "actual" : match actual {
1156            Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1157            None => serde_json::Value::Null
1158          },
1159          "mismatch" : mismatch
1160        })
1161      }
1162      Mismatch::MetadataMismatch { key, expected, actual, mismatch } => {
1163        json!({
1164          "type" : "MetadataMismatch",
1165          "key" : key,
1166          "expected" : expected,
1167          "actual" : actual,
1168          "mismatch" : mismatch
1169        })
1170      }
1171    }
1172  }
1173
1174    /// Returns the type of the mismatch as a string
1175    pub fn mismatch_type(&self) -> &str {
1176      match *self {
1177        Mismatch::MethodMismatch { .. } => "MethodMismatch",
1178        Mismatch::PathMismatch { .. } => "PathMismatch",
1179        Mismatch::StatusMismatch { .. } => "StatusMismatch",
1180        Mismatch::QueryMismatch { .. } => "QueryMismatch",
1181        Mismatch::HeaderMismatch { .. } => "HeaderMismatch",
1182        Mismatch::BodyTypeMismatch { .. } => "BodyTypeMismatch",
1183        Mismatch::BodyMismatch { .. } => "BodyMismatch",
1184        Mismatch::MetadataMismatch { .. } => "MetadataMismatch"
1185      }
1186    }
1187
1188    /// Returns a summary string for this mismatch
1189    pub fn summary(&self) -> String {
1190      match *self {
1191        Mismatch::MethodMismatch { expected: ref e, .. } => format!("is a {} request", e),
1192        Mismatch::PathMismatch { expected: ref e, .. } => format!("to path '{}'", e),
1193        Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e),
1194        Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e),
1195        Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e),
1196        Mismatch::BodyTypeMismatch { .. } => "has a matching body".to_string(),
1197        Mismatch::BodyMismatch { .. } => "has a matching body".to_string(),
1198        Mismatch::MetadataMismatch { .. } => "has matching metadata".to_string()
1199      }
1200    }
1201
1202    /// Returns a formatted string for this mismatch
1203    pub fn description(&self) -> String {
1204      match self {
1205        Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => if m.is_empty() {
1206          format!("expected {} but was {}", e, a)
1207        } else {
1208          m.clone()
1209        },
1210        Mismatch::PathMismatch { mismatch, .. } => mismatch.clone(),
1211        Mismatch::StatusMismatch { mismatch, .. } => mismatch.clone(),
1212        Mismatch::QueryMismatch { mismatch, .. } => mismatch.clone(),
1213        Mismatch::HeaderMismatch { mismatch, .. } => mismatch.clone(),
1214        Mismatch::BodyTypeMismatch {  expected: e, actual: a, .. } =>
1215          format!("Expected a body of '{}' but the actual content type was '{}'", e, a),
1216        Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", path, mismatch),
1217        Mismatch::MetadataMismatch { mismatch, .. } => mismatch.clone()
1218      }
1219    }
1220
1221    /// Returns a formatted string with ansi escape codes for this mismatch
1222    pub fn ansi_description(&self) -> String {
1223      match self {
1224        Mismatch::MethodMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.clone()), Green.paint(a.clone())),
1225        Mismatch::PathMismatch { expected: e, actual: a, .. } => format!("expected '{}' but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1226        Mismatch::StatusMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.to_string()), Green.paint(a.to_string())),
1227        Mismatch::QueryMismatch { expected: e, actual: a, parameter: p, .. } => format!("Expected '{}' but received '{}' for query parameter '{}'",
1228          Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())),
1229        Mismatch::HeaderMismatch { expected: e, actual: a, key: k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'",
1230          Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())),
1231        Mismatch::BodyTypeMismatch {  expected: e, actual: a, .. } =>
1232          format!("expected a body of '{}' but the actual content type was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1233        Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch),
1234        Mismatch::MetadataMismatch { expected: e, actual: a, key: k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'",
1235          Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string()))
1236      }
1237    }
1238}
1239
1240impl PartialEq for Mismatch {
1241  fn eq(&self, other: &Mismatch) -> bool {
1242    match (self, other) {
1243      (Mismatch::MethodMismatch { expected: e1, actual: a1, .. },
1244        Mismatch::MethodMismatch { expected: e2, actual: a2, .. }) => {
1245        e1 == e2 && a1 == a2
1246      },
1247      (Mismatch::PathMismatch { expected: e1, actual: a1, .. },
1248        Mismatch::PathMismatch { expected: e2, actual: a2, .. }) => {
1249        e1 == e2 && a1 == a2
1250      },
1251      (Mismatch::StatusMismatch { expected: e1, actual: a1, .. },
1252        Mismatch::StatusMismatch { expected: e2, actual: a2, .. }) => {
1253        e1 == e2 && a1 == a2
1254      },
1255      (Mismatch::BodyTypeMismatch { expected: e1, actual: a1, .. },
1256        Mismatch::BodyTypeMismatch { expected: e2, actual: a2, .. }) => {
1257        e1 == e2 && a1 == a2
1258      },
1259      (Mismatch::QueryMismatch { parameter: p1, expected: e1, actual: a1, .. },
1260        Mismatch::QueryMismatch { parameter: p2, expected: e2, actual: a2, .. }) => {
1261        p1 == p2 && e1 == e2 && a1 == a2
1262      },
1263      (Mismatch::HeaderMismatch { key: p1, expected: e1, actual: a1, .. },
1264        Mismatch::HeaderMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1265        p1 == p2 && e1 == e2 && a1 == a2
1266      },
1267      (Mismatch::BodyMismatch { path: p1, expected: e1, actual: a1, .. },
1268        Mismatch::BodyMismatch { path: p2, expected: e2, actual: a2, .. }) => {
1269        p1 == p2 && e1 == e2 && a1 == a2
1270      },
1271      (Mismatch::MetadataMismatch { key: p1, expected: e1, actual: a1, .. },
1272        Mismatch::MetadataMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1273        p1 == p2 && e1 == e2 && a1 == a2
1274      },
1275      (_, _) => false
1276    }
1277  }
1278}
1279
1280impl Display for Mismatch {
1281  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1282    write!(f, "{}", self.description())
1283  }
1284}
1285
1286fn merge_result<T: Clone>(res1: Result<(), Vec<T>>, res2: Result<(), Vec<T>>) -> Result<(), Vec<T>> {
1287  match (&res1, &res2) {
1288    (Ok(_), Ok(_)) => res1.clone(),
1289    (Err(_), Ok(_)) => res1.clone(),
1290    (Ok(_), Err(_)) => res2.clone(),
1291    (Err(m1), Err(m2)) => {
1292      let mut mismatches = m1.clone();
1293      mismatches.extend_from_slice(&*m2);
1294      Err(mismatches)
1295    }
1296  }
1297}
1298
1299/// Result of matching a request body
1300#[derive(Debug, Default, Clone, PartialEq)]
1301pub enum BodyMatchResult {
1302  /// Matched OK
1303  #[default]
1304  Ok,
1305  /// Mismatch in the content type of the body
1306  BodyTypeMismatch {
1307    /// Expected content type
1308    expected_type: String,
1309    /// Actual content type
1310    actual_type: String,
1311    /// Message
1312    message: String,
1313    /// Expected body
1314    expected: Option<Bytes>,
1315    /// Actual body
1316    actual: Option<Bytes>
1317  },
1318  /// Mismatches with the body contents
1319  BodyMismatches(HashMap<String, Vec<Mismatch>>)
1320}
1321
1322impl BodyMatchResult {
1323  /// Returns all the mismatches
1324  pub fn mismatches(&self) -> Vec<Mismatch> {
1325    match self {
1326      BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
1327        vec![Mismatch::BodyTypeMismatch {
1328          expected: expected_type.clone(),
1329          actual: actual_type.clone(),
1330          mismatch: message.clone(),
1331          expected_body: expected.clone(),
1332          actual_body: actual.clone()
1333        }]
1334      },
1335      BodyMatchResult::BodyMismatches(results) =>
1336        results.values().flatten().cloned().collect(),
1337      _ => vec![]
1338    }
1339  }
1340
1341  /// If all the things matched OK
1342  pub fn all_matched(&self) -> bool {
1343    match self {
1344      BodyMatchResult::BodyTypeMismatch { .. } => false,
1345      BodyMatchResult::BodyMismatches(results) =>
1346        results.values().all(|m| m.is_empty()),
1347      _ => true
1348    }
1349  }
1350}
1351
1352/// Result of matching a request
1353#[derive(Debug, Default, Clone, PartialEq)]
1354pub struct RequestMatchResult {
1355  /// Method match result
1356  pub method: Option<Mismatch>,
1357  /// Path match result
1358  pub path: Option<Vec<Mismatch>>,
1359  /// Body match result
1360  pub body: BodyMatchResult,
1361  /// Query parameter result
1362  pub query: HashMap<String, Vec<Mismatch>>,
1363  /// Headers result
1364  pub headers: HashMap<String, Vec<Mismatch>>
1365}
1366
1367impl RequestMatchResult {
1368  /// Returns all the mismatches
1369  pub fn mismatches(&self) -> Vec<Mismatch> {
1370    let mut m = vec![];
1371
1372    if let Some(ref mismatch) = self.method {
1373      m.push(mismatch.clone());
1374    }
1375    if let Some(ref mismatches) = self.path {
1376      m.extend_from_slice(mismatches.as_slice());
1377    }
1378    for mismatches in self.query.values() {
1379      m.extend_from_slice(mismatches.as_slice());
1380    }
1381    for mismatches in self.headers.values() {
1382      m.extend_from_slice(mismatches.as_slice());
1383    }
1384    m.extend_from_slice(self.body.mismatches().as_slice());
1385
1386    m
1387  }
1388
1389  /// Returns a score based on what was matched
1390  pub fn score(&self) -> i8 {
1391    let mut score = 0;
1392    if self.method.is_none() {
1393      score += 1;
1394    } else {
1395      score -= 1;
1396    }
1397    if self.path.is_none() {
1398      score += 1
1399    } else {
1400      score -= 1
1401    }
1402    for mismatches in self.query.values() {
1403      if mismatches.is_empty() {
1404        score += 1;
1405      } else {
1406        score -= 1;
1407      }
1408    }
1409    for mismatches in self.headers.values() {
1410      if mismatches.is_empty() {
1411        score += 1;
1412      } else {
1413        score -= 1;
1414      }
1415    }
1416    match &self.body {
1417      BodyMatchResult::BodyTypeMismatch { .. } => {
1418        score -= 1;
1419      },
1420      BodyMatchResult::BodyMismatches(results) => {
1421        for mismatches in results.values() {
1422          if mismatches.is_empty() {
1423            score += 1;
1424          } else {
1425            score -= 1;
1426          }
1427        }
1428      },
1429      _ => ()
1430    }
1431    score
1432  }
1433
1434  /// If all the things matched OK
1435  pub fn all_matched(&self) -> bool {
1436    self.method.is_none() && self.path.is_none() &&
1437      self.query.values().all(|m| m.is_empty()) &&
1438      self.headers.values().all(|m| m.is_empty()) &&
1439      self.body.all_matched()
1440  }
1441
1442  /// If there was a mismatch with the method or path
1443  pub fn method_or_path_mismatch(&self) -> bool {
1444    self.method.is_some() || self.path.is_some()
1445  }
1446}
1447
1448impl From<ExecutionPlan> for RequestMatchResult {
1449  fn from(plan: ExecutionPlan) -> Self {
1450    let request = plan.fetch_node(&[":request"]).unwrap_or_default();
1451    let method = method_mismatch(&request);
1452    let path = path_mismatch(&request);
1453    let query = query_mismatches(&request);
1454    let headers = header_mismatches(&request);
1455    let body = body_mismatches(&request);
1456    RequestMatchResult {
1457      method,
1458      path,
1459      body,
1460      query,
1461      headers
1462    }
1463  }
1464}
1465
1466/// Enum that defines the configuration options for performing a match.
1467#[derive(Debug, Clone, Copy, PartialEq)]
1468pub enum DiffConfig {
1469    /// If unexpected keys are allowed and ignored during matching.
1470    AllowUnexpectedKeys,
1471    /// If unexpected keys cause a mismatch.
1472    NoUnexpectedKeys
1473}
1474
1475/// Matches the actual text body to the expected one.
1476pub fn match_text(expected: &Option<Bytes>, actual: &Option<Bytes>, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1477  let path = DocPath::root();
1478  if context.matcher_is_defined(&path) {
1479    let mut mismatches = vec![];
1480    let empty = Bytes::default();
1481    let expected_str = match from_utf8(expected.as_ref().unwrap_or(&empty)) {
1482      Ok(expected) => expected,
1483      Err(err) => {
1484        mismatches.push(Mismatch::BodyMismatch {
1485          path: "$".to_string(),
1486          expected: expected.clone(),
1487          actual: actual.clone(),
1488          mismatch: format!("Could not parse expected value as UTF-8 text: {}", err)
1489        });
1490        ""
1491      }
1492    };
1493    let actual_str = match from_utf8(actual.as_ref().unwrap_or(&empty)) {
1494      Ok(actual) => actual,
1495      Err(err) => {
1496        mismatches.push(Mismatch::BodyMismatch {
1497          path: "$".to_string(),
1498          expected: expected.clone(),
1499          actual: actual.clone(),
1500          mismatch: format!("Could not parse actual value as UTF-8 text: {}", err)
1501        });
1502        ""
1503      }
1504    };
1505    if let Err(messages) = match_values(&path, &context.select_best_matcher(&path), expected_str, actual_str) {
1506      for message in messages {
1507        mismatches.push(Mismatch::BodyMismatch {
1508          path: "$".to_string(),
1509          expected: expected.clone(),
1510          actual: actual.clone(),
1511          mismatch: message.clone()
1512        })
1513      }
1514    };
1515    if mismatches.is_empty() {
1516      Ok(())
1517    } else {
1518      Err(mismatches)
1519    }
1520  } else if expected != actual {
1521    let expected = expected.clone().unwrap_or_default();
1522    let actual = actual.clone().unwrap_or_default();
1523    let e = String::from_utf8_lossy(&expected);
1524    let a = String::from_utf8_lossy(&actual);
1525    let mismatch = format!("Expected body '{}' to match '{}' using equality but did not match", e, a);
1526    Err(vec![
1527      Mismatch::BodyMismatch {
1528        path: "$".to_string(),
1529        expected: Some(expected.clone()),
1530        actual: Some(actual.clone()),
1531        mismatch
1532      }
1533    ])
1534  } else {
1535    Ok(())
1536  }
1537}
1538
1539/// Matches the actual request method to the expected one.
1540pub fn match_method(expected: &str, actual: &str) -> Result<(), Mismatch> {
1541  if expected.to_lowercase() != actual.to_lowercase() {
1542    Err(Mismatch::MethodMismatch { expected: expected.to_string(), actual: actual.to_string(), mismatch: "".to_string() })
1543  } else {
1544    Ok(())
1545  }
1546}
1547
1548/// Matches the actual request path to the expected one.
1549pub fn match_path(expected: &str, actual: &str, context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>> {
1550  let _scope = FieldMatchScope::category("path");
1551  let path = DocPath::empty();
1552  let matcher_result = if context.matcher_is_defined(&path) {
1553    match_values(&path, &context.select_best_matcher(&path), expected.to_string(), actual.to_string())
1554  } else {
1555    MatchingRule::Equality.match_value(expected, actual, false, false)
1556      .map_err(|err| vec![err.to_string()])
1557  };
1558  matcher_result.map_err(|messages| messages.iter().map(|message| {
1559    Mismatch::PathMismatch {
1560      expected: expected.to_string(),
1561      actual: actual.to_string(), mismatch: message.clone()
1562    }
1563  }).collect())
1564}
1565
1566/// Matches the actual query parameters to the expected ones.
1567pub fn match_query(
1568  expected: Option<HashMap<String, Vec<Option<String>>>>,
1569  actual: Option<HashMap<String, Vec<Option<String>>>>,
1570  context: &(dyn MatchingContext + Send + Sync)
1571) -> HashMap<String, Vec<Mismatch>> {
1572  let _scope = FieldMatchScope::category("query");
1573  match (actual, expected) {
1574    (Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
1575    (Some(aqm), None) => aqm.iter().map(|(key, value)| {
1576      let actual_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1577      (key.clone(), vec![Mismatch::QueryMismatch {
1578        parameter: key.clone(),
1579        expected: "".to_string(),
1580        actual: format!("{:?}", actual_value),
1581        mismatch: format!("Unexpected query parameter '{}' received", key)
1582      }])
1583    }).collect(),
1584    (None, Some(eqm)) => eqm.iter().map(|(key, value)| {
1585      let expected_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1586      (key.clone(), vec![Mismatch::QueryMismatch {
1587        parameter: key.clone(),
1588        expected: format!("{:?}", expected_value),
1589        actual: "".to_string(),
1590        mismatch: format!("Expected query parameter '{}' but was missing", key)
1591      }])
1592    }).collect(),
1593    (None, None) => hashmap!{}
1594  }
1595}
1596
1597fn group_by<I, F, K>(items: I, f: F) -> HashMap<K, Vec<I::Item>>
1598  where I: IntoIterator, F: Fn(&I::Item) -> K, K: Eq + Hash {
1599  let mut m = hashmap!{};
1600  for item in items {
1601    let key = f(&item);
1602    let values = m.entry(key).or_insert_with(Vec::new);
1603    values.push(item);
1604  }
1605  m
1606}
1607
1608#[instrument(level = "trace", ret, skip_all)]
1609pub(crate) async fn compare_bodies(
1610  content_type: &ContentType,
1611  expected: &(dyn HttpPart + Send + Sync),
1612  actual: &(dyn HttpPart + Send + Sync),
1613  context: &(dyn MatchingContext + Send + Sync)
1614) -> BodyMatchResult {
1615  let mut mismatches = vec![];
1616
1617  trace!(?content_type, "Comparing bodies");
1618
1619  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1620  {
1621    match find_content_matcher(content_type) {
1622      Some(matcher) => {
1623        debug!("Using content matcher {} for content type '{}'", matcher.catalogue_entry_key(), content_type);
1624        if matcher.is_core() {
1625          if let Err(m) = match matcher.catalogue_entry_key().as_str() {
1626            "core/content-matcher/form-urlencoded" => form_urlencoded::match_form_urlencoded(expected, actual, context),
1627            "core/content-matcher/json" => match_json(expected, actual, context),
1628            "core/content-matcher/multipart-form-data" => binary_utils::match_mime_multipart(expected, actual, context),
1629            "core/content-matcher/text" => match_text(&expected.body().value(), &actual.body().value(), context),
1630            "core/content-matcher/xml" => {
1631              #[cfg(feature = "xml")]
1632              {
1633                xml::match_xml(expected, actual, context)
1634              }
1635              #[cfg(not(feature = "xml"))]
1636              {
1637                warn!("Matching XML bodies requires the xml feature to be enabled");
1638                match_text(&expected.body().value(), &actual.body().value(), context)
1639              }
1640            },
1641            "core/content-matcher/binary" => binary_utils::match_octet_stream(expected, actual, context),
1642            _ => {
1643              warn!("There is no core content matcher for entry {}", matcher.catalogue_entry_key());
1644              match_text(&expected.body().value(), &actual.body().value(), context)
1645            }
1646          } {
1647            mismatches.extend_from_slice(&*m);
1648          }
1649        } else {
1650          trace!(plugin_name = matcher.plugin_name(),"Content matcher is provided via a plugin");
1651          let plugin_config = context.plugin_configuration().get(&matcher.plugin_name()).cloned();
1652          trace!("Plugin config = {:?}", plugin_config);
1653          if pact_plugin_driver::test_context::current_test_run_id().is_none() {
1654            pact_plugin_driver::test_context::set_test_run_id(Some(uuid::Uuid::new_v4().to_string()));
1655          }
1656          if let Err(map) = matcher.match_contents(expected.body(), actual.body(), &context.matchers(),
1657                                                   context.config() == DiffConfig::AllowUnexpectedKeys, plugin_config).await {
1658            // TODO: group the mismatches by key
1659            for (_key, list) in map {
1660              for mismatch in list {
1661                mismatches.push(Mismatch::BodyMismatch {
1662                  path: mismatch.path.clone(),
1663                  expected: Some(Bytes::from(mismatch.expected)),
1664                  actual: Some(Bytes::from(mismatch.actual)),
1665                  mismatch: mismatch.mismatch.clone()
1666                });
1667              }
1668            }
1669          }
1670        }
1671      }
1672      None => {
1673        debug!("No content matcher defined for content type '{}', using core matcher implementation", content_type);
1674        mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1675      }
1676    }
1677  }
1678
1679  #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1680  {
1681    mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1682  }
1683
1684  if mismatches.is_empty() {
1685    BodyMatchResult::Ok
1686  } else {
1687    BodyMatchResult::BodyMismatches(group_by(mismatches, |m| match m {
1688      Mismatch::BodyMismatch { path: m, ..} => m.to_string(),
1689      _ => String::default()
1690    }))
1691  }
1692}
1693
1694fn compare_bodies_core(
1695  content_type: &ContentType,
1696  expected: &(dyn HttpPart + Send + Sync),
1697  actual: &(dyn HttpPart + Send + Sync),
1698  context: &(dyn MatchingContext + Send + Sync)
1699) -> Vec<Mismatch> {
1700  let mut mismatches = vec![];
1701  match BODY_MATCHERS.iter().find(|mt| mt.0(content_type)) {
1702    Some(match_fn) => {
1703      debug!("Using body matcher for content type '{}'", content_type);
1704      if let Err(m) = match_fn.1(expected, actual, context) {
1705        mismatches.extend_from_slice(&*m);
1706      }
1707    },
1708    None => {
1709      debug!("No body matcher defined for content type '{}', checking for a content type matcher", content_type);
1710      let path = DocPath::root();
1711      if context.matcher_is_defined(&path) && context.select_best_matcher(&path).rules
1712        .iter().any(|rule| if let MatchingRule::ContentType(_) = rule { true } else { false }) {
1713        debug!("Found a content type matcher");
1714        if let Err(m) = binary_utils::match_octet_stream(expected, actual, context) {
1715          mismatches.extend_from_slice(&*m);
1716        }
1717      } else {
1718        debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
1719        if let Err(m) = match_text(&expected.body().value(), &actual.body().value(), context) {
1720          mismatches.extend_from_slice(&*m);
1721        }
1722      }
1723    }
1724  };
1725  mismatches
1726}
1727
1728#[instrument(level = "trace", ret, skip_all, fields(%content_type, ?context))]
1729async fn match_body_content(
1730  content_type: &ContentType,
1731  expected: &(dyn HttpPart + Send + Sync),
1732  actual: &(dyn HttpPart + Send + Sync),
1733  context: &(dyn MatchingContext + Send + Sync)
1734) -> BodyMatchResult {
1735  let expected_body = expected.body();
1736  let actual_body = actual.body();
1737  match (expected_body, actual_body) {
1738    (&OptionalBody::Missing, _) => BodyMatchResult::Ok,
1739    (&OptionalBody::Null, &OptionalBody::Present(ref b, _, _)) => {
1740      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1741        mismatch: format!("Expected empty body but received {}", actual_body),
1742        path: s!("/")}]})
1743    },
1744    (&OptionalBody::Empty, &OptionalBody::Present(ref b, _, _)) => {
1745      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1746        mismatch: format!("Expected empty body but received {}", actual_body),
1747        path: s!("/")}]})
1748    },
1749    (&OptionalBody::Null, _) => BodyMatchResult::Ok,
1750    (&OptionalBody::Empty, _) => BodyMatchResult::Ok,
1751    (e, &OptionalBody::Missing) => {
1752      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1753        expected: e.value(),
1754        actual: None,
1755        mismatch: format!("Expected body {} but was missing", e),
1756        path: s!("/")}]})
1757    },
1758    (e, &OptionalBody::Empty) => {
1759      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1760        expected: e.value(),
1761        actual: None,
1762        mismatch: format!("Expected body {} but was empty", e),
1763        path: s!("/")}]})
1764    },
1765    (_, _) => compare_bodies(content_type, expected, actual, context).await
1766  }
1767}
1768
1769/// Matches the actual body to the expected one. This takes into account the content type of each.
1770pub async fn match_body(
1771  expected: &(dyn HttpPart + Send + Sync),
1772  actual: &(dyn HttpPart + Send + Sync),
1773  context: &(dyn MatchingContext + Send + Sync),
1774  header_context: &(dyn MatchingContext + Send + Sync)
1775) -> BodyMatchResult {
1776  let expected_content_type = expected.content_type().unwrap_or_default();
1777  let actual_content_type = actual.content_type().unwrap_or_default();
1778  debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
1779         actual_content_type);
1780  let content_type_matcher = header_context.select_best_matcher(&DocPath::root().join("content-type"));
1781  debug!("content type header matcher = '{:?}'", content_type_matcher);
1782  if expected_content_type.is_unknown() || actual_content_type.is_unknown() ||
1783    expected_content_type.is_equivalent_to(&actual_content_type) ||
1784    expected_content_type.is_equivalent_to(&actual_content_type.base_type()) ||
1785    (!content_type_matcher.is_empty() &&
1786      match_header_value("Content-Type", 0, expected_content_type.to_string().as_str(),
1787                         actual_content_type.to_string().as_str(), header_context, true
1788      ).is_ok()) {
1789    match_body_content(&expected_content_type, expected, actual, context).await
1790  } else if expected.body().is_present() {
1791    BodyMatchResult::BodyTypeMismatch {
1792      expected_type: expected_content_type.to_string(),
1793      actual_type: actual_content_type.to_string(),
1794      message: format!("Expected a body of '{}' but the actual content type was '{}'", expected_content_type,
1795                       actual_content_type),
1796      expected: expected.body().value(),
1797      actual: actual.body().value()
1798    }
1799  } else {
1800    BodyMatchResult::Ok
1801  }
1802}
1803
1804/// Matches the expected and actual requests
1805#[allow(unused_variables)]
1806pub async fn match_request<'a>(
1807  expected: HttpRequest,
1808  actual: HttpRequest,
1809  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1810  interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1811) -> anyhow::Result<RequestMatchResult> {
1812  debug!("comparing to expected {}", expected);
1813  debug!("     body: '{}'", expected.body.display_string());
1814  debug!("     matching_rules:\n{}", expected.matching_rules);
1815  debug!("     generators: {:?}", expected.generators);
1816
1817  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1818    .map(|val| val.to_lowercase() == "v2")
1819    .unwrap_or(false);
1820  if use_v2_engine {
1821    let config = MatchingConfiguration {
1822      allow_unexpected_entries: false,
1823      .. MatchingConfiguration::init_from_env()
1824    };
1825    let mut context = PlanMatchingContext {
1826      pact: pact.as_v4_pact().unwrap_or_default(),
1827      interaction: interaction.as_v4().unwrap(),
1828      matching_rules: Default::default(),
1829      config
1830    };
1831
1832    let plan = build_request_plan(&expected, &mut context)?;
1833    let executed_plan = execute_request_plan(&plan, &actual, &mut context)?;
1834
1835    if config.log_executed_plan {
1836      debug!("config = {:?}", config);
1837      debug!("\n{}", executed_plan.pretty_form());
1838    }
1839    if config.log_plan_summary {
1840      info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1841    }
1842    Ok(executed_plan.into())
1843  } else {
1844    let result;
1845
1846    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1847    {
1848      let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Request);
1849      trace!("plugin_data = {:?}", plugin_data);
1850
1851      let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1852        &expected.matching_rules.rules_for_category("path").unwrap_or_default(),
1853        &plugin_data
1854      );
1855      let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1856        &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1857        &plugin_data
1858      );
1859      let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1860        &expected.matching_rules.rules_for_category("query").unwrap_or_default(),
1861        &plugin_data
1862      );
1863      let header_context = HeaderMatchingContext::new(
1864        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1865          &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1866          &plugin_data
1867        )
1868      );
1869      result = RequestMatchResult {
1870        method: match_method(&expected.method, &actual.method).err(),
1871        path: match_path(&expected.path, &actual.path, &path_context).err(),
1872        body: match_body(&expected, &actual, &body_context, &header_context).await,
1873        query: match_query(expected.query, actual.query, &query_context),
1874        headers: match_headers(expected.headers, actual.headers, &header_context)
1875      };
1876    }
1877
1878    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1879    {
1880      let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1881        &expected.matching_rules.rules_for_category("path").unwrap_or_default()
1882      );
1883      let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1884        &expected.matching_rules.rules_for_category("body").unwrap_or_default()
1885      );
1886      let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1887        &expected.matching_rules.rules_for_category("query").unwrap_or_default()
1888      );
1889      let header_context = HeaderMatchingContext::new(
1890        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1891          &expected.matching_rules.rules_for_category("header").unwrap_or_default()
1892        )
1893      );
1894      result = RequestMatchResult {
1895        method: match_method(&expected.method, &actual.method).err(),
1896        path: match_path(&expected.path, &actual.path, &path_context).err(),
1897        body: match_body(&expected, &actual, &body_context, &header_context).await,
1898        query: match_query(expected.query, actual.query, &query_context),
1899        headers: match_headers(expected.headers, actual.headers, &header_context)
1900      };
1901    }
1902
1903    debug!("--> Mismatches: {:?}", result.mismatches());
1904    Ok(result)
1905  }
1906}
1907
1908/// Matches the actual response status to the expected one.
1909#[instrument(level = "trace")]
1910pub fn match_status(expected: u16, actual: u16, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1911  let _scope = FieldMatchScope::category("status");
1912  let path = DocPath::empty();
1913  let result = if context.matcher_is_defined(&path) {
1914    match_values(&path, &context.select_best_matcher(&path), expected, actual)
1915      .map_err(|messages| messages.iter().map(|message| {
1916        Mismatch::StatusMismatch {
1917          expected,
1918          actual,
1919          mismatch: message.clone()
1920        }
1921      }).collect())
1922  } else if expected != actual {
1923    Err(vec![Mismatch::StatusMismatch {
1924      expected,
1925      actual,
1926      mismatch: format!("expected {} but was {}", expected, actual)
1927    }])
1928  } else {
1929    Ok(())
1930  };
1931  trace!(?result, "matching response status");
1932  result
1933}
1934
1935/// Matches the actual and expected responses.
1936#[allow(unused_variables)]
1937pub async fn match_response<'a>(
1938  expected: HttpResponse,
1939  actual: HttpResponse,
1940  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1941  interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1942) -> anyhow::Result<Vec<Mismatch>> {
1943  let mut mismatches = vec![];
1944
1945  debug!("comparing to expected response: {}", expected);
1946
1947  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1948    .map(|val| val.to_lowercase() == "v2")
1949    .unwrap_or(false);
1950  if use_v2_engine {
1951    let config = MatchingConfiguration {
1952      allow_unexpected_entries: true,
1953      .. MatchingConfiguration::init_from_env()
1954    };
1955    let mut context = PlanMatchingContext {
1956      pact: pact.as_v4_pact().unwrap_or_default(),
1957      interaction: interaction.as_v4().unwrap(),
1958      matching_rules: Default::default(),
1959      config
1960    };
1961
1962    let plan = build_response_plan(&expected, &mut context)?;
1963    let executed_plan = execute_response_plan(&plan, &actual, &mut context)?;
1964
1965    if config.log_executed_plan {
1966      debug!("config = {:?}", config);
1967      debug!("\n{}", executed_plan.pretty_form());
1968    }
1969    if config.log_plan_summary {
1970      info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1971    }
1972    Ok(executed_plan.into())
1973  } else {
1974
1975    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1976    {
1977      let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Response);
1978      trace!("plugin_data = {:?}", plugin_data);
1979
1980      let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1981        &expected.matching_rules.rules_for_category("status").unwrap_or_default(),
1982        &plugin_data);
1983      let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1984        &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1985        &plugin_data);
1986      let header_context = HeaderMatchingContext::new(
1987        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1988          &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1989          &plugin_data
1990        )
1991      );
1992
1993      mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
1994        .mismatches().as_slice());
1995      if let Err(m) = match_status(expected.status, actual.status, &status_context) {
1996        mismatches.extend_from_slice(&m);
1997      }
1998      let result = match_headers(expected.headers, actual.headers,
1999        &header_context);
2000      for values in result.values() {
2001        mismatches.extend_from_slice(values.as_slice());
2002      }
2003    }
2004
2005    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2006    {
2007      let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2008        &expected.matching_rules.rules_for_category("status").unwrap_or_default());
2009      let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2010        &expected.matching_rules.rules_for_category("body").unwrap_or_default());
2011      let header_context = HeaderMatchingContext::new(
2012        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
2013          &expected.matching_rules.rules_for_category("header").unwrap_or_default()
2014        )
2015      );
2016
2017      mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
2018        .mismatches().as_slice());
2019      if let Err(m) = match_status(expected.status, actual.status, &status_context) {
2020        mismatches.extend_from_slice(&m);
2021      }
2022      let result = match_headers(expected.headers, actual.headers,
2023        &header_context);
2024      for values in result.values() {
2025        mismatches.extend_from_slice(values.as_slice());
2026      }
2027    }
2028
2029    trace!(?mismatches, "match response");
2030
2031    Ok(mismatches)
2032  }
2033}
2034
2035/// Matches the actual message contents to the expected one. This takes into account the content type of each.
2036#[instrument(level = "trace")]
2037pub async fn match_message_contents(
2038  expected: &MessageContents,
2039  actual: &MessageContents,
2040  context: &(dyn MatchingContext + Send + Sync)
2041) -> Result<(), Vec<Mismatch>> {
2042  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2043    .map(|val| val.to_lowercase() == "v2")
2044    .unwrap_or(false);
2045  if use_v2_engine {
2046    let config = MatchingConfiguration {
2047      allow_unexpected_entries: true,
2048      show_types_in_errors: true,
2049      .. MatchingConfiguration::init_from_env()
2050    };
2051    let plan_context = PlanMatchingContext {
2052      config,
2053      .. PlanMatchingContext::default()
2054    };
2055    match build_message_plan(expected, &plan_context) {
2056      Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2057        Ok(executed_plan) => {
2058          if config.log_executed_plan {
2059            debug!("config = {:?}", config);
2060            debug!("\n{}", executed_plan.pretty_form());
2061          }
2062          if config.log_plan_summary {
2063            info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2064          }
2065          if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2066            return match body_mismatches(&message_node) {
2067              BodyMatchResult::Ok => Ok(()),
2068              BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected: e, actual: a } => {
2069                Err(vec![Mismatch::BodyTypeMismatch {
2070                  expected: expected_type,
2071                  actual: actual_type,
2072                  mismatch: message,
2073                  expected_body: e,
2074                  actual_body: a
2075                }])
2076              }
2077              BodyMatchResult::BodyMismatches(results) => {
2078                let mismatches: Vec<Mismatch> = results.values()
2079                  .flat_map(|values| values.iter().cloned())
2080                  .collect();
2081                if mismatches.is_empty() { Ok(()) } else { Err(mismatches) }
2082              }
2083            };
2084          }
2085          return Ok(());
2086        }
2087        Err(err) => warn!("Failed to execute message plan: {}", err)
2088      },
2089      Err(err) => warn!("Failed to build message plan: {}", err)
2090    }
2091  }
2092
2093  let expected_content_type = expected.message_content_type().unwrap_or_default();
2094  let actual_content_type = actual.message_content_type().unwrap_or_default();
2095  debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
2096         actual_content_type);
2097  if expected_content_type.is_equivalent_to(&actual_content_type) {
2098    let result = match_body_content(&expected_content_type, expected, actual, context).await;
2099    match result {
2100      BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
2101        Err(vec![ Mismatch::BodyTypeMismatch {
2102          expected: expected_type,
2103          actual: actual_type,
2104          mismatch: message,
2105          expected_body: expected,
2106          actual_body: actual
2107        } ])
2108      },
2109      BodyMatchResult::BodyMismatches(results) => {
2110        Err(results.values().flat_map(|values| values.iter().cloned()).collect())
2111      },
2112      _ => Ok(())
2113    }
2114  } else if expected.contents.is_present() {
2115    Err(vec![ Mismatch::BodyTypeMismatch {
2116      expected: expected_content_type.to_string(),
2117      actual: actual_content_type.to_string(),
2118      mismatch: format!("Expected message with content type {} but was {}",
2119                        expected_content_type, actual_content_type),
2120      expected_body: expected.contents.value(),
2121      actual_body: actual.contents.value()
2122    } ])
2123  } else {
2124    Ok(())
2125  }
2126}
2127
2128/// Matches the actual message metadata to the expected one.
2129#[instrument(level = "trace")]
2130pub fn match_message_metadata(
2131  expected: &MessageContents,
2132  actual: &MessageContents,
2133  context: &dyn MatchingContext
2134) -> HashMap<String, Vec<Mismatch>> {
2135  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2136    .map(|val| val.to_lowercase() == "v2")
2137    .unwrap_or(false);
2138  if use_v2_engine {
2139    let config = MatchingConfiguration {
2140      allow_unexpected_entries: true,
2141      .. MatchingConfiguration::init_from_env()
2142    };
2143    let plan_context = PlanMatchingContext {
2144      config,
2145      .. PlanMatchingContext::default()
2146    };
2147    match build_message_plan(expected, &plan_context) {
2148      Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2149        Ok(executed_plan) => {
2150          if config.log_executed_plan {
2151            debug!("config = {:?}", config);
2152            debug!("\n{}", executed_plan.pretty_form());
2153          }
2154          if config.log_plan_summary {
2155            info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2156          }
2157          if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2158            return metadata_mismatches(&message_node);
2159          }
2160          return hashmap!{};
2161        }
2162        Err(err) => warn!("Failed to execute message plan: {}", err)
2163      },
2164      Err(err) => warn!("Failed to build message plan: {}", err)
2165    }
2166  }
2167
2168  debug!("Matching message metadata");
2169  let mut result = hashmap!{};
2170  let expected_metadata = &expected.metadata;
2171  let actual_metadata = &actual.metadata;
2172  debug!("Matching message metadata. Expected '{:?}', Actual '{:?}'", expected_metadata, actual_metadata);
2173
2174  if !expected_metadata.is_empty() || context.config() == DiffConfig::NoUnexpectedKeys {
2175    for (key, value) in expected_metadata {
2176      match actual_metadata.get(key) {
2177        Some(actual_value) => {
2178          result.insert(key.clone(), match_metadata_value(key, value,
2179            actual_value, context).err().unwrap_or_default());
2180        },
2181        None => {
2182          result.insert(key.clone(), vec![Mismatch::MetadataMismatch { key: key.clone(),
2183            expected: json_to_string(&value),
2184            actual: "".to_string(),
2185            mismatch: format!("Expected message metadata '{}' but was missing", key) }]);
2186        }
2187      }
2188    }
2189  }
2190  result
2191}
2192
2193#[instrument(level = "trace")]
2194fn match_metadata_value(
2195  key: &str,
2196  expected: &Value,
2197  actual: &Value,
2198  context: &dyn MatchingContext
2199) -> Result<(), Vec<Mismatch>> {
2200  debug!("Comparing metadata values for key '{}'", key);
2201  let _scope = FieldMatchScope::category("metadata");
2202  let path = DocPath::root().join(key);
2203  let matcher_result = if context.matcher_is_defined(&path) {
2204    match_values(&path, &context.select_best_matcher(&path), expected, actual)
2205  } else if key.to_ascii_lowercase() == "contenttype" || key.to_ascii_lowercase() == "content-type" {
2206    debug!("Comparing message context type '{}' => '{}'", expected, actual);
2207    headers::match_parameter_header(expected.as_str().unwrap_or_default(), actual.as_str().unwrap_or_default(),
2208      key, "metadata", 0, true)
2209  } else {
2210    MatchingRule::Equality.match_value(expected, actual, false, false).map_err(|err| vec![err.to_string()])
2211  };
2212  matcher_result.map_err(|messages| {
2213    messages.iter().map(|message| {
2214      Mismatch::MetadataMismatch {
2215        key: key.to_string(),
2216        expected: expected.to_string(),
2217        actual: actual.to_string(),
2218        mismatch: format!("Expected metadata key '{}' to have value '{}' but was '{}' - {}", key, expected, actual, message)
2219      }
2220    }).collect()
2221  })
2222}
2223
2224/// Matches the actual and expected messages.
2225#[allow(unused_variables)]
2226pub async fn match_message<'a>(
2227  expected: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2228  actual: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2229  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2230  let mut mismatches = vec![];
2231
2232  if expected.is_message() && actual.is_message() {
2233    debug!("comparing to expected message: {:?}", expected);
2234    let expected_message = expected.as_message().unwrap();
2235    let actual_message = actual.as_message().unwrap();
2236
2237    let matching_rules = &expected_message.matching_rules;
2238
2239    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2240    {
2241      let plugin_data  = setup_plugin_config(pact, expected, InteractionPart::None);
2242
2243      let body_context = if expected.is_v4() {
2244        CoreMatchingContext {
2245          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2246          config: DiffConfig::AllowUnexpectedKeys,
2247          matching_spec: PactSpecification::V4,
2248          plugin_configuration: plugin_data.clone()
2249        }
2250      } else {
2251        CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2252          &matching_rules.rules_for_category("body").unwrap_or_default(),
2253          &plugin_data)
2254      };
2255
2256      let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2257        &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2258        &plugin_data);
2259      let result = match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2260      mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2261      for values in match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2262        mismatches.extend_from_slice(values.as_slice());
2263      }
2264    }
2265
2266    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2267    {
2268      let body_context = if expected.is_v4() {
2269        CoreMatchingContext {
2270          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2271          config: DiffConfig::AllowUnexpectedKeys,
2272          matching_spec: PactSpecification::V4
2273        }
2274      } else {
2275        CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2276          &matching_rules.rules_for_category("body").unwrap_or_default())
2277      };
2278
2279      let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2280        &matching_rules.rules_for_category("metadata").unwrap_or_default());
2281      let result = crate::match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2282      mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2283      for values in crate::match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2284        mismatches.extend_from_slice(values.as_slice());
2285      }
2286    }
2287  } else {
2288    mismatches.push(Mismatch::BodyTypeMismatch {
2289      expected: "message".into(),
2290      actual: actual.type_of(),
2291      mismatch: format!("Cannot compare a {} with a {}", expected.type_of(), actual.type_of()),
2292      expected_body: None,
2293      actual_body: None
2294    });
2295  }
2296
2297  mismatches
2298}
2299
2300/// Matches synchronous request/response messages
2301pub async fn match_sync_message<'a>(expected: SynchronousMessage, actual: SynchronousMessage, pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2302  let mut mismatches = match_sync_message_request(&expected, &actual, pact).await;
2303  let response_result = match_sync_message_response(&expected, &expected.response, &actual.response, pact).await;
2304  mismatches.extend_from_slice(&*response_result);
2305  mismatches
2306}
2307
2308/// Match the request part of a synchronous request/response message
2309#[allow(unused_variables)]
2310pub async fn match_sync_message_request<'a>(
2311  expected: &SynchronousMessage,
2312  actual: &SynchronousMessage,
2313  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2314) -> Vec<Mismatch> {
2315  debug!("comparing to expected message request: {:?}", expected);
2316
2317  let mut mismatches = vec![];
2318  let matching_rules = &expected.request.matching_rules;
2319
2320  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2321  {
2322    let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2323
2324    let body_context = CoreMatchingContext {
2325      matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2326      config: DiffConfig::AllowUnexpectedKeys,
2327      matching_spec: PactSpecification::V4,
2328      plugin_configuration: plugin_data.clone()
2329    };
2330
2331    let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2332      &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2333      &plugin_data);
2334    let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2335
2336    mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2337    for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2338      mismatches.extend_from_slice(values.as_slice());
2339    }
2340  }
2341
2342  #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2343  {
2344    let body_context = CoreMatchingContext {
2345      matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2346      config: DiffConfig::AllowUnexpectedKeys,
2347      matching_spec: PactSpecification::V4
2348    };
2349
2350    let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2351      &matching_rules.rules_for_category("metadata").unwrap_or_default());
2352    let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2353
2354    mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2355    for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2356      mismatches.extend_from_slice(values.as_slice());
2357    }
2358  }
2359
2360  mismatches
2361}
2362
2363/// Match the response part of a synchronous request/response message
2364#[allow(unused_variables)]
2365pub async fn match_sync_message_response<'a>(
2366  expected: &SynchronousMessage,
2367  expected_responses: &[MessageContents],
2368  actual_responses: &[MessageContents],
2369  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2370) -> Vec<Mismatch> {
2371  debug!("comparing to expected message responses: {:?}", expected_responses);
2372
2373  let mut mismatches = vec![];
2374
2375  if expected_responses.len() != actual_responses.len() {
2376    if !expected_responses.is_empty() && actual_responses.is_empty() {
2377      mismatches.push(Mismatch::BodyTypeMismatch {
2378        expected: "message response".into(),
2379        actual: "".into(),
2380        mismatch: "Expected a message with a response, but the actual response was empty".into(),
2381        expected_body: None,
2382        actual_body: None
2383      });
2384    } else if !expected_responses.is_empty() {
2385      mismatches.push(Mismatch::BodyTypeMismatch {
2386        expected: "message response".into(),
2387        actual: "".into(),
2388        mismatch: format!("Expected a message with {} responses, but the actual response had {}",
2389                          expected_responses.len(), actual_responses.len()),
2390        expected_body: None,
2391        actual_body: None
2392      });
2393    }
2394  } else {
2395
2396    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2397    {
2398      let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2399      for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2400        let matching_rules = &expected_response.matching_rules;
2401        let body_context = CoreMatchingContext {
2402          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2403          config: DiffConfig::AllowUnexpectedKeys,
2404          matching_spec: PactSpecification::V4,
2405          plugin_configuration: plugin_data.clone()
2406        };
2407
2408        let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2409          &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2410          &plugin_data);
2411        let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2412
2413        mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2414        for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2415          mismatches.extend_from_slice(values.as_slice());
2416        }
2417      }
2418    }
2419
2420    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2421    {
2422      for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2423        let matching_rules = &expected_response.matching_rules;
2424        let body_context = CoreMatchingContext {
2425          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2426          config: DiffConfig::AllowUnexpectedKeys,
2427          matching_spec: PactSpecification::V4
2428        };
2429
2430        let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2431          &matching_rules.rules_for_category("metadata").unwrap_or_default());
2432        let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2433
2434        mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2435        for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2436          mismatches.extend_from_slice(values.as_slice());
2437        }
2438      }
2439    }
2440  }
2441  mismatches
2442}
2443
2444/// Generates the request by applying any defined generators
2445// TODO: Need to pass in any plugin data
2446#[instrument(level = "trace")]
2447pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpRequest {
2448  trace!(?request, ?mode, ?context, "generate_request");
2449  let mut request = request.clone();
2450
2451  let generators = request.build_generators(&GeneratorCategory::PATH);
2452  if !generators.is_empty() {
2453    debug!("Applying path generator...");
2454    apply_generators(mode, &generators, &mut |_, generator| {
2455      if let Ok(v) = generator.generate_value(&request.path, context, &DefaultVariantMatcher.boxed()) {
2456        request.path = v;
2457      }
2458    });
2459  }
2460
2461  let generators = request.build_generators(&GeneratorCategory::HEADER);
2462  if !generators.is_empty() {
2463    debug!("Applying header generators...");
2464    apply_generators(mode, &generators, &mut |key, generator| {
2465      if let Some(header) = key.first_field() {
2466        if let Some(ref mut headers) = request.headers {
2467          if headers.contains_key(header) {
2468            if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2469              headers.insert(header.to_string(), v);
2470            }
2471          } else {
2472            if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2473              headers.insert(header.to_string(), vec![ v.to_string() ]);
2474            }
2475          }
2476        } else {
2477          if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2478            request.headers = Some(hashmap!{
2479              header.to_string() => vec![ v.to_string() ]
2480            })
2481          }
2482        }
2483      }
2484    });
2485  }
2486
2487  let generators = request.build_generators(&GeneratorCategory::QUERY);
2488  if !generators.is_empty() {
2489    debug!("Applying query generators...");
2490    apply_generators(mode, &generators, &mut |key, generator| {
2491      if let Some(param) = key.first_field() {
2492        if let Some(ref mut parameters) = request.query {
2493          if let Some(parameter) = parameters.get_mut(param) {
2494            let mut generated = parameter.clone();
2495            for (index, val) in parameter.iter().enumerate() {
2496              let value = val.clone().unwrap_or_default();
2497              if let Ok(v) = generator.generate_value(&value, context, &DefaultVariantMatcher.boxed()) {
2498                generated[index] = Some(v);
2499              }
2500            }
2501            *parameter = generated;
2502          } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2503            parameters.insert(param.to_string(), vec![ Some(v.to_string()) ]);
2504          }
2505        } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2506          request.query = Some(hashmap!{
2507            param.to_string() => vec![ Some(v.to_string()) ]
2508          })
2509        }
2510      }
2511    });
2512  }
2513
2514  let generators = request.build_generators(&GeneratorCategory::BODY);
2515  if !generators.is_empty() && request.body.is_present() {
2516    debug!("Applying body generators...");
2517    match generators_process_body(mode, &request.body, request.content_type(),
2518                                  context, &generators, &DefaultVariantMatcher {}, &vec![], &hashmap!{}).await {
2519      Ok(body) => request.body = body,
2520      Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2521    }
2522  }
2523
2524  request
2525}
2526
2527/// Generates the response by applying any defined generators
2528// TODO: Need to pass in any plugin data
2529pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpResponse {
2530  trace!(?response, ?mode, ?context, "generate_response");
2531  let mut response = response.clone();
2532  let generators = response.build_generators(&GeneratorCategory::STATUS);
2533  if !generators.is_empty() {
2534    debug!("Applying status generator...");
2535    apply_generators(mode, &generators, &mut |_, generator| {
2536      if let Ok(v) = generator.generate_value(&response.status, context, &DefaultVariantMatcher.boxed()) {
2537        debug!("Generated value for status: {}", v);
2538        response.status = v;
2539      }
2540    });
2541  }
2542  let generators = response.build_generators(&GeneratorCategory::HEADER);
2543  if !generators.is_empty() {
2544    debug!("Applying header generators...");
2545    apply_generators(mode, &generators, &mut |key, generator| {
2546      if let Some(header) = key.first_field() {
2547        if let Some(ref mut headers) = response.headers {
2548          if headers.contains_key(header) {
2549            if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2550              headers.insert(header.to_string(), v);
2551            }
2552          } else {
2553            if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2554              headers.insert(header.to_string(), vec![ v.to_string() ]);
2555            }
2556          }
2557        } else {
2558          if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2559            response.headers = Some(hashmap!{
2560              header.to_string() => vec![ v.to_string() ]
2561            })
2562          }
2563        }
2564      }
2565    });
2566  }
2567  let generators = response.build_generators(&GeneratorCategory::BODY);
2568  if !generators.is_empty() && response.body.is_present() {
2569    debug!("Applying body generators...");
2570    match generators_process_body(mode, &response.body, response.content_type(),
2571      context, &generators, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await {
2572      Ok(body) => response.body = body,
2573      Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2574    }
2575  }
2576  response
2577}
2578
2579/// Matches the request part of the interaction
2580pub async fn match_interaction_request(
2581  expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2582  actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2583  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2584  _spec_version: &PactSpecification
2585) -> anyhow::Result<RequestMatchResult> {
2586  if let Some(http_interaction) = expected.as_v4_http() {
2587    let request = actual.as_v4_http()
2588      .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2589    match_request(http_interaction.request, request, &pact, &expected).await
2590  } else {
2591    Err(anyhow!("match_interaction_request must be called with HTTP request/response interactions, got {}", expected.type_of()))
2592  }
2593}
2594
2595/// Matches the response part of the interaction
2596pub async fn match_interaction_response(
2597  expected: Box<dyn Interaction + Sync + RefUnwindSafe>,
2598  actual: Box<dyn Interaction + Sync + RefUnwindSafe>,
2599  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2600  _spec_version: &PactSpecification
2601) -> anyhow::Result<Vec<Mismatch>> {
2602  if let Some(expected) = expected.as_v4_http() {
2603    let expected_response = expected.response.clone();
2604    let expected = expected.boxed();
2605    let response = actual.as_v4_http()
2606      .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2607    match_response(expected_response, response, &pact, &expected).await
2608  } else {
2609    Err(anyhow!("match_interaction_response must be called with HTTP request/response interactions, got {}", expected.type_of()))
2610  }
2611}
2612
2613/// Matches an interaction
2614pub async fn match_interaction(
2615  expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2616  actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2617  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2618  _spec_version: &PactSpecification
2619) -> anyhow::Result<Vec<Mismatch>> {
2620  if let Some(expected) = expected.as_v4_http() {
2621    let expected_request = expected.request.clone();
2622    let expected_response = expected.response.clone();
2623    let expected = expected.boxed();
2624    let request = actual.as_v4_http()
2625      .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2626    let request_result = match_request(expected_request, request, &pact, &expected).await?;
2627    let response = actual.as_v4_http()
2628      .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2629    let response_result = match_response(expected_response, response, &pact, &expected).await?;
2630    let mut mismatches = request_result.mismatches();
2631    mismatches.extend_from_slice(&*response_result);
2632    Ok(mismatches)
2633  } else if expected.is_message() || expected.is_v4() {
2634    Ok(match_message(&expected, &actual, &pact).await)
2635  } else {
2636    Err(anyhow!("match_interaction must be called with either an HTTP request/response interaction or a Message, got {}", expected.type_of()))
2637  }
2638}
2639
2640#[cfg(test)]
2641mod tests;
2642#[cfg(test)]
2643mod generator_tests;