diff --git a/rust/pact_matching/src/lib.rs b/rust/pact_matching/src/lib.rs index 35aa8e85..0f075281 100644 --- a/rust/pact_matching/src/lib.rs +++ b/rust/pact_matching/src/lib.rs @@ -367,6 +367,11 @@ use bytes::Bytes; use itertools::{Either, Itertools}; use lazy_static::*; use maplit::{hashmap, hashset}; +#[cfg(feature = "plugins")] use pact_plugin_driver::catalogue_manager::find_content_matcher; +#[cfg(feature = "plugins")] use pact_plugin_driver::plugin_models::PluginInteractionConfig; +use serde_json::{json, Value}; +#[allow(unused_imports)] use tracing::{debug, error, info, instrument, trace, warn}; + use pact_models::bodies::OptionalBody; use pact_models::content_types::ContentType; use pact_models::generators::{apply_generators, GenerateValue, GeneratorCategory, GeneratorTestMode, VariantMatcher}; @@ -380,18 +385,14 @@ use pact_models::path_exp::DocPath; use pact_models::v4::http_parts::{HttpRequest, HttpResponse}; use pact_models::v4::message_parts::MessageContents; use pact_models::v4::sync_message::SynchronousMessage; -#[cfg(feature = "plugins")] use pact_plugin_driver::catalogue_manager::find_content_matcher; -#[cfg(feature = "plugins")] use pact_plugin_driver::plugin_models::PluginInteractionConfig; -use serde::__private::from_utf8_lossy; -use serde_json::{json, Value}; -#[allow(unused_imports)] use tracing::{debug, error, info, instrument, trace, warn}; -use crate::generators::DefaultVariantMatcher; use crate::generators::bodies::generators_process_body; +use crate::generators::DefaultVariantMatcher; use crate::headers::{match_header_value, match_headers}; #[cfg(feature = "plugins")] use crate::json::match_json; use crate::matchers::*; use crate::matchingrules::DisplayForMismatch; +#[cfg(feature = "plugins")] use crate::plugin_support::{InteractionPart, setup_plugin_config}; use crate::query::match_query_maps; /// Simple macro to convert a string slice to a `String` struct. @@ -415,6 +416,7 @@ pub mod binary_utils; pub mod headers; pub mod query; pub mod form_urlencoded; +#[cfg(feature = "plugins")] mod plugin_support; #[cfg(not(feature = "plugins"))] #[derive(Clone, Debug, PartialEq)] @@ -859,8 +861,8 @@ impl From for CommonMismatch { }, Mismatch::BodyMismatch { path, expected, actual, mismatch } => CommonMismatch { path: path.clone(), - expected: from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(), - actual: from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(), + expected: String::from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(), + actual: String::from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(), description: mismatch.clone() }, Mismatch::MetadataMismatch { key, expected, actual, mismatch } => CommonMismatch { @@ -1674,7 +1676,7 @@ pub async fn match_request<'a>( #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{}; #[cfg(feature = "plugins")] { - plugin_data = setup_plugin_config(pact, interaction); + plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Request); }; trace!("plugin_data = {:?}", plugin_data); @@ -1745,7 +1747,7 @@ pub async fn match_response<'a>( #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{}; #[cfg(feature = "plugins")] { - plugin_data = setup_plugin_config(pact, interaction); + plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Response); }; trace!("plugin_data = {:?}", plugin_data); @@ -1778,24 +1780,6 @@ pub async fn match_response<'a>( mismatches } -#[cfg(feature = "plugins")] -fn setup_plugin_config<'a>( - pact: &Box, - interaction: &Box -) -> HashMap { - pact.plugin_data().iter().map(|data| { - let interaction_config = if let Some(v4_interaction) = interaction.as_v4() { - v4_interaction.plugin_config().get(&data.name).cloned().unwrap_or_default() - } else { - hashmap! {} - }; - (data.name.clone(), PluginInteractionConfig { - pact_configuration: data.configuration.clone(), - interaction_configuration: interaction_config - }) - }).collect() -} - /// Matches the actual message contents to the expected one. This takes into account the content type of each. #[instrument(level = "trace")] pub async fn match_message_contents( @@ -1917,7 +1901,7 @@ pub async fn match_message<'a>( #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{}; #[cfg(feature = "plugins")] { - plugin_data = setup_plugin_config(pact, expected); + plugin_data = setup_plugin_config(pact, expected, InteractionPart::None); }; let body_context = if expected.is_v4() { @@ -1976,7 +1960,7 @@ pub async fn match_sync_message_request<'a>( #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{}; #[cfg(feature = "plugins")] { - plugin_data = setup_plugin_config(pact, &expected.boxed()); + plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None); }; let body_context = CoreMatchingContext { @@ -2034,7 +2018,7 @@ pub async fn match_sync_message_response<'a>( #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{}; #[cfg(feature = "plugins")] { - plugin_data = setup_plugin_config(pact, &expected.boxed()); + plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None); }; for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) { let matching_rules = &expected_response.matching_rules; diff --git a/rust/pact_matching/src/plugin_support.rs b/rust/pact_matching/src/plugin_support.rs new file mode 100644 index 00000000..d5f21a9e --- /dev/null +++ b/rust/pact_matching/src/plugin_support.rs @@ -0,0 +1,291 @@ +//! Support functions for dealing with content from plugins + +use std::collections::HashMap; +use std::panic::RefUnwindSafe; + +use maplit::hashmap; +use pact_plugin_driver::plugin_models::PluginInteractionConfig; +use serde_json::Map; + +use pact_models::interaction::Interaction; +use pact_models::pact::Pact; + +/// Which part of the interaction should the config be extracted +#[derive(Clone, Copy, Debug, Default)] +pub(crate) enum InteractionPart { + /// No part, use the whole config + #[default] None, + /// Request part under the "request" key + Request, + /// Response part under the "response" key + Response +} + +pub(crate) fn setup_plugin_config<'a>( + pact: &Box, + interaction: &Box, + part: InteractionPart +) -> HashMap { + pact.plugin_data().iter().map(|data| { + let interaction_config = if let Some(v4_interaction) = interaction.as_v4() { + if let Some(config) = v4_interaction.plugin_config().get(&data.name) { + // In some cases, depending on how the interaction is setup, the plugin configuration + // could be stored under a request or response key. + match part { + InteractionPart::None => config.clone(), + InteractionPart::Request => if let Some(request_config) = config.get("request") { + request_config + .as_object() + .cloned() + .unwrap_or_else(|| Map::new()) + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } else { + config.clone() + } + InteractionPart::Response => if let Some(response_config) = config.get("response") { + response_config + .as_object() + .cloned() + .unwrap_or_else(|| Map::new()) + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } else { + config.clone() + } + } + } else { + hashmap!{} + } + } else { + hashmap!{} + }; + (data.name.clone(), PluginInteractionConfig { + pact_configuration: data.configuration.clone(), + interaction_configuration: interaction_config + }) + }).collect() +} + +#[cfg(test)] +mod tests { + use expectest::prelude::*; + use maplit::hashmap; + use pact_plugin_driver::plugin_models::PluginInteractionConfig; + use serde_json::json; + use pact_models::interaction::Interaction; + use pact_models::pact::Pact; + use pact_models::plugins::PluginData; + use pact_models::v4::interaction::V4Interaction; + use pact_models::v4::pact::V4Pact; + use pact_models::v4::synch_http::SynchronousHttp; + + use crate::plugin_support::{InteractionPart, setup_plugin_config}; + + #[test] + fn setup_plugin_config_extracts_plugin_data_from_the_pact_object_for_the_interaction() { + let plugin1 = PluginData { + name: "plugin1".to_string(), + version: "1".to_string(), + configuration: hashmap!{ + "a".to_string() => json!(100) + } + }; + let plugin2 = PluginData { + name: "plugin2".to_string(), + version: "2".to_string(), + configuration: hashmap!{ + "b".to_string() => json!(200) + } + }; + let interaction1 = SynchronousHttp { + plugin_config: hashmap!{ + "plugin1".to_string() => hashmap!{ + "ia".to_string() => json!(1000) + } + }, + .. SynchronousHttp::default() + }; + let interaction2 = SynchronousHttp { + plugin_config: hashmap!{ + "plugin2".to_string() => hashmap!{ + "ib".to_string() => json!(2000) + } + }, + .. SynchronousHttp::default() + }; + let pact = V4Pact { + interactions: vec![interaction1.boxed_v4(), interaction2.boxed_v4()], + plugin_data: vec![plugin1, plugin2], + .. V4Pact::default() + }; + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::None); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "ia".to_string() => json!(1000) + } + }, + "plugin2".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "b".to_string() => json!(200) + }, + interaction_configuration: hashmap!{} + } + })); + + let result = setup_plugin_config(&pact.boxed(), &interaction2.boxed(), InteractionPart::None); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{} + }, + "plugin2".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "b".to_string() => json!(200) + }, + interaction_configuration: hashmap!{ + "ib".to_string() => json!(2000) + } + } + })); + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::Request); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "ia".to_string() => json!(1000) + } + }, + "plugin2".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "b".to_string() => json!(200) + }, + interaction_configuration: hashmap!{} + } + })); + } + + #[test] + fn setup_plugin_config_extracts_plugin_data_from_the_request_part_for_the_interaction() { + let plugin1 = PluginData { + name: "plugin1".to_string(), + version: "1".to_string(), + configuration: hashmap!{ + "a".to_string() => json!(100) + } + }; + let interaction1 = SynchronousHttp { + plugin_config: hashmap!{ + "plugin1".to_string() => hashmap!{ + "ia".to_string() => json!(1000), + "request".to_string() => json!({ + "req": "req_value" + }), + "response".to_string() => json!({ + "res": "res_value" + }) + } + }, + .. SynchronousHttp::default() + }; + let pact = V4Pact { + interactions: vec![interaction1.boxed_v4()], + plugin_data: vec![plugin1], + .. V4Pact::default() + }; + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::None); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "ia".to_string() => json!(1000), + "request".to_string() => json!({"req": "req_value"}), + "response".to_string() => json!({"res": "res_value"}) + } + } + })); + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::Request); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "req".to_string() => json!("req_value") + } + } + })); + } + + #[test] + fn setup_plugin_config_extracts_plugin_data_from_the_response_part_for_the_interaction() { + let plugin1 = PluginData { + name: "plugin1".to_string(), + version: "1".to_string(), + configuration: hashmap!{ + "a".to_string() => json!(100) + } + }; + let interaction1 = SynchronousHttp { + plugin_config: hashmap!{ + "plugin1".to_string() => hashmap!{ + "ia".to_string() => json!(1000), + "request".to_string() => json!({ + "req": "req_value" + }), + "response".to_string() => json!({ + "res": "res_value" + }) + } + }, + .. SynchronousHttp::default() + }; + let pact = V4Pact { + interactions: vec![interaction1.boxed_v4()], + plugin_data: vec![plugin1], + .. V4Pact::default() + }; + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::None); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "ia".to_string() => json!(1000), + "request".to_string() => json!({"req": "req_value"}), + "response".to_string() => json!({"res": "res_value"}) + } + } + })); + + let result = setup_plugin_config(&pact.boxed(), &interaction1.boxed(), InteractionPart::Response); + expect!(result).to(be_equal_to(hashmap!{ + "plugin1".to_string() => PluginInteractionConfig { + pact_configuration: hashmap!{ + "a".to_string() => json!(100) + }, + interaction_configuration: hashmap!{ + "res".to_string() => json!("res_value") + } + } + })); + } +}