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;
433#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] mod plugin_support;
434
435#[cfg(not(feature = "plugins"))]
436#[derive(Clone, Debug, PartialEq)]
437/// Stub for when plugins feature is not enabled
438pub struct PluginInteractionConfig {}
439
440/// Context used to apply matching logic
441pub trait MatchingContext: Debug {
442  /// If there is a matcher defined at the path in this context
443  fn matcher_is_defined(&self, path: &DocPath) -> bool;
444
445  /// Selected the best matcher from the context for the given path
446  fn select_best_matcher(&self, path: &DocPath) -> RuleList;
447
448  /// If there is a type matcher defined at the path in this context
449  fn type_matcher_defined(&self, path: &DocPath) -> bool;
450
451  /// If there is a values matcher defined at the path in this context
452  fn values_matcher_defined(&self, path: &DocPath) -> bool;
453
454  /// If a matcher defined at the path (ignoring parents)
455  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool;
456
457  /// Matches the keys of the expected and actual maps
458  fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>>;
459
460  /// Returns the plugin configuration associated with the context
461  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
462  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig>;
463
464  /// Returns the matching rules for the matching context
465  fn matchers(&self) -> &MatchingRuleCategory;
466
467  /// Configuration to apply when matching with the context
468  fn config(&self) -> DiffConfig;
469
470  /// Clones the current context with the provided matching rules
471  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync>;
472}
473
474#[derive(Debug, Clone)]
475/// Core implementation of a matching context
476pub struct CoreMatchingContext {
477  /// Matching rules that apply when matching with the context
478  pub matchers: MatchingRuleCategory,
479  /// Configuration to apply when matching with the context
480  pub config: DiffConfig,
481  /// Specification version to apply when matching with the context
482  pub matching_spec: PactSpecification,
483  /// Any plugin configuration available for the interaction
484  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
485  pub plugin_configuration: HashMap<String, PluginInteractionConfig>
486}
487
488impl CoreMatchingContext {
489  /// Creates a new context with the given config and matching rules
490  pub fn new(
491    config: DiffConfig,
492    matchers: &MatchingRuleCategory,
493    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
494    plugin_configuration: &HashMap<String, PluginInteractionConfig>
495  ) -> Self {
496    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
497    {
498      CoreMatchingContext {
499        matchers: matchers.clone(),
500        config,
501        plugin_configuration: plugin_configuration.clone(),
502        ..CoreMatchingContext::default()
503      }
504    }
505
506    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
507    {
508      CoreMatchingContext {
509        matchers: matchers.clone(),
510        config,
511        ..CoreMatchingContext::default()
512      }
513    }
514  }
515
516  /// Creates a new empty context with the given config
517  pub fn with_config(config: DiffConfig) -> Self {
518    CoreMatchingContext {
519      config,
520      .. CoreMatchingContext::default()
521    }
522  }
523
524  fn matchers_for_exact_path(&self, path: &DocPath) -> MatchingRuleCategory {
525    match self.matchers.name {
526      Category::HEADER | Category::QUERY => self.matchers.filter(|&(val, _)| {
527        path.len() == 1 && path.first_field() == val.first_field()
528      }),
529      Category::BODY => self.matchers.filter(|&(val, _)| {
530        let p = path.to_vec();
531        let p_slice = p.iter().map(|p| p.as_str()).collect_vec();
532        val.matches_path_exactly(p_slice.as_slice())
533      }),
534      _ => self.matchers.filter(|_| false)
535    }
536  }
537
538  #[allow(dead_code)]
539  pub(crate) fn clone_from(context: &(dyn MatchingContext + Send + Sync)) -> Self {
540    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
541    {
542      CoreMatchingContext {
543        matchers: context.matchers().clone(),
544        config: context.config().clone(),
545        plugin_configuration: context.plugin_configuration().clone(),
546        .. CoreMatchingContext::default()
547      }
548    }
549
550    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
551    {
552      CoreMatchingContext {
553        matchers: context.matchers().clone(),
554        config: context.config().clone(),
555        .. CoreMatchingContext::default()
556      }
557    }
558  }
559}
560
561impl Default for CoreMatchingContext {
562  fn default() -> Self {
563    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
564    {
565      CoreMatchingContext {
566        matchers: Default::default(),
567        config: DiffConfig::AllowUnexpectedKeys,
568        matching_spec: PactSpecification::V3,
569        plugin_configuration: Default::default()
570      }
571    }
572
573    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
574    {
575      CoreMatchingContext {
576        matchers: Default::default(),
577        config: DiffConfig::AllowUnexpectedKeys,
578        matching_spec: PactSpecification::V3
579      }
580    }
581  }
582}
583
584impl MatchingContext for CoreMatchingContext {
585  #[instrument(level = "trace", ret, skip_all, fields(path, matchers = ?self.matchers))]
586  fn matcher_is_defined(&self, path: &DocPath) -> bool {
587    let path = path.to_vec();
588    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
589    self.matchers.matcher_is_defined(path_slice.as_slice())
590  }
591
592  fn select_best_matcher(&self, path: &DocPath) -> RuleList {
593    let path = path.to_vec();
594    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
595    self.matchers.select_best_matcher(path_slice.as_slice())
596  }
597
598  fn type_matcher_defined(&self, path: &DocPath) -> bool {
599    let path = path.to_vec();
600    let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
601    self.matchers.resolve_matchers_for_path(path_slice.as_slice()).type_matcher_defined()
602  }
603
604  fn values_matcher_defined(&self, path: &DocPath) -> bool {
605    self.matchers_for_exact_path(path).values_matcher_defined()
606  }
607
608  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
609    let actual = self.matchers_for_exact_path(path);
610    if matchers.is_empty() {
611      actual.is_not_empty()
612    } else {
613      actual.as_rule_list().rules.iter().any(|r| matchers.contains(r.name().as_str()))
614    }
615  }
616
617  fn match_keys(
618    &self,
619    path: &DocPath,
620    expected: &BTreeSet<String>,
621    actual: &BTreeSet<String>
622  ) -> Result<(), Vec<CommonMismatch>> {
623    let mut expected_keys = expected.iter().cloned().collect::<Vec<String>>();
624    expected_keys.sort();
625    let mut actual_keys = actual.iter().cloned().collect::<Vec<String>>();
626    actual_keys.sort();
627    let missing_keys: Vec<String> = expected.iter().filter(|key| !actual.contains(*key)).cloned().collect();
628    let mut result = vec![];
629
630    if !self.direct_matcher_defined(path, &hashset! { "values", "each-value", "each-key" }) {
631      match self.config {
632        DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
633          result.push(CommonMismatch {
634            path: path.to_string(),
635            expected: expected.for_mismatch(),
636            actual: actual.for_mismatch(),
637            description: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
638          });
639        }
640        DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
641          result.push(CommonMismatch {
642            path: path.to_string(),
643            expected: expected.for_mismatch(),
644            actual: actual.for_mismatch(),
645            description: format!("Expected a Map with keys [{}] but received one with keys [{}]",
646                              expected_keys.join(", "), actual_keys.join(", ")),
647          });
648        }
649        _ => {}
650      }
651    }
652
653    if self.direct_matcher_defined(path, &Default::default()) {
654      let matchers = self.select_best_matcher(path);
655      for matcher in matchers.rules {
656        match matcher {
657          MatchingRule::EachKey(definition) => {
658            for sub_matcher in definition.rules {
659              match sub_matcher {
660                Either::Left(rule) => {
661                  for key in &actual_keys {
662                    let key_path = path.join(key);
663                    if let Err(err) = rule.match_value("", key.as_str(), false, false) {
664                      result.push(CommonMismatch {
665                        path: key_path.to_string(),
666                        expected: "".to_string(),
667                        actual: key.clone(),
668                        description: err.to_string(),
669                      });
670                    }
671                  }
672                }
673                Either::Right(name) => {
674                  result.push(CommonMismatch {
675                    path: path.to_string(),
676                    expected: expected.for_mismatch(),
677                    actual: actual.for_mismatch(),
678                    description: format!("Expected a matching rule, found an unresolved reference '{}'",
679                      name.name),
680                  });
681                }
682              }
683            }
684          }
685          _ => {}
686        }
687      }
688    }
689
690    if result.is_empty() {
691      Ok(())
692    } else {
693      Err(result)
694    }
695  }
696
697  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
698  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
699    &self.plugin_configuration
700  }
701
702  fn matchers(&self) -> &MatchingRuleCategory {
703    &self.matchers
704  }
705
706  fn config(&self) -> DiffConfig {
707    self.config
708  }
709
710  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
711    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
712    {
713      Box::new(CoreMatchingContext {
714        matchers: matchers.clone(),
715        config: self.config.clone(),
716        matching_spec: self.matching_spec,
717        plugin_configuration: self.plugin_configuration.clone()
718      })
719    }
720
721    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
722    {
723      Box::new(CoreMatchingContext {
724        matchers: matchers.clone(),
725        config: self.config.clone(),
726        matching_spec: self.matching_spec
727      })
728    }
729  }
730}
731
732#[derive(Debug, Clone, Default)]
733/// Matching context for headers. Keys will be applied in a case-insensitive manor
734pub struct HeaderMatchingContext {
735  inner_context: CoreMatchingContext
736}
737
738impl HeaderMatchingContext {
739  /// Wraps a MatchingContext, downcasing all the matching path keys
740  pub fn new(context: &(dyn MatchingContext + Send + Sync)) -> Self {
741    let matchers = context.matchers();
742
743    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
744    {
745      HeaderMatchingContext {
746        inner_context: CoreMatchingContext::new(
747          context.config(),
748          &MatchingRuleCategory {
749            name: matchers.name.clone(),
750            rules: matchers.rules.iter()
751              .map(|(path, rules)| {
752                (path.to_lower_case(), rules.clone())
753              })
754              .collect()
755          },
756          &context.plugin_configuration()
757        )
758      }
759    }
760
761    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
762    {
763      HeaderMatchingContext {
764        inner_context: CoreMatchingContext::new(
765          context.config(),
766          &MatchingRuleCategory {
767            name: matchers.name.clone(),
768            rules: matchers.rules.iter()
769              .map(|(path, rules)| {
770                (path.to_lower_case(), rules.clone())
771              })
772              .collect()
773          }
774        )
775      }
776    }
777  }
778}
779
780impl MatchingContext for HeaderMatchingContext {
781  fn matcher_is_defined(&self, path: &DocPath) -> bool {
782    self.inner_context.matcher_is_defined(path)
783  }
784
785  fn select_best_matcher(&self, path: &DocPath) -> RuleList {
786    self.inner_context.select_best_matcher(path)
787  }
788
789  fn type_matcher_defined(&self, path: &DocPath) -> bool {
790    self.inner_context.type_matcher_defined(path)
791  }
792
793  fn values_matcher_defined(&self, path: &DocPath) -> bool {
794    self.inner_context.values_matcher_defined(path)
795  }
796
797  fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
798    self.inner_context.direct_matcher_defined(path, matchers)
799  }
800
801  fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
802    self.inner_context.match_keys(path, expected, actual)
803  }
804
805  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
806  fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
807    self.inner_context.plugin_configuration()
808  }
809
810  fn matchers(&self) -> &MatchingRuleCategory {
811    self.inner_context.matchers()
812  }
813
814  fn config(&self) -> DiffConfig {
815    self.inner_context.config()
816  }
817
818  fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
819    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
820    {
821      Box::new(HeaderMatchingContext::new(
822        &CoreMatchingContext {
823          matchers: matchers.clone(),
824          config: self.inner_context.config.clone(),
825          matching_spec: self.inner_context.matching_spec,
826          plugin_configuration: self.inner_context.plugin_configuration.clone()
827        }
828      ))
829    }
830
831    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
832    {
833      Box::new(HeaderMatchingContext::new(
834        &CoreMatchingContext {
835          matchers: matchers.clone(),
836          config: self.inner_context.config.clone(),
837          matching_spec: self.inner_context.matching_spec
838        }
839      ))
840    }
841  }
842}
843
844lazy_static! {
845  static ref BODY_MATCHERS: [
846    (fn(content_type: &ContentType) -> bool,
847    fn(expected: &(dyn HttpPart + Send + Sync), actual: &(dyn HttpPart + Send + Sync), context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>>); 5]
848     = [
849      (|content_type| { content_type.is_json() }, json::match_json),
850      (|content_type| { content_type.is_xml() }, match_xml),
851      (|content_type| { content_type.main_type == "multipart" }, binary_utils::match_mime_multipart),
852      (|content_type| { content_type.base_type() == "application/x-www-form-urlencoded" }, form_urlencoded::match_form_urlencoded),
853      (|content_type| { content_type.is_binary() || content_type.base_type() == "application/octet-stream" }, binary_utils::match_octet_stream)
854  ];
855}
856
857fn match_xml(
858  expected: &(dyn HttpPart + Send + Sync),
859  actual: &(dyn HttpPart + Send + Sync),
860  context: &(dyn MatchingContext + Send + Sync)
861) -> Result<(), Vec<Mismatch>> {
862  #[cfg(feature = "xml")]
863  {
864    xml::match_xml(expected, actual, context)
865  }
866  #[cfg(not(feature = "xml"))]
867  {
868    warn!("Matching XML documents requires the xml feature to be enabled");
869    match_text(&expected.body().value(), &actual.body().value(), context)
870  }
871}
872
873/// Store common mismatch information so it can be converted to different type of mismatches
874#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
875pub struct CommonMismatch {
876  /// path expression to where the mismatch occurred
877  pub path: String,
878  /// expected value (as a string)
879  expected: String,
880  /// actual value (as a string)
881  actual: String,
882  /// Description of the mismatch
883  description: String
884}
885
886impl CommonMismatch {
887  /// Convert common mismatch to body mismatch
888  pub fn to_body_mismatch(&self) -> Mismatch {
889    Mismatch::BodyMismatch {
890      path: self.path.clone(),
891      expected: Some(self.expected.clone().into()),
892      actual: Some(self.actual.clone().into()),
893      mismatch: self.description.clone()
894    }
895  }
896
897  /// Convert common mismatch to query mismatch
898  pub fn to_query_mismatch(&self) -> Mismatch {
899    Mismatch::QueryMismatch {
900      parameter: self.path.clone(),
901      expected: self.expected.clone(),
902      actual: self.actual.clone(),
903      mismatch: self.description.clone()
904    }
905  }
906
907  /// Convert common mismatch to header mismatch
908  pub fn to_header_mismatch(&self) -> Mismatch {
909    Mismatch::HeaderMismatch {
910      key: self.path.clone(),
911      expected: self.expected.clone().into(),
912      actual: self.actual.clone().into(),
913      mismatch: self.description.clone()
914    }
915  }
916}
917
918impl Display for CommonMismatch {
919  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
920    write!(f, "{}", self.description)
921  }
922}
923
924impl PartialEq for CommonMismatch {
925  fn eq(&self, other: &CommonMismatch) -> bool {
926    self.path == other.path && self.expected == other.expected && self.actual == other.actual
927  }
928}
929
930impl From<Mismatch> for CommonMismatch {
931  fn from(value: Mismatch) -> Self {
932    match value {
933      Mismatch::MethodMismatch { expected, actual , mismatch} => CommonMismatch {
934        path: "".to_string(),
935        expected: expected.clone(),
936        actual: actual.clone(),
937        description: mismatch.clone()
938      },
939      Mismatch::PathMismatch { expected, actual, mismatch } => CommonMismatch {
940        path: "".to_string(),
941        expected: expected.clone(),
942        actual: actual.clone(),
943        description: mismatch.clone()
944      },
945      Mismatch::StatusMismatch { expected, actual, mismatch } => CommonMismatch {
946        path: "".to_string(),
947        expected: expected.to_string(),
948        actual: actual.to_string(),
949        description: mismatch.clone()
950      },
951      Mismatch::QueryMismatch { parameter, expected, actual, mismatch } => CommonMismatch {
952        path: parameter.clone(),
953        expected: expected.clone(),
954        actual: actual.clone(),
955        description: mismatch.clone()
956      },
957      Mismatch::HeaderMismatch { key, expected, actual, mismatch } => CommonMismatch {
958        path: key.clone(),
959        expected: expected.clone(),
960        actual: actual.clone(),
961        description: mismatch.clone()
962      },
963      Mismatch::BodyTypeMismatch { expected, actual, mismatch, .. } => CommonMismatch {
964        path: "".to_string(),
965        expected: expected.clone(),
966        actual: actual.clone(),
967        description: mismatch.clone()
968      },
969      Mismatch::BodyMismatch { path, expected, actual, mismatch } => CommonMismatch {
970        path: path.clone(),
971        expected: String::from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(),
972        actual: String::from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(),
973        description: mismatch.clone()
974      },
975      Mismatch::MetadataMismatch { key, expected, actual, mismatch } => CommonMismatch {
976        path: key.clone(),
977        expected: expected.clone(),
978        actual: actual.clone(),
979        description: mismatch.clone()
980      }
981    }
982  }
983}
984
985/// Enum that defines the different types of mismatches that can occur.
986#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
987pub enum Mismatch {
988    /// Request Method mismatch
989    MethodMismatch {
990      /// Expected request method
991      expected: String,
992      /// Actual request method
993      actual: String,
994      /// description of the mismatch
995      mismatch: String
996    },
997    /// Request Path mismatch
998    PathMismatch {
999        /// expected request path
1000        expected: String,
1001        /// actual request path
1002        actual: String,
1003        /// description of the mismatch
1004        mismatch: String
1005    },
1006    /// Response status mismatch
1007    StatusMismatch {
1008        /// expected response status
1009      expected: u16,
1010      /// actual response status
1011      actual: u16,
1012      /// description of the mismatch
1013      mismatch: String
1014    },
1015    /// Request query mismatch
1016    QueryMismatch {
1017        /// query parameter name
1018        parameter: String,
1019        /// expected value
1020        expected: String,
1021        /// actual value
1022        actual: String,
1023        /// description of the mismatch
1024        mismatch: String
1025    },
1026    /// Header mismatch
1027    HeaderMismatch {
1028        /// header key
1029        key: String,
1030        /// expected value
1031        expected: String,
1032        /// actual value
1033        actual: String,
1034        /// description of the mismatch
1035        mismatch: String
1036    },
1037    /// Mismatch in the content type of the body
1038    BodyTypeMismatch {
1039      /// expected content type of the body
1040      expected: String,
1041      /// actual content type of the body
1042      actual: String,
1043      /// description of the mismatch
1044      mismatch: String,
1045      /// expected value
1046      expected_body: Option<Bytes>,
1047      /// actual value
1048      actual_body: Option<Bytes>
1049    },
1050    /// Body element mismatch
1051    BodyMismatch {
1052      /// path expression to where the mismatch occurred
1053      path: String,
1054      /// expected value
1055      expected: Option<Bytes>,
1056      /// actual value
1057      actual: Option<Bytes>,
1058      /// description of the mismatch
1059      mismatch: String
1060    },
1061    /// Message metadata mismatch
1062    MetadataMismatch {
1063      /// key
1064      key: String,
1065      /// expected value
1066      expected: String,
1067      /// actual value
1068      actual: String,
1069      /// description of the mismatch
1070      mismatch: String
1071    }
1072}
1073
1074impl Mismatch {
1075  /// Converts the mismatch to a `Value` struct.
1076  pub fn to_json(&self) -> serde_json::Value {
1077    match self {
1078      Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => {
1079        json!({
1080          "type" : "MethodMismatch",
1081          "expected" : e,
1082          "actual" : a,
1083          "mismatch" : m
1084        })
1085      },
1086      Mismatch::PathMismatch { expected: e, actual: a, mismatch: m } => {
1087        json!({
1088          "type" : "PathMismatch",
1089          "expected" : e,
1090          "actual" : a,
1091          "mismatch" : m
1092        })
1093      },
1094      Mismatch::StatusMismatch { expected: e, actual: a, mismatch: m } => {
1095        json!({
1096          "type" : "StatusMismatch",
1097          "expected" : e,
1098          "actual" : a,
1099          "mismatch": m
1100        })
1101      },
1102      Mismatch::QueryMismatch { parameter: p, expected: e, actual: a, mismatch: m } => {
1103        json!({
1104          "type" : "QueryMismatch",
1105          "parameter" : p,
1106          "expected" : e,
1107          "actual" : a,
1108          "mismatch" : m
1109        })
1110      },
1111      Mismatch::HeaderMismatch { key: k, expected: e, actual: a, mismatch: m } => {
1112        json!({
1113          "type" : "HeaderMismatch",
1114          "key" : k,
1115          "expected" : e,
1116          "actual" : a,
1117          "mismatch" : m
1118        })
1119      },
1120      Mismatch::BodyTypeMismatch {
1121        expected,
1122        actual,
1123        mismatch,
1124        expected_body,
1125        actual_body
1126      } => {
1127        json!({
1128          "type" : "BodyTypeMismatch",
1129          "expected" : expected,
1130          "actual" : actual,
1131          "mismatch" : mismatch,
1132          "expectedBody": match expected_body {
1133            Some(v) => serde_json::Value::String(str::from_utf8(v)
1134              .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1135            None => serde_json::Value::Null
1136          },
1137          "actualBody": match actual_body {
1138            Some(v) => serde_json::Value::String(str::from_utf8(v)
1139              .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1140            None => serde_json::Value::Null
1141          }
1142        })
1143      },
1144      Mismatch::BodyMismatch { path, expected, actual, mismatch } => {
1145        json!({
1146          "type" : "BodyMismatch",
1147          "path" : path,
1148          "expected" : match expected {
1149            Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1150            None => serde_json::Value::Null
1151          },
1152          "actual" : match actual {
1153            Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1154            None => serde_json::Value::Null
1155          },
1156          "mismatch" : mismatch
1157        })
1158      }
1159      Mismatch::MetadataMismatch { key, expected, actual, mismatch } => {
1160        json!({
1161          "type" : "MetadataMismatch",
1162          "key" : key,
1163          "expected" : expected,
1164          "actual" : actual,
1165          "mismatch" : mismatch
1166        })
1167      }
1168    }
1169  }
1170
1171    /// Returns the type of the mismatch as a string
1172    pub fn mismatch_type(&self) -> &str {
1173      match *self {
1174        Mismatch::MethodMismatch { .. } => "MethodMismatch",
1175        Mismatch::PathMismatch { .. } => "PathMismatch",
1176        Mismatch::StatusMismatch { .. } => "StatusMismatch",
1177        Mismatch::QueryMismatch { .. } => "QueryMismatch",
1178        Mismatch::HeaderMismatch { .. } => "HeaderMismatch",
1179        Mismatch::BodyTypeMismatch { .. } => "BodyTypeMismatch",
1180        Mismatch::BodyMismatch { .. } => "BodyMismatch",
1181        Mismatch::MetadataMismatch { .. } => "MetadataMismatch"
1182      }
1183    }
1184
1185    /// Returns a summary string for this mismatch
1186    pub fn summary(&self) -> String {
1187      match *self {
1188        Mismatch::MethodMismatch { expected: ref e, .. } => format!("is a {} request", e),
1189        Mismatch::PathMismatch { expected: ref e, .. } => format!("to path '{}'", e),
1190        Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e),
1191        Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e),
1192        Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e),
1193        Mismatch::BodyTypeMismatch { .. } => "has a matching body".to_string(),
1194        Mismatch::BodyMismatch { .. } => "has a matching body".to_string(),
1195        Mismatch::MetadataMismatch { .. } => "has matching metadata".to_string()
1196      }
1197    }
1198
1199    /// Returns a formatted string for this mismatch
1200    pub fn description(&self) -> String {
1201      match self {
1202        Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => if m.is_empty() {
1203          format!("expected {} but was {}", e, a)
1204        } else {
1205          m.clone()
1206        },
1207        Mismatch::PathMismatch { mismatch, .. } => mismatch.clone(),
1208        Mismatch::StatusMismatch { mismatch, .. } => mismatch.clone(),
1209        Mismatch::QueryMismatch { mismatch, .. } => mismatch.clone(),
1210        Mismatch::HeaderMismatch { mismatch, .. } => mismatch.clone(),
1211        Mismatch::BodyTypeMismatch {  expected: e, actual: a, .. } =>
1212          format!("Expected a body of '{}' but the actual content type was '{}'", e, a),
1213        Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", path, mismatch),
1214        Mismatch::MetadataMismatch { mismatch, .. } => mismatch.clone()
1215      }
1216    }
1217
1218    /// Returns a formatted string with ansi escape codes for this mismatch
1219    pub fn ansi_description(&self) -> String {
1220      match self {
1221        Mismatch::MethodMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.clone()), Green.paint(a.clone())),
1222        Mismatch::PathMismatch { expected: e, actual: a, .. } => format!("expected '{}' but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1223        Mismatch::StatusMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.to_string()), Green.paint(a.to_string())),
1224        Mismatch::QueryMismatch { expected: e, actual: a, parameter: p, .. } => format!("Expected '{}' but received '{}' for query parameter '{}'",
1225          Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())),
1226        Mismatch::HeaderMismatch { expected: e, actual: a, key: k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'",
1227          Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())),
1228        Mismatch::BodyTypeMismatch {  expected: e, actual: a, .. } =>
1229          format!("expected a body of '{}' but the actual content type was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1230        Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch),
1231        Mismatch::MetadataMismatch { expected: e, actual: a, key: k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'",
1232          Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string()))
1233      }
1234    }
1235}
1236
1237impl PartialEq for Mismatch {
1238  fn eq(&self, other: &Mismatch) -> bool {
1239    match (self, other) {
1240      (Mismatch::MethodMismatch { expected: e1, actual: a1, .. },
1241        Mismatch::MethodMismatch { expected: e2, actual: a2, .. }) => {
1242        e1 == e2 && a1 == a2
1243      },
1244      (Mismatch::PathMismatch { expected: e1, actual: a1, .. },
1245        Mismatch::PathMismatch { expected: e2, actual: a2, .. }) => {
1246        e1 == e2 && a1 == a2
1247      },
1248      (Mismatch::StatusMismatch { expected: e1, actual: a1, .. },
1249        Mismatch::StatusMismatch { expected: e2, actual: a2, .. }) => {
1250        e1 == e2 && a1 == a2
1251      },
1252      (Mismatch::BodyTypeMismatch { expected: e1, actual: a1, .. },
1253        Mismatch::BodyTypeMismatch { expected: e2, actual: a2, .. }) => {
1254        e1 == e2 && a1 == a2
1255      },
1256      (Mismatch::QueryMismatch { parameter: p1, expected: e1, actual: a1, .. },
1257        Mismatch::QueryMismatch { parameter: p2, expected: e2, actual: a2, .. }) => {
1258        p1 == p2 && e1 == e2 && a1 == a2
1259      },
1260      (Mismatch::HeaderMismatch { key: p1, expected: e1, actual: a1, .. },
1261        Mismatch::HeaderMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1262        p1 == p2 && e1 == e2 && a1 == a2
1263      },
1264      (Mismatch::BodyMismatch { path: p1, expected: e1, actual: a1, .. },
1265        Mismatch::BodyMismatch { path: p2, expected: e2, actual: a2, .. }) => {
1266        p1 == p2 && e1 == e2 && a1 == a2
1267      },
1268      (Mismatch::MetadataMismatch { key: p1, expected: e1, actual: a1, .. },
1269        Mismatch::MetadataMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1270        p1 == p2 && e1 == e2 && a1 == a2
1271      },
1272      (_, _) => false
1273    }
1274  }
1275}
1276
1277impl Display for Mismatch {
1278  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1279    write!(f, "{}", self.description())
1280  }
1281}
1282
1283fn merge_result<T: Clone>(res1: Result<(), Vec<T>>, res2: Result<(), Vec<T>>) -> Result<(), Vec<T>> {
1284  match (&res1, &res2) {
1285    (Ok(_), Ok(_)) => res1.clone(),
1286    (Err(_), Ok(_)) => res1.clone(),
1287    (Ok(_), Err(_)) => res2.clone(),
1288    (Err(m1), Err(m2)) => {
1289      let mut mismatches = m1.clone();
1290      mismatches.extend_from_slice(&*m2);
1291      Err(mismatches)
1292    }
1293  }
1294}
1295
1296/// Result of matching a request body
1297#[derive(Debug, Default, Clone, PartialEq)]
1298pub enum BodyMatchResult {
1299  /// Matched OK
1300  #[default]
1301  Ok,
1302  /// Mismatch in the content type of the body
1303  BodyTypeMismatch {
1304    /// Expected content type
1305    expected_type: String,
1306    /// Actual content type
1307    actual_type: String,
1308    /// Message
1309    message: String,
1310    /// Expected body
1311    expected: Option<Bytes>,
1312    /// Actual body
1313    actual: Option<Bytes>
1314  },
1315  /// Mismatches with the body contents
1316  BodyMismatches(HashMap<String, Vec<Mismatch>>)
1317}
1318
1319impl BodyMatchResult {
1320  /// Returns all the mismatches
1321  pub fn mismatches(&self) -> Vec<Mismatch> {
1322    match self {
1323      BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
1324        vec![Mismatch::BodyTypeMismatch {
1325          expected: expected_type.clone(),
1326          actual: actual_type.clone(),
1327          mismatch: message.clone(),
1328          expected_body: expected.clone(),
1329          actual_body: actual.clone()
1330        }]
1331      },
1332      BodyMatchResult::BodyMismatches(results) =>
1333        results.values().flatten().cloned().collect(),
1334      _ => vec![]
1335    }
1336  }
1337
1338  /// If all the things matched OK
1339  pub fn all_matched(&self) -> bool {
1340    match self {
1341      BodyMatchResult::BodyTypeMismatch { .. } => false,
1342      BodyMatchResult::BodyMismatches(results) =>
1343        results.values().all(|m| m.is_empty()),
1344      _ => true
1345    }
1346  }
1347}
1348
1349/// Result of matching a request
1350#[derive(Debug, Default, Clone, PartialEq)]
1351pub struct RequestMatchResult {
1352  /// Method match result
1353  pub method: Option<Mismatch>,
1354  /// Path match result
1355  pub path: Option<Vec<Mismatch>>,
1356  /// Body match result
1357  pub body: BodyMatchResult,
1358  /// Query parameter result
1359  pub query: HashMap<String, Vec<Mismatch>>,
1360  /// Headers result
1361  pub headers: HashMap<String, Vec<Mismatch>>
1362}
1363
1364impl RequestMatchResult {
1365  /// Returns all the mismatches
1366  pub fn mismatches(&self) -> Vec<Mismatch> {
1367    let mut m = vec![];
1368
1369    if let Some(ref mismatch) = self.method {
1370      m.push(mismatch.clone());
1371    }
1372    if let Some(ref mismatches) = self.path {
1373      m.extend_from_slice(mismatches.as_slice());
1374    }
1375    for mismatches in self.query.values() {
1376      m.extend_from_slice(mismatches.as_slice());
1377    }
1378    for mismatches in self.headers.values() {
1379      m.extend_from_slice(mismatches.as_slice());
1380    }
1381    m.extend_from_slice(self.body.mismatches().as_slice());
1382
1383    m
1384  }
1385
1386  /// Returns a score based on what was matched
1387  pub fn score(&self) -> i8 {
1388    let mut score = 0;
1389    if self.method.is_none() {
1390      score += 1;
1391    } else {
1392      score -= 1;
1393    }
1394    if self.path.is_none() {
1395      score += 1
1396    } else {
1397      score -= 1
1398    }
1399    for mismatches in self.query.values() {
1400      if mismatches.is_empty() {
1401        score += 1;
1402      } else {
1403        score -= 1;
1404      }
1405    }
1406    for mismatches in self.headers.values() {
1407      if mismatches.is_empty() {
1408        score += 1;
1409      } else {
1410        score -= 1;
1411      }
1412    }
1413    match &self.body {
1414      BodyMatchResult::BodyTypeMismatch { .. } => {
1415        score -= 1;
1416      },
1417      BodyMatchResult::BodyMismatches(results) => {
1418        for mismatches in results.values() {
1419          if mismatches.is_empty() {
1420            score += 1;
1421          } else {
1422            score -= 1;
1423          }
1424        }
1425      },
1426      _ => ()
1427    }
1428    score
1429  }
1430
1431  /// If all the things matched OK
1432  pub fn all_matched(&self) -> bool {
1433    self.method.is_none() && self.path.is_none() &&
1434      self.query.values().all(|m| m.is_empty()) &&
1435      self.headers.values().all(|m| m.is_empty()) &&
1436      self.body.all_matched()
1437  }
1438
1439  /// If there was a mismatch with the method or path
1440  pub fn method_or_path_mismatch(&self) -> bool {
1441    self.method.is_some() || self.path.is_some()
1442  }
1443}
1444
1445impl From<ExecutionPlan> for RequestMatchResult {
1446  fn from(plan: ExecutionPlan) -> Self {
1447    let request = plan.fetch_node(&[":request"]).unwrap_or_default();
1448    let method = method_mismatch(&request);
1449    let path = path_mismatch(&request);
1450    let query = query_mismatches(&request);
1451    let headers = header_mismatches(&request);
1452    let body = body_mismatches(&request);
1453    RequestMatchResult {
1454      method,
1455      path,
1456      body,
1457      query,
1458      headers
1459    }
1460  }
1461}
1462
1463/// Enum that defines the configuration options for performing a match.
1464#[derive(Debug, Clone, Copy, PartialEq)]
1465pub enum DiffConfig {
1466    /// If unexpected keys are allowed and ignored during matching.
1467    AllowUnexpectedKeys,
1468    /// If unexpected keys cause a mismatch.
1469    NoUnexpectedKeys
1470}
1471
1472/// Matches the actual text body to the expected one.
1473pub fn match_text(expected: &Option<Bytes>, actual: &Option<Bytes>, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1474  let path = DocPath::root();
1475  if context.matcher_is_defined(&path) {
1476    let mut mismatches = vec![];
1477    let empty = Bytes::default();
1478    let expected_str = match from_utf8(expected.as_ref().unwrap_or(&empty)) {
1479      Ok(expected) => expected,
1480      Err(err) => {
1481        mismatches.push(Mismatch::BodyMismatch {
1482          path: "$".to_string(),
1483          expected: expected.clone(),
1484          actual: actual.clone(),
1485          mismatch: format!("Could not parse expected value as UTF-8 text: {}", err)
1486        });
1487        ""
1488      }
1489    };
1490    let actual_str = match from_utf8(actual.as_ref().unwrap_or(&empty)) {
1491      Ok(actual) => actual,
1492      Err(err) => {
1493        mismatches.push(Mismatch::BodyMismatch {
1494          path: "$".to_string(),
1495          expected: expected.clone(),
1496          actual: actual.clone(),
1497          mismatch: format!("Could not parse actual value as UTF-8 text: {}", err)
1498        });
1499        ""
1500      }
1501    };
1502    if let Err(messages) = match_values(&path, &context.select_best_matcher(&path), expected_str, actual_str) {
1503      for message in messages {
1504        mismatches.push(Mismatch::BodyMismatch {
1505          path: "$".to_string(),
1506          expected: expected.clone(),
1507          actual: actual.clone(),
1508          mismatch: message.clone()
1509        })
1510      }
1511    };
1512    if mismatches.is_empty() {
1513      Ok(())
1514    } else {
1515      Err(mismatches)
1516    }
1517  } else if expected != actual {
1518    let expected = expected.clone().unwrap_or_default();
1519    let actual = actual.clone().unwrap_or_default();
1520    let e = String::from_utf8_lossy(&expected);
1521    let a = String::from_utf8_lossy(&actual);
1522    let mismatch = format!("Expected body '{}' to match '{}' using equality but did not match", e, a);
1523    Err(vec![
1524      Mismatch::BodyMismatch {
1525        path: "$".to_string(),
1526        expected: Some(expected.clone()),
1527        actual: Some(actual.clone()),
1528        mismatch
1529      }
1530    ])
1531  } else {
1532    Ok(())
1533  }
1534}
1535
1536/// Matches the actual request method to the expected one.
1537pub fn match_method(expected: &str, actual: &str) -> Result<(), Mismatch> {
1538  if expected.to_lowercase() != actual.to_lowercase() {
1539    Err(Mismatch::MethodMismatch { expected: expected.to_string(), actual: actual.to_string(), mismatch: "".to_string() })
1540  } else {
1541    Ok(())
1542  }
1543}
1544
1545/// Matches the actual request path to the expected one.
1546pub fn match_path(expected: &str, actual: &str, context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>> {
1547  let path = DocPath::empty();
1548  let matcher_result = if context.matcher_is_defined(&path) {
1549    match_values(&path, &context.select_best_matcher(&path), expected.to_string(), actual.to_string())
1550  } else {
1551    MatchingRule::Equality.match_value(expected, actual, false, false)
1552      .map_err(|err| vec![err.to_string()])
1553  };
1554  matcher_result.map_err(|messages| messages.iter().map(|message| {
1555    Mismatch::PathMismatch {
1556      expected: expected.to_string(),
1557      actual: actual.to_string(), mismatch: message.clone()
1558    }
1559  }).collect())
1560}
1561
1562/// Matches the actual query parameters to the expected ones.
1563pub fn match_query(
1564  expected: Option<HashMap<String, Vec<Option<String>>>>,
1565  actual: Option<HashMap<String, Vec<Option<String>>>>,
1566  context: &(dyn MatchingContext + Send + Sync)
1567) -> HashMap<String, Vec<Mismatch>> {
1568  match (actual, expected) {
1569    (Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
1570    (Some(aqm), None) => aqm.iter().map(|(key, value)| {
1571      let actual_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1572      (key.clone(), vec![Mismatch::QueryMismatch {
1573        parameter: key.clone(),
1574        expected: "".to_string(),
1575        actual: format!("{:?}", actual_value),
1576        mismatch: format!("Unexpected query parameter '{}' received", key)
1577      }])
1578    }).collect(),
1579    (None, Some(eqm)) => eqm.iter().map(|(key, value)| {
1580      let expected_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1581      (key.clone(), vec![Mismatch::QueryMismatch {
1582        parameter: key.clone(),
1583        expected: format!("{:?}", expected_value),
1584        actual: "".to_string(),
1585        mismatch: format!("Expected query parameter '{}' but was missing", key)
1586      }])
1587    }).collect(),
1588    (None, None) => hashmap!{}
1589  }
1590}
1591
1592fn group_by<I, F, K>(items: I, f: F) -> HashMap<K, Vec<I::Item>>
1593  where I: IntoIterator, F: Fn(&I::Item) -> K, K: Eq + Hash {
1594  let mut m = hashmap!{};
1595  for item in items {
1596    let key = f(&item);
1597    let values = m.entry(key).or_insert_with(Vec::new);
1598    values.push(item);
1599  }
1600  m
1601}
1602
1603#[instrument(level = "trace", ret, skip_all)]
1604pub(crate) async fn compare_bodies(
1605  content_type: &ContentType,
1606  expected: &(dyn HttpPart + Send + Sync),
1607  actual: &(dyn HttpPart + Send + Sync),
1608  context: &(dyn MatchingContext + Send + Sync)
1609) -> BodyMatchResult {
1610  let mut mismatches = vec![];
1611
1612  trace!(?content_type, "Comparing bodies");
1613
1614  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1615  {
1616    match find_content_matcher(content_type) {
1617      Some(matcher) => {
1618        debug!("Using content matcher {} for content type '{}'", matcher.catalogue_entry_key(), content_type);
1619        if matcher.is_core() {
1620          if let Err(m) = match matcher.catalogue_entry_key().as_str() {
1621            "core/content-matcher/form-urlencoded" => form_urlencoded::match_form_urlencoded(expected, actual, context),
1622            "core/content-matcher/json" => match_json(expected, actual, context),
1623            "core/content-matcher/multipart-form-data" => binary_utils::match_mime_multipart(expected, actual, context),
1624            "core/content-matcher/text" => match_text(&expected.body().value(), &actual.body().value(), context),
1625            "core/content-matcher/xml" => {
1626              #[cfg(feature = "xml")]
1627              {
1628                xml::match_xml(expected, actual, context)
1629              }
1630              #[cfg(not(feature = "xml"))]
1631              {
1632                warn!("Matching XML bodies requires the xml feature to be enabled");
1633                match_text(&expected.body().value(), &actual.body().value(), context)
1634              }
1635            },
1636            "core/content-matcher/binary" => binary_utils::match_octet_stream(expected, actual, context),
1637            _ => {
1638              warn!("There is no core content matcher for entry {}", matcher.catalogue_entry_key());
1639              match_text(&expected.body().value(), &actual.body().value(), context)
1640            }
1641          } {
1642            mismatches.extend_from_slice(&*m);
1643          }
1644        } else {
1645          trace!(plugin_name = matcher.plugin_name(),"Content matcher is provided via a plugin");
1646          let plugin_config = context.plugin_configuration().get(&matcher.plugin_name()).cloned();
1647          trace!("Plugin config = {:?}", plugin_config);
1648          if pact_plugin_driver::test_context::current_test_run_id().is_none() {
1649            pact_plugin_driver::test_context::set_test_run_id(Some(uuid::Uuid::new_v4().to_string()));
1650          }
1651          if let Err(map) = matcher.match_contents(expected.body(), actual.body(), &context.matchers(),
1652                                                   context.config() == DiffConfig::AllowUnexpectedKeys, plugin_config).await {
1653            // TODO: group the mismatches by key
1654            for (_key, list) in map {
1655              for mismatch in list {
1656                mismatches.push(Mismatch::BodyMismatch {
1657                  path: mismatch.path.clone(),
1658                  expected: Some(Bytes::from(mismatch.expected)),
1659                  actual: Some(Bytes::from(mismatch.actual)),
1660                  mismatch: mismatch.mismatch.clone()
1661                });
1662              }
1663            }
1664          }
1665        }
1666      }
1667      None => {
1668        debug!("No content matcher defined for content type '{}', using core matcher implementation", content_type);
1669        mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1670      }
1671    }
1672  }
1673
1674  #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1675  {
1676    mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1677  }
1678
1679  if mismatches.is_empty() {
1680    BodyMatchResult::Ok
1681  } else {
1682    BodyMatchResult::BodyMismatches(group_by(mismatches, |m| match m {
1683      Mismatch::BodyMismatch { path: m, ..} => m.to_string(),
1684      _ => String::default()
1685    }))
1686  }
1687}
1688
1689fn compare_bodies_core(
1690  content_type: &ContentType,
1691  expected: &(dyn HttpPart + Send + Sync),
1692  actual: &(dyn HttpPart + Send + Sync),
1693  context: &(dyn MatchingContext + Send + Sync)
1694) -> Vec<Mismatch> {
1695  let mut mismatches = vec![];
1696  match BODY_MATCHERS.iter().find(|mt| mt.0(content_type)) {
1697    Some(match_fn) => {
1698      debug!("Using body matcher for content type '{}'", content_type);
1699      if let Err(m) = match_fn.1(expected, actual, context) {
1700        mismatches.extend_from_slice(&*m);
1701      }
1702    },
1703    None => {
1704      debug!("No body matcher defined for content type '{}', checking for a content type matcher", content_type);
1705      let path = DocPath::root();
1706      if context.matcher_is_defined(&path) && context.select_best_matcher(&path).rules
1707        .iter().any(|rule| if let MatchingRule::ContentType(_) = rule { true } else { false }) {
1708        debug!("Found a content type matcher");
1709        if let Err(m) = binary_utils::match_octet_stream(expected, actual, context) {
1710          mismatches.extend_from_slice(&*m);
1711        }
1712      } else {
1713        debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
1714        if let Err(m) = match_text(&expected.body().value(), &actual.body().value(), context) {
1715          mismatches.extend_from_slice(&*m);
1716        }
1717      }
1718    }
1719  };
1720  mismatches
1721}
1722
1723#[instrument(level = "trace", ret, skip_all, fields(%content_type, ?context))]
1724async fn match_body_content(
1725  content_type: &ContentType,
1726  expected: &(dyn HttpPart + Send + Sync),
1727  actual: &(dyn HttpPart + Send + Sync),
1728  context: &(dyn MatchingContext + Send + Sync)
1729) -> BodyMatchResult {
1730  let expected_body = expected.body();
1731  let actual_body = actual.body();
1732  match (expected_body, actual_body) {
1733    (&OptionalBody::Missing, _) => BodyMatchResult::Ok,
1734    (&OptionalBody::Null, &OptionalBody::Present(ref b, _, _)) => {
1735      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1736        mismatch: format!("Expected empty body but received {}", actual_body),
1737        path: s!("/")}]})
1738    },
1739    (&OptionalBody::Empty, &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::Null, _) => BodyMatchResult::Ok,
1745    (&OptionalBody::Empty, _) => BodyMatchResult::Ok,
1746    (e, &OptionalBody::Missing) => {
1747      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1748        expected: e.value(),
1749        actual: None,
1750        mismatch: format!("Expected body {} but was missing", e),
1751        path: s!("/")}]})
1752    },
1753    (e, &OptionalBody::Empty) => {
1754      BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1755        expected: e.value(),
1756        actual: None,
1757        mismatch: format!("Expected body {} but was empty", e),
1758        path: s!("/")}]})
1759    },
1760    (_, _) => compare_bodies(content_type, expected, actual, context).await
1761  }
1762}
1763
1764/// Matches the actual body to the expected one. This takes into account the content type of each.
1765pub async fn match_body(
1766  expected: &(dyn HttpPart + Send + Sync),
1767  actual: &(dyn HttpPart + Send + Sync),
1768  context: &(dyn MatchingContext + Send + Sync),
1769  header_context: &(dyn MatchingContext + Send + Sync)
1770) -> BodyMatchResult {
1771  let expected_content_type = expected.content_type().unwrap_or_default();
1772  let actual_content_type = actual.content_type().unwrap_or_default();
1773  debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
1774         actual_content_type);
1775  let content_type_matcher = header_context.select_best_matcher(&DocPath::root().join("content-type"));
1776  debug!("content type header matcher = '{:?}'", content_type_matcher);
1777  if expected_content_type.is_unknown() || actual_content_type.is_unknown() ||
1778    expected_content_type.is_equivalent_to(&actual_content_type) ||
1779    expected_content_type.is_equivalent_to(&actual_content_type.base_type()) ||
1780    (!content_type_matcher.is_empty() &&
1781      match_header_value("Content-Type", 0, expected_content_type.to_string().as_str(),
1782                         actual_content_type.to_string().as_str(), header_context, true
1783      ).is_ok()) {
1784    match_body_content(&expected_content_type, expected, actual, context).await
1785  } else if expected.body().is_present() {
1786    BodyMatchResult::BodyTypeMismatch {
1787      expected_type: expected_content_type.to_string(),
1788      actual_type: actual_content_type.to_string(),
1789      message: format!("Expected a body of '{}' but the actual content type was '{}'", expected_content_type,
1790                       actual_content_type),
1791      expected: expected.body().value(),
1792      actual: actual.body().value()
1793    }
1794  } else {
1795    BodyMatchResult::Ok
1796  }
1797}
1798
1799/// Matches the expected and actual requests
1800#[allow(unused_variables)]
1801pub async fn match_request<'a>(
1802  expected: HttpRequest,
1803  actual: HttpRequest,
1804  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1805  interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1806) -> anyhow::Result<RequestMatchResult> {
1807  debug!("comparing to expected {}", expected);
1808  debug!("     body: '{}'", expected.body.display_string());
1809  debug!("     matching_rules:\n{}", expected.matching_rules);
1810  debug!("     generators: {:?}", expected.generators);
1811
1812  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1813    .map(|val| val.to_lowercase() == "v2")
1814    .unwrap_or(false);
1815  if use_v2_engine {
1816    let config = MatchingConfiguration {
1817      allow_unexpected_entries: false,
1818      .. MatchingConfiguration::init_from_env()
1819    };
1820    let mut context = PlanMatchingContext {
1821      pact: pact.as_v4_pact().unwrap_or_default(),
1822      interaction: interaction.as_v4().unwrap(),
1823      matching_rules: Default::default(),
1824      config
1825    };
1826
1827    let plan = build_request_plan(&expected, &mut context)?;
1828    let executed_plan = execute_request_plan(&plan, &actual, &mut context)?;
1829
1830    if config.log_executed_plan {
1831      debug!("config = {:?}", config);
1832      debug!("\n{}", executed_plan.pretty_form());
1833    }
1834    if config.log_plan_summary {
1835      info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1836    }
1837    Ok(executed_plan.into())
1838  } else {
1839    let result;
1840
1841    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1842    {
1843      let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Request);
1844      trace!("plugin_data = {:?}", plugin_data);
1845
1846      let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1847        &expected.matching_rules.rules_for_category("path").unwrap_or_default(),
1848        &plugin_data
1849      );
1850      let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1851        &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1852        &plugin_data
1853      );
1854      let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1855        &expected.matching_rules.rules_for_category("query").unwrap_or_default(),
1856        &plugin_data
1857      );
1858      let header_context = HeaderMatchingContext::new(
1859        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1860          &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1861          &plugin_data
1862        )
1863      );
1864      result = RequestMatchResult {
1865        method: match_method(&expected.method, &actual.method).err(),
1866        path: match_path(&expected.path, &actual.path, &path_context).err(),
1867        body: match_body(&expected, &actual, &body_context, &header_context).await,
1868        query: match_query(expected.query, actual.query, &query_context),
1869        headers: match_headers(expected.headers, actual.headers, &header_context)
1870      };
1871    }
1872
1873    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1874    {
1875      let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1876        &expected.matching_rules.rules_for_category("path").unwrap_or_default()
1877      );
1878      let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1879        &expected.matching_rules.rules_for_category("body").unwrap_or_default()
1880      );
1881      let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1882        &expected.matching_rules.rules_for_category("query").unwrap_or_default()
1883      );
1884      let header_context = HeaderMatchingContext::new(
1885        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1886          &expected.matching_rules.rules_for_category("header").unwrap_or_default()
1887        )
1888      );
1889      result = RequestMatchResult {
1890        method: match_method(&expected.method, &actual.method).err(),
1891        path: match_path(&expected.path, &actual.path, &path_context).err(),
1892        body: match_body(&expected, &actual, &body_context, &header_context).await,
1893        query: match_query(expected.query, actual.query, &query_context),
1894        headers: match_headers(expected.headers, actual.headers, &header_context)
1895      };
1896    }
1897
1898    debug!("--> Mismatches: {:?}", result.mismatches());
1899    Ok(result)
1900  }
1901}
1902
1903/// Matches the actual response status to the expected one.
1904#[instrument(level = "trace")]
1905pub fn match_status(expected: u16, actual: u16, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1906  let path = DocPath::empty();
1907  let result = if context.matcher_is_defined(&path) {
1908    match_values(&path, &context.select_best_matcher(&path), expected, actual)
1909      .map_err(|messages| messages.iter().map(|message| {
1910        Mismatch::StatusMismatch {
1911          expected,
1912          actual,
1913          mismatch: message.clone()
1914        }
1915      }).collect())
1916  } else if expected != actual {
1917    Err(vec![Mismatch::StatusMismatch {
1918      expected,
1919      actual,
1920      mismatch: format!("expected {} but was {}", expected, actual)
1921    }])
1922  } else {
1923    Ok(())
1924  };
1925  trace!(?result, "matching response status");
1926  result
1927}
1928
1929/// Matches the actual and expected responses.
1930#[allow(unused_variables)]
1931pub async fn match_response<'a>(
1932  expected: HttpResponse,
1933  actual: HttpResponse,
1934  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1935  interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1936) -> anyhow::Result<Vec<Mismatch>> {
1937  let mut mismatches = vec![];
1938
1939  debug!("comparing to expected response: {}", expected);
1940
1941  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1942    .map(|val| val.to_lowercase() == "v2")
1943    .unwrap_or(false);
1944  if use_v2_engine {
1945    let config = MatchingConfiguration {
1946      allow_unexpected_entries: true,
1947      .. MatchingConfiguration::init_from_env()
1948    };
1949    let mut context = PlanMatchingContext {
1950      pact: pact.as_v4_pact().unwrap_or_default(),
1951      interaction: interaction.as_v4().unwrap(),
1952      matching_rules: Default::default(),
1953      config
1954    };
1955
1956    let plan = build_response_plan(&expected, &mut context)?;
1957    let executed_plan = execute_response_plan(&plan, &actual, &mut context)?;
1958
1959    if config.log_executed_plan {
1960      debug!("config = {:?}", config);
1961      debug!("\n{}", executed_plan.pretty_form());
1962    }
1963    if config.log_plan_summary {
1964      info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1965    }
1966    Ok(executed_plan.into())
1967  } else {
1968
1969    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1970    {
1971      let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Response);
1972      trace!("plugin_data = {:?}", plugin_data);
1973
1974      let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1975        &expected.matching_rules.rules_for_category("status").unwrap_or_default(),
1976        &plugin_data);
1977      let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1978        &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1979        &plugin_data);
1980      let header_context = HeaderMatchingContext::new(
1981        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1982          &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1983          &plugin_data
1984        )
1985      );
1986
1987      mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
1988        .mismatches().as_slice());
1989      if let Err(m) = match_status(expected.status, actual.status, &status_context) {
1990        mismatches.extend_from_slice(&m);
1991      }
1992      let result = match_headers(expected.headers, actual.headers,
1993        &header_context);
1994      for values in result.values() {
1995        mismatches.extend_from_slice(values.as_slice());
1996      }
1997    }
1998
1999    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2000    {
2001      let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2002        &expected.matching_rules.rules_for_category("status").unwrap_or_default());
2003      let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2004        &expected.matching_rules.rules_for_category("body").unwrap_or_default());
2005      let header_context = HeaderMatchingContext::new(
2006        &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
2007          &expected.matching_rules.rules_for_category("header").unwrap_or_default()
2008        )
2009      );
2010
2011      mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
2012        .mismatches().as_slice());
2013      if let Err(m) = match_status(expected.status, actual.status, &status_context) {
2014        mismatches.extend_from_slice(&m);
2015      }
2016      let result = match_headers(expected.headers, actual.headers,
2017        &header_context);
2018      for values in result.values() {
2019        mismatches.extend_from_slice(values.as_slice());
2020      }
2021    }
2022
2023    trace!(?mismatches, "match response");
2024
2025    Ok(mismatches)
2026  }
2027}
2028
2029/// Matches the actual message contents to the expected one. This takes into account the content type of each.
2030#[instrument(level = "trace")]
2031pub async fn match_message_contents(
2032  expected: &MessageContents,
2033  actual: &MessageContents,
2034  context: &(dyn MatchingContext + Send + Sync)
2035) -> Result<(), Vec<Mismatch>> {
2036  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2037    .map(|val| val.to_lowercase() == "v2")
2038    .unwrap_or(false);
2039  if use_v2_engine {
2040    let config = MatchingConfiguration {
2041      allow_unexpected_entries: true,
2042      show_types_in_errors: true,
2043      .. MatchingConfiguration::init_from_env()
2044    };
2045    let plan_context = PlanMatchingContext {
2046      config,
2047      .. PlanMatchingContext::default()
2048    };
2049    match build_message_plan(expected, &plan_context) {
2050      Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2051        Ok(executed_plan) => {
2052          if config.log_executed_plan {
2053            debug!("config = {:?}", config);
2054            debug!("\n{}", executed_plan.pretty_form());
2055          }
2056          if config.log_plan_summary {
2057            info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2058          }
2059          if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2060            return match body_mismatches(&message_node) {
2061              BodyMatchResult::Ok => Ok(()),
2062              BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected: e, actual: a } => {
2063                Err(vec![Mismatch::BodyTypeMismatch {
2064                  expected: expected_type,
2065                  actual: actual_type,
2066                  mismatch: message,
2067                  expected_body: e,
2068                  actual_body: a
2069                }])
2070              }
2071              BodyMatchResult::BodyMismatches(results) => {
2072                let mismatches: Vec<Mismatch> = results.values()
2073                  .flat_map(|values| values.iter().cloned())
2074                  .collect();
2075                if mismatches.is_empty() { Ok(()) } else { Err(mismatches) }
2076              }
2077            };
2078          }
2079          return Ok(());
2080        }
2081        Err(err) => warn!("Failed to execute message plan: {}", err)
2082      },
2083      Err(err) => warn!("Failed to build message plan: {}", err)
2084    }
2085  }
2086
2087  let expected_content_type = expected.message_content_type().unwrap_or_default();
2088  let actual_content_type = actual.message_content_type().unwrap_or_default();
2089  debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
2090         actual_content_type);
2091  if expected_content_type.is_equivalent_to(&actual_content_type) {
2092    let result = match_body_content(&expected_content_type, expected, actual, context).await;
2093    match result {
2094      BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
2095        Err(vec![ Mismatch::BodyTypeMismatch {
2096          expected: expected_type,
2097          actual: actual_type,
2098          mismatch: message,
2099          expected_body: expected,
2100          actual_body: actual
2101        } ])
2102      },
2103      BodyMatchResult::BodyMismatches(results) => {
2104        Err(results.values().flat_map(|values| values.iter().cloned()).collect())
2105      },
2106      _ => Ok(())
2107    }
2108  } else if expected.contents.is_present() {
2109    Err(vec![ Mismatch::BodyTypeMismatch {
2110      expected: expected_content_type.to_string(),
2111      actual: actual_content_type.to_string(),
2112      mismatch: format!("Expected message with content type {} but was {}",
2113                        expected_content_type, actual_content_type),
2114      expected_body: expected.contents.value(),
2115      actual_body: actual.contents.value()
2116    } ])
2117  } else {
2118    Ok(())
2119  }
2120}
2121
2122/// Matches the actual message metadata to the expected one.
2123#[instrument(level = "trace")]
2124pub fn match_message_metadata(
2125  expected: &MessageContents,
2126  actual: &MessageContents,
2127  context: &dyn MatchingContext
2128) -> HashMap<String, Vec<Mismatch>> {
2129  let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2130    .map(|val| val.to_lowercase() == "v2")
2131    .unwrap_or(false);
2132  if use_v2_engine {
2133    let config = MatchingConfiguration {
2134      allow_unexpected_entries: true,
2135      .. MatchingConfiguration::init_from_env()
2136    };
2137    let plan_context = PlanMatchingContext {
2138      config,
2139      .. PlanMatchingContext::default()
2140    };
2141    match build_message_plan(expected, &plan_context) {
2142      Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2143        Ok(executed_plan) => {
2144          if config.log_executed_plan {
2145            debug!("config = {:?}", config);
2146            debug!("\n{}", executed_plan.pretty_form());
2147          }
2148          if config.log_plan_summary {
2149            info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2150          }
2151          if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2152            return metadata_mismatches(&message_node);
2153          }
2154          return hashmap!{};
2155        }
2156        Err(err) => warn!("Failed to execute message plan: {}", err)
2157      },
2158      Err(err) => warn!("Failed to build message plan: {}", err)
2159    }
2160  }
2161
2162  debug!("Matching message metadata");
2163  let mut result = hashmap!{};
2164  let expected_metadata = &expected.metadata;
2165  let actual_metadata = &actual.metadata;
2166  debug!("Matching message metadata. Expected '{:?}', Actual '{:?}'", expected_metadata, actual_metadata);
2167
2168  if !expected_metadata.is_empty() || context.config() == DiffConfig::NoUnexpectedKeys {
2169    for (key, value) in expected_metadata {
2170      match actual_metadata.get(key) {
2171        Some(actual_value) => {
2172          result.insert(key.clone(), match_metadata_value(key, value,
2173            actual_value, context).err().unwrap_or_default());
2174        },
2175        None => {
2176          result.insert(key.clone(), vec![Mismatch::MetadataMismatch { key: key.clone(),
2177            expected: json_to_string(&value),
2178            actual: "".to_string(),
2179            mismatch: format!("Expected message metadata '{}' but was missing", key) }]);
2180        }
2181      }
2182    }
2183  }
2184  result
2185}
2186
2187#[instrument(level = "trace")]
2188fn match_metadata_value(
2189  key: &str,
2190  expected: &Value,
2191  actual: &Value,
2192  context: &dyn MatchingContext
2193) -> Result<(), Vec<Mismatch>> {
2194  debug!("Comparing metadata values for key '{}'", key);
2195  let path = DocPath::root().join(key);
2196  let matcher_result = if context.matcher_is_defined(&path) {
2197    match_values(&path, &context.select_best_matcher(&path), expected, actual)
2198  } else if key.to_ascii_lowercase() == "contenttype" || key.to_ascii_lowercase() == "content-type" {
2199    debug!("Comparing message context type '{}' => '{}'", expected, actual);
2200    headers::match_parameter_header(expected.as_str().unwrap_or_default(), actual.as_str().unwrap_or_default(),
2201      key, "metadata", 0, true)
2202  } else {
2203    MatchingRule::Equality.match_value(expected, actual, false, false).map_err(|err| vec![err.to_string()])
2204  };
2205  matcher_result.map_err(|messages| {
2206    messages.iter().map(|message| {
2207      Mismatch::MetadataMismatch {
2208        key: key.to_string(),
2209        expected: expected.to_string(),
2210        actual: actual.to_string(),
2211        mismatch: format!("Expected metadata key '{}' to have value '{}' but was '{}' - {}", key, expected, actual, message)
2212      }
2213    }).collect()
2214  })
2215}
2216
2217/// Matches the actual and expected messages.
2218#[allow(unused_variables)]
2219pub async fn match_message<'a>(
2220  expected: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2221  actual: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2222  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2223  let mut mismatches = vec![];
2224
2225  if expected.is_message() && actual.is_message() {
2226    debug!("comparing to expected message: {:?}", expected);
2227    let expected_message = expected.as_message().unwrap();
2228    let actual_message = actual.as_message().unwrap();
2229
2230    let matching_rules = &expected_message.matching_rules;
2231
2232    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2233    {
2234      let plugin_data  = setup_plugin_config(pact, expected, InteractionPart::None);
2235
2236      let body_context = if expected.is_v4() {
2237        CoreMatchingContext {
2238          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2239          config: DiffConfig::AllowUnexpectedKeys,
2240          matching_spec: PactSpecification::V4,
2241          plugin_configuration: plugin_data.clone()
2242        }
2243      } else {
2244        CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2245          &matching_rules.rules_for_category("body").unwrap_or_default(),
2246          &plugin_data)
2247      };
2248
2249      let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2250        &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2251        &plugin_data);
2252      let result = match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2253      mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2254      for values in match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2255        mismatches.extend_from_slice(values.as_slice());
2256      }
2257    }
2258
2259    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2260    {
2261      let body_context = if expected.is_v4() {
2262        CoreMatchingContext {
2263          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2264          config: DiffConfig::AllowUnexpectedKeys,
2265          matching_spec: PactSpecification::V4
2266        }
2267      } else {
2268        CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2269          &matching_rules.rules_for_category("body").unwrap_or_default())
2270      };
2271
2272      let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2273        &matching_rules.rules_for_category("metadata").unwrap_or_default());
2274      let result = crate::match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2275      mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2276      for values in crate::match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2277        mismatches.extend_from_slice(values.as_slice());
2278      }
2279    }
2280  } else {
2281    mismatches.push(Mismatch::BodyTypeMismatch {
2282      expected: "message".into(),
2283      actual: actual.type_of(),
2284      mismatch: format!("Cannot compare a {} with a {}", expected.type_of(), actual.type_of()),
2285      expected_body: None,
2286      actual_body: None
2287    });
2288  }
2289
2290  mismatches
2291}
2292
2293/// Matches synchronous request/response messages
2294pub async fn match_sync_message<'a>(expected: SynchronousMessage, actual: SynchronousMessage, pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2295  let mut mismatches = match_sync_message_request(&expected, &actual, pact).await;
2296  let response_result = match_sync_message_response(&expected, &expected.response, &actual.response, pact).await;
2297  mismatches.extend_from_slice(&*response_result);
2298  mismatches
2299}
2300
2301/// Match the request part of a synchronous request/response message
2302#[allow(unused_variables)]
2303pub async fn match_sync_message_request<'a>(
2304  expected: &SynchronousMessage,
2305  actual: &SynchronousMessage,
2306  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2307) -> Vec<Mismatch> {
2308  debug!("comparing to expected message request: {:?}", expected);
2309
2310  let mut mismatches = vec![];
2311  let matching_rules = &expected.request.matching_rules;
2312
2313  #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2314  {
2315    let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2316
2317    let body_context = CoreMatchingContext {
2318      matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2319      config: DiffConfig::AllowUnexpectedKeys,
2320      matching_spec: PactSpecification::V4,
2321      plugin_configuration: plugin_data.clone()
2322    };
2323
2324    let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2325      &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2326      &plugin_data);
2327    let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2328
2329    mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2330    for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2331      mismatches.extend_from_slice(values.as_slice());
2332    }
2333  }
2334
2335  #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2336  {
2337    let body_context = CoreMatchingContext {
2338      matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2339      config: DiffConfig::AllowUnexpectedKeys,
2340      matching_spec: PactSpecification::V4
2341    };
2342
2343    let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2344      &matching_rules.rules_for_category("metadata").unwrap_or_default());
2345    let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2346
2347    mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2348    for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2349      mismatches.extend_from_slice(values.as_slice());
2350    }
2351  }
2352
2353  mismatches
2354}
2355
2356/// Match the response part of a synchronous request/response message
2357#[allow(unused_variables)]
2358pub async fn match_sync_message_response<'a>(
2359  expected: &SynchronousMessage,
2360  expected_responses: &[MessageContents],
2361  actual_responses: &[MessageContents],
2362  pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2363) -> Vec<Mismatch> {
2364  debug!("comparing to expected message responses: {:?}", expected_responses);
2365
2366  let mut mismatches = vec![];
2367
2368  if expected_responses.len() != actual_responses.len() {
2369    if !expected_responses.is_empty() && actual_responses.is_empty() {
2370      mismatches.push(Mismatch::BodyTypeMismatch {
2371        expected: "message response".into(),
2372        actual: "".into(),
2373        mismatch: "Expected a message with a response, but the actual response was empty".into(),
2374        expected_body: None,
2375        actual_body: None
2376      });
2377    } else if !expected_responses.is_empty() {
2378      mismatches.push(Mismatch::BodyTypeMismatch {
2379        expected: "message response".into(),
2380        actual: "".into(),
2381        mismatch: format!("Expected a message with {} responses, but the actual response had {}",
2382                          expected_responses.len(), actual_responses.len()),
2383        expected_body: None,
2384        actual_body: None
2385      });
2386    }
2387  } else {
2388
2389    #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2390    {
2391      let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2392      for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2393        let matching_rules = &expected_response.matching_rules;
2394        let body_context = CoreMatchingContext {
2395          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2396          config: DiffConfig::AllowUnexpectedKeys,
2397          matching_spec: PactSpecification::V4,
2398          plugin_configuration: plugin_data.clone()
2399        };
2400
2401        let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2402          &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2403          &plugin_data);
2404        let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2405
2406        mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2407        for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2408          mismatches.extend_from_slice(values.as_slice());
2409        }
2410      }
2411    }
2412
2413    #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2414    {
2415      for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2416        let matching_rules = &expected_response.matching_rules;
2417        let body_context = CoreMatchingContext {
2418          matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2419          config: DiffConfig::AllowUnexpectedKeys,
2420          matching_spec: PactSpecification::V4
2421        };
2422
2423        let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2424          &matching_rules.rules_for_category("metadata").unwrap_or_default());
2425        let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2426
2427        mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2428        for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2429          mismatches.extend_from_slice(values.as_slice());
2430        }
2431      }
2432    }
2433  }
2434  mismatches
2435}
2436
2437/// Generates the request by applying any defined generators
2438// TODO: Need to pass in any plugin data
2439#[instrument(level = "trace")]
2440pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpRequest {
2441  trace!(?request, ?mode, ?context, "generate_request");
2442  let mut request = request.clone();
2443
2444  let generators = request.build_generators(&GeneratorCategory::PATH);
2445  if !generators.is_empty() {
2446    debug!("Applying path generator...");
2447    apply_generators(mode, &generators, &mut |_, generator| {
2448      if let Ok(v) = generator.generate_value(&request.path, context, &DefaultVariantMatcher.boxed()) {
2449        request.path = v;
2450      }
2451    });
2452  }
2453
2454  let generators = request.build_generators(&GeneratorCategory::HEADER);
2455  if !generators.is_empty() {
2456    debug!("Applying header generators...");
2457    apply_generators(mode, &generators, &mut |key, generator| {
2458      if let Some(header) = key.first_field() {
2459        if let Some(ref mut headers) = request.headers {
2460          if headers.contains_key(header) {
2461            if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2462              headers.insert(header.to_string(), v);
2463            }
2464          } else {
2465            if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2466              headers.insert(header.to_string(), vec![ v.to_string() ]);
2467            }
2468          }
2469        } else {
2470          if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2471            request.headers = Some(hashmap!{
2472              header.to_string() => vec![ v.to_string() ]
2473            })
2474          }
2475        }
2476      }
2477    });
2478  }
2479
2480  let generators = request.build_generators(&GeneratorCategory::QUERY);
2481  if !generators.is_empty() {
2482    debug!("Applying query generators...");
2483    apply_generators(mode, &generators, &mut |key, generator| {
2484      if let Some(param) = key.first_field() {
2485        if let Some(ref mut parameters) = request.query {
2486          if let Some(parameter) = parameters.get_mut(param) {
2487            let mut generated = parameter.clone();
2488            for (index, val) in parameter.iter().enumerate() {
2489              let value = val.clone().unwrap_or_default();
2490              if let Ok(v) = generator.generate_value(&value, context, &DefaultVariantMatcher.boxed()) {
2491                generated[index] = Some(v);
2492              }
2493            }
2494            *parameter = generated;
2495          } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2496            parameters.insert(param.to_string(), vec![ Some(v.to_string()) ]);
2497          }
2498        } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2499          request.query = Some(hashmap!{
2500            param.to_string() => vec![ Some(v.to_string()) ]
2501          })
2502        }
2503      }
2504    });
2505  }
2506
2507  let generators = request.build_generators(&GeneratorCategory::BODY);
2508  if !generators.is_empty() && request.body.is_present() {
2509    debug!("Applying body generators...");
2510    match generators_process_body(mode, &request.body, request.content_type(),
2511                                  context, &generators, &DefaultVariantMatcher {}, &vec![], &hashmap!{}).await {
2512      Ok(body) => request.body = body,
2513      Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2514    }
2515  }
2516
2517  request
2518}
2519
2520/// Generates the response by applying any defined generators
2521// TODO: Need to pass in any plugin data
2522pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpResponse {
2523  trace!(?response, ?mode, ?context, "generate_response");
2524  let mut response = response.clone();
2525  let generators = response.build_generators(&GeneratorCategory::STATUS);
2526  if !generators.is_empty() {
2527    debug!("Applying status generator...");
2528    apply_generators(mode, &generators, &mut |_, generator| {
2529      if let Ok(v) = generator.generate_value(&response.status, context, &DefaultVariantMatcher.boxed()) {
2530        debug!("Generated value for status: {}", v);
2531        response.status = v;
2532      }
2533    });
2534  }
2535  let generators = response.build_generators(&GeneratorCategory::HEADER);
2536  if !generators.is_empty() {
2537    debug!("Applying header generators...");
2538    apply_generators(mode, &generators, &mut |key, generator| {
2539      if let Some(header) = key.first_field() {
2540        if let Some(ref mut headers) = response.headers {
2541          if headers.contains_key(header) {
2542            if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2543              headers.insert(header.to_string(), v);
2544            }
2545          } else {
2546            if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2547              headers.insert(header.to_string(), vec![ v.to_string() ]);
2548            }
2549          }
2550        } else {
2551          if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2552            response.headers = Some(hashmap!{
2553              header.to_string() => vec![ v.to_string() ]
2554            })
2555          }
2556        }
2557      }
2558    });
2559  }
2560  let generators = response.build_generators(&GeneratorCategory::BODY);
2561  if !generators.is_empty() && response.body.is_present() {
2562    debug!("Applying body generators...");
2563    match generators_process_body(mode, &response.body, response.content_type(),
2564      context, &generators, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await {
2565      Ok(body) => response.body = body,
2566      Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2567    }
2568  }
2569  response
2570}
2571
2572/// Matches the request part of the interaction
2573pub async fn match_interaction_request(
2574  expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2575  actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2576  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2577  _spec_version: &PactSpecification
2578) -> anyhow::Result<RequestMatchResult> {
2579  if let Some(http_interaction) = expected.as_v4_http() {
2580    let request = actual.as_v4_http()
2581      .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2582    match_request(http_interaction.request, request, &pact, &expected).await
2583  } else {
2584    Err(anyhow!("match_interaction_request must be called with HTTP request/response interactions, got {}", expected.type_of()))
2585  }
2586}
2587
2588/// Matches the response part of the interaction
2589pub async fn match_interaction_response(
2590  expected: Box<dyn Interaction + Sync + RefUnwindSafe>,
2591  actual: Box<dyn Interaction + Sync + RefUnwindSafe>,
2592  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2593  _spec_version: &PactSpecification
2594) -> anyhow::Result<Vec<Mismatch>> {
2595  if let Some(expected) = expected.as_v4_http() {
2596    let expected_response = expected.response.clone();
2597    let expected = expected.boxed();
2598    let response = actual.as_v4_http()
2599      .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2600    match_response(expected_response, response, &pact, &expected).await
2601  } else {
2602    Err(anyhow!("match_interaction_response must be called with HTTP request/response interactions, got {}", expected.type_of()))
2603  }
2604}
2605
2606/// Matches an interaction
2607pub async fn match_interaction(
2608  expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2609  actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2610  pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2611  _spec_version: &PactSpecification
2612) -> anyhow::Result<Vec<Mismatch>> {
2613  if let Some(expected) = expected.as_v4_http() {
2614    let expected_request = expected.request.clone();
2615    let expected_response = expected.response.clone();
2616    let expected = expected.boxed();
2617    let request = actual.as_v4_http()
2618      .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2619    let request_result = match_request(expected_request, request, &pact, &expected).await?;
2620    let response = actual.as_v4_http()
2621      .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2622    let response_result = match_response(expected_response, response, &pact, &expected).await?;
2623    let mut mismatches = request_result.mismatches();
2624    mismatches.extend_from_slice(&*response_result);
2625    Ok(mismatches)
2626  } else if expected.is_message() || expected.is_v4() {
2627    Ok(match_message(&expected, &actual, &pact).await)
2628  } else {
2629    Err(anyhow!("match_interaction must be called with either an HTTP request/response interaction or a Message, got {}", expected.type_of()))
2630  }
2631}
2632
2633#[cfg(test)]
2634mod tests;
2635#[cfg(test)]
2636mod generator_tests;