diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9de14a9fe..83eb72a5d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1505,6 +1505,8 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pact-plugin-driver" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e73f6bd46855faf0e4934c295d1fe5004777bc35594a630808d51b23fe111c0" dependencies = [ "anyhow", "async-trait", @@ -1516,7 +1518,7 @@ dependencies = [ "maplit", "md5", "os_info", - "pact_models 1.0.3", + "pact_models 1.0.2", "prost", "prost-types", "regex", @@ -1567,7 +1569,7 @@ dependencies = [ "pact-plugin-driver", "pact_matching", "pact_mock_server", - "pact_models 1.0.3", + "pact_models 1.0.2", "quickcheck", "rand", "regex", @@ -1607,7 +1609,7 @@ dependencies = [ "pact-plugin-driver", "pact_matching", "pact_mock_server", - "pact_models 1.0.3", + "pact_models 1.0.2", "pact_verifier", "quickcheck", "rand", @@ -1633,7 +1635,7 @@ dependencies = [ [[package]] name = "pact_matching" -version = "0.12.16" +version = "1.0.0" dependencies = [ "ansi_term", "anyhow", @@ -1657,7 +1659,7 @@ dependencies = [ "ntest", "onig", "pact-plugin-driver", - "pact_models 1.0.3", + "pact_models 1.0.2", "pretty_assertions", "quickcheck", "rand", @@ -1691,7 +1693,7 @@ dependencies = [ "maplit", "pact-plugin-driver", "pact_matching", - "pact_models 1.0.3", + "pact_models 1.0.2", "quickcheck", "reqwest", "rustls", @@ -1725,7 +1727,7 @@ dependencies = [ "maplit", "pact_matching", "pact_mock_server", - "pact_models 1.0.3", + "pact_models 1.0.2", "quickcheck", "rand", "regex", @@ -1841,7 +1843,7 @@ dependencies = [ "pact-plugin-driver", "pact_consumer", "pact_matching", - "pact_models 1.0.3", + "pact_models 1.0.2", "quickcheck", "regex", "reqwest", @@ -1868,7 +1870,7 @@ dependencies = [ "expectest", "log", "maplit", - "pact_models 1.0.3", + "pact_models 1.0.2", "pact_verifier", "regex", "reqwest", diff --git a/rust/pact_matching/Cargo.toml b/rust/pact_matching/Cargo.toml index 7adb5c7b7..2fabae46e 100644 --- a/rust/pact_matching/Cargo.toml +++ b/rust/pact_matching/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pact_matching" -version = "0.12.16" +version = "1.0.0" authors = ["Ronald Holshausen "] edition = "2021" description = "Pact-Rust support library that implements request and response matching logic" @@ -15,7 +15,7 @@ exclude = [ ] [dependencies] -pact_models = "1.0.1" +pact_models = "1.0.2" anyhow = "1.0.57" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" @@ -40,10 +40,10 @@ http = "0.2.7" mime = "0.3.16" bytes = { version = "1.1.0", features = ["serde"] } tokio = { version = "1.18.2", features = ["full"] } -pact-plugin-driver = "^0.1" +pact-plugin-driver = "0.2" md5 = "0.7.0" -tracing = "0.1.36" # This needs to be the same version across all the libs (i.e. plugin driver, pact ffi) -tracing-core = "0.1.29" # This needs to be the same version across all the pact libs (i.e. plugin driver, pact ffi) +tracing = "0.1.37" # This needs to be the same version across all the libs (i.e. plugin driver, pact ffi) +tracing-core = "0.1.30" # This needs to be the same version across all the pact libs (i.e. plugin driver, pact ffi) [dependencies.reqwest] version = "0.11.10" diff --git a/rust/pact_matching/src/generator_tests.rs b/rust/pact_matching/src/generator_tests.rs index 053c9a210..3fba792bb 100644 --- a/rust/pact_matching/src/generator_tests.rs +++ b/rust/pact_matching/src/generator_tests.rs @@ -5,7 +5,6 @@ use expectest::prelude::*; use serde_json::Value; use pact_models::bodies::OptionalBody; -use pact_models::content_types::{JSON, TEXT}; use pact_models::generators; use pact_models::generators::{ContentTypeHandler, Generator, JsonHandler}; use pact_models::path_exp::DocPath; @@ -86,31 +85,6 @@ async fn applies_query_generator_for_query_parameters_to_the_copy_of_the_request expect!(query_val).to_not(be_equal_to("a")); } -#[tokio::test] -async fn apply_generator_to_empty_body_test() { - expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Empty, - Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}).await.unwrap()).to(be_equal_to(OptionalBody::Empty)); - expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Null, - Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}).await.unwrap()).to(be_equal_to(OptionalBody::Null)); - expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Missing, - Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}).await.unwrap()).to(be_equal_to(OptionalBody::Missing)); -} - -#[tokio::test] -async fn do_not_apply_generators_if_there_are_no_body_generators() { - let body = OptionalBody::Present("{\"a\":100,\"b\":\"B\"}".into(), Some(JSON.clone()), None); - expect!(generators_process_body(&GeneratorTestMode::Provider, &body, Some(JSON.clone()), - &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}).await.unwrap()).to( - be_equal_to(body)); -} - -#[tokio::test] -async fn apply_generator_to_text_body_test() { - let body = OptionalBody::Present("some text".into(), None, None); - expect!(generators_process_body(&GeneratorTestMode::Provider, &body, Some(TEXT.clone()), - &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}).await.unwrap()).to(be_equal_to(body)); -} - #[tokio::test] async fn applies_body_generator_to_the_copy_of_the_request() { let request = HttpRequest { body: OptionalBody::Present("{\"a\": 100, \"b\": \"B\"}".into(), None, None), diff --git a/rust/pact_matching/src/generators/bodies.rs b/rust/pact_matching/src/generators/bodies.rs new file mode 100644 index 000000000..917796bbd --- /dev/null +++ b/rust/pact_matching/src/generators/bodies.rs @@ -0,0 +1,114 @@ +//! Functions to apply generators to body contents + +use std::collections::HashMap; + +use pact_plugin_driver::catalogue_manager::find_content_generator; +use serde_json::Value; +use tracing::{debug, error, warn}; + +use pact_models::bodies::OptionalBody; +use pact_models::content_types::ContentType; +use pact_models::generators::{ContentTypeHandler, Generator, GeneratorTestMode, JsonHandler, VariantMatcher}; +use pact_models::path_exp::DocPath; +use pact_models::plugins::PluginData; +use pact_models::xml_utils::parse_bytes; + +use crate::generators::XmlHandler; + +/// Apply the generators to the body, returning a new body +pub async fn generators_process_body( + mode: &GeneratorTestMode, + body: &OptionalBody, + content_type: Option, + context: &HashMap<&str, Value>, + generators: &HashMap, + matcher: &(dyn VariantMatcher + Send + Sync), + plugin_data: &Vec, + interaction_data: &HashMap> +) -> anyhow::Result { + match content_type { + Some(content_type) => if content_type.is_json() { + debug!("apply_body_generators: JSON content type"); + let result: Result = serde_json::from_slice(&body.value().unwrap_or_default()); + match result { + Ok(val) => { + let mut handler = JsonHandler { value: val }; + Ok(handler.process_body(generators, mode, context, &matcher.boxed()).unwrap_or_else(|err| { + error!("Failed to generate the body: {}", err); + body.clone() + })) + }, + Err(err) => { + error!("Failed to parse the body, so not applying any generators: {}", err); + Ok(body.clone()) + } + } + } else if content_type.is_xml() { + debug!("apply_body_generators: XML content type"); + match parse_bytes(&body.value().unwrap_or_default()) { + Ok(val) => { + let mut handler = XmlHandler { value: val.as_document() }; + Ok(handler.process_body(generators, mode, context, &matcher.boxed()).unwrap_or_else(|err| { + error!("Failed to generate the body: {}", err); + body.clone() + })) + }, + Err(err) => { + error!("Failed to parse the body, so not applying any generators: {}", err); + Ok(body.clone()) + } + } + } else if let Some(content_generator) = find_content_generator(&content_type) { + debug!("apply_body_generators: Found a content generator from a plugin"); + let generators = generators.iter() + .map(|(k, v)| (k.to_string(), v.clone())) + .collect(); + content_generator.generate_content(&content_type, &generators, body, plugin_data, interaction_data, context).await + } else { + warn!("Unsupported content type {} - Generators only support JSON and XML", content_type); + Ok(body.clone()) + }, + _ => Ok(body.clone()) + } +} + +#[cfg(test)] +mod tests { + use expectest::prelude::*; + use maplit::hashmap; + + use pact_models::bodies::OptionalBody; + use pact_models::content_types::{JSON, TEXT}; + use pact_models::generators::GeneratorTestMode; + + use super::generators_process_body; + use crate::DefaultVariantMatcher; + + #[tokio::test] + async fn apply_generator_to_empty_body_test() { + expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Empty, + Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}, &vec![], &hashmap!{}) + .await.unwrap()).to(be_equal_to(OptionalBody::Empty)); + expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Null, + Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}, &vec![], &hashmap!{}) + .await.unwrap()).to(be_equal_to(OptionalBody::Null)); + expect!(generators_process_body(&GeneratorTestMode::Provider, &OptionalBody::Missing, + Some(TEXT.clone()), &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}, &vec![], &hashmap!{}) + .await.unwrap()).to(be_equal_to(OptionalBody::Missing)); + } + + #[tokio::test] + async fn do_not_apply_generators_if_there_are_no_body_generators() { + let body = OptionalBody::Present("{\"a\":100,\"b\":\"B\"}".into(), Some(JSON.clone()), None); + expect!(generators_process_body(&GeneratorTestMode::Provider, &body, Some(JSON.clone()), + &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await.unwrap()).to( + be_equal_to(body)); + } + + #[tokio::test] + async fn apply_generator_to_text_body_test() { + let body = OptionalBody::Present("some text".into(), None, None); + expect!(generators_process_body(&GeneratorTestMode::Provider, &body, Some(TEXT.clone()), + &hashmap!{}, &hashmap!{}, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await.unwrap()).to(be_equal_to(body)); + } +} diff --git a/rust/pact_matching/src/generators.rs b/rust/pact_matching/src/generators/mod.rs similarity index 50% rename from rust/pact_matching/src/generators.rs rename to rust/pact_matching/src/generators/mod.rs index e70021702..5c2e3c370 100644 --- a/rust/pact_matching/src/generators.rs +++ b/rust/pact_matching/src/generators/mod.rs @@ -5,18 +5,32 @@ use std::collections::HashMap; use maplit::hashmap; use pact_models::bodies::OptionalBody; use pact_models::content_types::ContentType; -use pact_models::generators::{ContentTypeHandler, GenerateValue, Generator, GeneratorTestMode, JsonHandler, VariantMatcher}; +use pact_models::generators::{ + apply_generators, + ContentTypeHandler, + GenerateValue, + Generator, + GeneratorCategory, + GeneratorTestMode, + NoopVariantMatcher, + VariantMatcher +}; use pact_models::matchingrules::MatchingRuleCategory; use pact_models::path_exp::DocPath; -use pact_models::xml_utils::parse_bytes; -use pact_plugin_driver::catalogue_manager::find_content_generator; use serde_json::{self, Value}; use sxd_document::dom::Document; -use tracing::{debug, error, warn}; +use tracing::{debug, error}; +use pact_models::http_parts::HttpPart; +use pact_models::plugins::PluginData; +use pact_models::v4::async_message::AsynchronousMessage; +use pact_models::v4::message_parts::MessageContents; +use pact_models::v4::sync_message::SynchronousMessage; use crate::{CoreMatchingContext, DiffConfig, MatchingContext}; use crate::json::compare_json; +pub mod bodies; + /// Implementation of a content type handler for XML (currently unimplemented). pub struct XmlHandler<'a> { /// XML document to apply the generators to. @@ -47,6 +61,7 @@ impl <'a> ContentTypeHandler> for XmlHandler<'a> { } /// Apply the generators to the body, returning a new body +#[deprecated(note = "moved to the generators::bodies module", since = "0.12.16")] pub async fn generators_process_body( mode: &GeneratorTestMode, body: &OptionalBody, @@ -55,48 +70,7 @@ pub async fn generators_process_body( generators: &HashMap, matcher: &(dyn VariantMatcher + Send + Sync) ) -> anyhow::Result { - match content_type { - Some(content_type) => if content_type.is_json() { - debug!("apply_body_generators: JSON content type"); - let result: Result = serde_json::from_slice(&body.value().unwrap_or_default()); - match result { - Ok(val) => { - let mut handler = JsonHandler { value: val }; - Ok(handler.process_body(generators, mode, context, &matcher.boxed()).unwrap_or_else(|err| { - error!("Failed to generate the body: {}", err); - body.clone() - })) - }, - Err(err) => { - error!("Failed to parse the body, so not applying any generators: {}", err); - Ok(body.clone()) - } - } - } else if content_type.is_xml() { - debug!("apply_body_generators: XML content type"); - match parse_bytes(&body.value().unwrap_or_default()) { - Ok(val) => { - let mut handler = XmlHandler { value: val.as_document() }; - Ok(handler.process_body(generators, mode, context, &matcher.boxed()).unwrap_or_else(|err| { - error!("Failed to generate the body: {}", err); - body.clone() - })) - }, - Err(err) => { - error!("Failed to parse the body, so not applying any generators: {}", err); - Ok(body.clone()) - } - } - } else if let Some(content_generator) = find_content_generator(&content_type) { - debug!("apply_body_generators: Found a content generator from a plugin"); - content_generator.generate_content(&content_type, &generators.iter() - .map(|(k, v)| (k.to_string(), v.clone())).collect(), body).await - } else { - warn!("Unsupported content type {} - Generators only support JSON and XML", content_type); - Ok(body.clone()) - }, - _ => Ok(body.clone()) - } + bodies::generators_process_body(mode, body, content_type, context, generators, matcher, &vec![], &hashmap!{}).await } pub(crate) fn find_matching_variant( @@ -109,7 +83,7 @@ pub(crate) fn find_matching_variant( .find(|(index, rules, _)| { debug!("find_matching_variant: Comparing variant {} with value '{:?}'", index, value); let context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys, - rules, &hashmap!{}); + rules, &hashmap!{}); let matches = callback(&DocPath::root(), value, &context); debug!("find_matching_variant: Comparing variant {} => {}", index, matches); matches @@ -118,8 +92,9 @@ pub(crate) fn find_matching_variant( result.map(|(index, _, generators)| (*index, generators.clone())) } +/// Default implementation of a VariantMatcher #[derive(Debug, Clone)] -pub(crate) struct DefaultVariantMatcher; +pub struct DefaultVariantMatcher; impl VariantMatcher for DefaultVariantMatcher { fn find_matching_variant( @@ -138,6 +113,109 @@ impl VariantMatcher for DefaultVariantMatcher { } } +/// Apply any generators to the synchronous message contents and then return a copy of the +/// request and response contents +pub async fn apply_generators_to_sync_message( + message: &SynchronousMessage, + mode: &GeneratorTestMode, + context: &HashMap<&str, Value>, + plugin_data: &Vec, + interaction_data: &HashMap> +) -> (MessageContents, Vec) { + let mut request = message.request.clone(); + let variant_matcher = NoopVariantMatcher {}; + let vm_boxed = variant_matcher.boxed(); + + let generators = request.build_generators(&GeneratorCategory::METADATA); + if !generators.is_empty() { + debug!("Applying request metadata generators..."); + apply_generators(mode, &generators, &mut |key, generator| { + if let Some(k) = key.first_field() { + let value = request.metadata.get(k).cloned().unwrap_or_default(); + if let Ok(v) = generator.generate_value(&value, context, &vm_boxed) { + request.metadata.insert(k.to_string(), v); + } + } + }); + } + + let generators = request.build_generators(&GeneratorCategory::BODY); + if !generators.is_empty() && request.contents.is_present() { + debug!("Applying request content generators..."); + match bodies::generators_process_body(mode, &request.contents, request.content_type(), + context, &generators, &variant_matcher, plugin_data, interaction_data).await { + Ok(contents) => request.contents = contents, + Err(err) => error!("Failed to generate the message contents, will use the original: {}", err) + } + } + + let mut responses = message.response.clone(); + for response in responses.iter_mut() { + let generators = response.build_generators(&GeneratorCategory::METADATA); + if !generators.is_empty() { + debug!("Applying response metadata generators..."); + apply_generators(mode, &generators, &mut |key, generator| { + if let Some(k) = key.first_field() { + let value = response.metadata.get(k).cloned().unwrap_or_default(); + if let Ok(v) = generator.generate_value(&value, context, &vm_boxed) { + response.metadata.insert(k.to_string(), v); + } + } + }); + } + + let generators = response.build_generators(&GeneratorCategory::BODY); + if !generators.is_empty() && response.contents.is_present() { + debug!("Applying response content generators..."); + match bodies::generators_process_body(mode, &response.contents, response.content_type(), + context, &generators, &variant_matcher, plugin_data, interaction_data).await { + Ok(contents) => response.contents = contents, + Err(err) => error!("Failed to generate the message contents, will use the original: {}", err) + } + } + } + + (request, responses) +} + +/// Apply any generators to the asynchronous message contents and then return a copy of the contents +pub async fn apply_generators_to_async_message( + message: &AsynchronousMessage, + mode: &GeneratorTestMode, + context: &HashMap<&str, Value>, + plugin_data: &Vec, + interaction_data: &HashMap> +) -> MessageContents { + let mut copy = message.contents.clone(); + let variant_matcher = NoopVariantMatcher {}; + let vm_boxed = variant_matcher.boxed(); + + let generators = message.build_generators(&GeneratorCategory::METADATA); + if !generators.is_empty() { + debug!("Applying metadata generators..."); + apply_generators(mode, &generators, &mut |key, generator| { + if let Some(k) = key.first_field() { + let value = message.contents.metadata.get(k).cloned().unwrap_or_default(); + if let Ok(v) = generator.generate_value(&value, context, &vm_boxed) { + copy.metadata.insert(k.to_string(), v); + } + } + }); + } + + let generators = message.build_generators(&GeneratorCategory::BODY); + if !generators.is_empty() && message.contents.contents.is_present() { + debug!("Applying content generators..."); + match bodies::generators_process_body(mode, &message.contents.contents, message.contents.content_type(), + context, &generators, &variant_matcher, plugin_data, interaction_data).await { + Ok(contents) => copy.contents = contents, + Err(err) => error!("Failed to generate the message contents, will use the original: {}", err) + } + } + + copy +} + #[cfg(test)] mod tests { use expectest::prelude::*; diff --git a/rust/pact_matching/src/lib.rs b/rust/pact_matching/src/lib.rs index e37fa2bd0..93e42d323 100644 --- a/rust/pact_matching/src/lib.rs +++ b/rust/pact_matching/src/lib.rs @@ -376,7 +376,8 @@ use pact_plugin_driver::plugin_models::PluginInteractionConfig; use serde_json::{json, Value}; use tracing::{debug, error, info, warn}; -use crate::generators::{DefaultVariantMatcher, generators_process_body}; +use crate::generators::DefaultVariantMatcher; +use crate::generators::bodies::generators_process_body; use crate::headers::{match_header_value, match_headers}; use crate::json::match_json; use crate::matchers::*; @@ -397,11 +398,11 @@ pub mod json; pub mod logging; pub mod matchingrules; pub mod metrics; +pub mod generators; mod xml; mod binary_utils; mod headers; -mod generators; mod query; /// Context used to apply matching logic @@ -1743,6 +1744,7 @@ pub async fn match_sync_message_response<'a>( } /// Generates the request by applying any defined generators +// TODO: Need to pass in any plugin data pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpRequest { let mut request = request.clone(); @@ -1796,7 +1798,7 @@ pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, c if !generators.is_empty() && request.body.is_present() { debug!("Applying body generators..."); match generators_process_body(mode, &request.body, request.content_type(), - context, &generators, &DefaultVariantMatcher{}).await { + context, &generators, &DefaultVariantMatcher {}, &vec![], &hashmap!{}).await { Ok(body) => request.body = body, Err(err) => error!("Failed to generate the body, will use the original: {}", err) } @@ -1806,6 +1808,7 @@ pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, c } /// Generates the response by applying any defined generators +// TODO: Need to pass in any plugin data pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpResponse { let mut response = response.clone(); let generators = response.build_generators(&GeneratorCategory::STATUS); @@ -1841,7 +1844,7 @@ pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode if !generators.is_empty() && response.body.is_present() { debug!("Applying body generators..."); match generators_process_body(mode, &response.body, response.content_type(), - context, &generators, &DefaultVariantMatcher{}).await { + context, &generators, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await { Ok(body) => response.body = body, Err(err) => error!("Failed to generate the body, will use the original: {}", err) }