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