diff --git a/rust/pact_ffi/IntegrationJson.md b/rust/pact_ffi/IntegrationJson.md index c4a68d7a3..4de165257 100644 --- a/rust/pact_ffi/IntegrationJson.md +++ b/rust/pact_ffi/IntegrationJson.md @@ -157,3 +157,86 @@ Here the `interests` attribute would be expanded to } } ``` + +## Supporting multiple matching rules + +Matching rules can be combined. These rules will be evaluated with an AND (i.e. all the rules must match successfully +for the result to be successful). The main reason to do this is to combine the `EachKey` and `EachValue` matching rules +on a map structure, but other rules make sense to combine (like the `include` matcher). + +To provide multiple matchers, you need to provide an array format. + +For example, assume you have an API that returns results for a document store where the documents are keyed based on some index: +```json +{ + "results": { + "AUK-155332": { + "title": "...", + "description": "...", + "link": "http://....", + "relatesTo": ["BAF-88654"] + } + } +} +``` + +Here you may want to provide a matching rule for the keys that they conform to the `AAA-NNNNNNN...` format, as well +as a type matcher for the values. + +So the resulting intermediate JSON would be something like: +```json +{ + "results": { + "pact:matcher:type": [ + { + "pact:matcher:type": "each-key", + "value": "AUK-155332", + "rules": [ + { + "pact:matcher:type": "regex", + "regex": "\\w{3}-\\d+" + } + ] + }, { + "pact:matcher:type": "each-value", + "rules": [ + { + "pact:matcher:type": "type" + } + ] + } + ], + "AUK-155332": { + "title": "...", + "description": "...", + "link": "http://....", + "relatesTo": ["BAF-88654"] + } + } +} +``` + +## Supporting matching rule definitions + +You can use the [matching rule definition expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html) +in the `pact:matcher:type` field. + +For example, with the previous document result JSON, you could then use the following for the `relatesTo` field: + +```json +{ + "relatesTo": { + "pact:matcher:type": "eachValue(matching(regex, '\\w{3}-\\d+', 'BAF-88654'))" + } +} +``` + +You can then also combine matchers: + +```json +{ + "relatesTo": { + "pact:matcher:type": "atLeast(1), atMost(10), eachValue(matching(regex, '\\w{3}-\\d+', 'BAF-88654'))" + } +} +``` diff --git a/rust/pact_ffi/src/mock_server/bodies.rs b/rust/pact_ffi/src/mock_server/bodies.rs index 7d26c68a3..d6523c6ae 100644 --- a/rust/pact_ffi/src/mock_server/bodies.rs +++ b/rust/pact_ffi/src/mock_server/bodies.rs @@ -2,7 +2,7 @@ use std::path::Path; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use bytes::{Bytes, BytesMut}; use lazy_static::lazy_static; use pact_models::bodies::OptionalBody; @@ -91,53 +91,50 @@ fn process_matcher( skip_matchers: bool, matcher_type: &Value ) -> Value { - let matcher_type = json_to_string(matcher_type); - let matching_rule = match matcher_type.as_str() { - "arrayContains" | "array-contains" => { - match obj.get("variants") { - Some(Value::Array(variants)) => { - let mut json_values = vec![]; - - let values = variants.iter().enumerate().map(|(index, variant)| { - let mut category = MatchingRuleCategory::empty("body"); - let mut generators = Generators::default(); - let value = match variant { - Value::Object(map) => { - process_object(map, &mut category, &mut generators, DocPath::root(), false) - } - _ => { - warn!("arrayContains: JSON for variant {} is not correctly formed: {}", index, variant); - Value::Null - } - }; - json_values.push(value); - (index, category, generators.categories.get(&GeneratorCategory::BODY).cloned().unwrap_or_default()) - }).collect(); + let is_array_contains = match matcher_type { + Value::String(s) => s == "arrayContains" || s == "array-contains", + _ => false + }; - Ok((Some(MatchingRule::ArrayContains(values)), Value::Array(json_values))) - } - _ => Err(anyhow!("ArrayContains 'variants' attribute is missing or not an array")) + let matching_rule_result = if is_array_contains { + match obj.get("variants") { + Some(Value::Array(variants)) => { + let mut json_values = vec![]; + + let values = variants.iter().enumerate().map(|(index, variant)| { + let mut category = MatchingRuleCategory::empty("body"); + let mut generators = Generators::default(); + let value = match variant { + Value::Object(map) => { + process_object(map, &mut category, &mut generators, DocPath::root(), false) + } + _ => { + warn!("arrayContains: JSON for variant {} is not correctly formed: {}", index, variant); + Value::Null + } + }; + json_values.push(value); + (index, category, generators.categories.get(&GeneratorCategory::BODY).cloned().unwrap_or_default()) + }).collect(); + + Ok((vec!(MatchingRule::ArrayContains(values)), Value::Array(json_values))) } - }, - _ => { - let attributes = Value::Object(obj.clone()); - let (rule, is_values_matcher) = match MatchingRule::create(matcher_type.as_str(), &attributes) { - Ok(rule) => (Some(rule.clone()), rule.is_values_matcher()), - Err(err) => { - error!("Failed to parse matching rule from JSON - {}", err); - (None, false) - } - }; + _ => Err(anyhow!("ArrayContains 'variants' attribute is missing or not an array")) + } + } else { + matchers_from_integration_json(obj).map(|rules| { + let has_values_matcher = rules.iter().any(MatchingRule::is_values_matcher); + let json_value = match obj.get("value") { Some(inner) => match inner { - Value::Object(ref map) => process_object(map, matching_rules, generators, path.clone(), is_values_matcher), + Value::Object(map) => process_object(map, matching_rules, generators, path.clone(), has_values_matcher), Value::Array(array) => process_array(array, matching_rules, generators, path.clone(), true, skip_matchers), _ => inner.clone() }, None => Value::Null }; - Ok((rule, json_value)) - } + (rules, json_value) + }) }; if let Some(gen) = obj.get("pact:generator:type") { @@ -158,10 +155,10 @@ fn process_matcher( } } - trace!("matching_rule = {matching_rule:?}"); - match &matching_rule { - Ok((rule, value)) => { - if let Some(rule) = rule { + trace!("matching_rules = {matching_rule_result:?}"); + match &matching_rule_result { + Ok((rules, value)) => { + for rule in rules { matching_rules.add_rule(path.clone(), rule.clone(), RuleLogic::And); } value.clone() @@ -174,7 +171,7 @@ fn process_matcher( } /// Builds a `MatchingRule` from a `Value` struct used by language integrations -#[deprecated(note = "Replace with MatchingRule::create")] +#[deprecated(note = "Replace with MatchingRule::create or matchers_from_integration_json")] pub fn matcher_from_integration_json(m: &Map) -> Option { match m.get("pact:matcher:type") { Some(value) => { @@ -187,6 +184,43 @@ pub fn matcher_from_integration_json(m: &Map) -> Option) -> anyhow::Result> { + match m.get("pact:matcher:type") { + Some(value) => match value { + Value::Array(arr) => { + let mut rules = vec![]; + for v in arr { + match v.get("pact:matcher:type") { + Some(t) => { + let val = json_to_string(t); + let rule = MatchingRule::create(val.as_str(), &v) + .inspect_err(|err| { + error!("Failed to create matching rule from JSON '{:?}': {}", m, err); + })?; + rules.push(rule); + } + None => { + error!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", v); + bail!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", v); + } + } + } + Ok(rules) + } + _ => { + let val = json_to_string(value); + MatchingRule::create(val.as_str(), &Value::Object(m.clone())) + .map(|r| vec![r]) + .inspect_err(|err| { + error!("Failed to create matching rule from JSON '{:?}': {}", m, err); + }) + } + }, + _ => Ok(vec![]) + } +} + /// Process a JSON body with embedded matching rules and generators pub fn process_json(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> String { trace!("process_json"); @@ -375,6 +409,7 @@ mod test { use pact_models::path_exp::DocPath; use serde_json::json; use pretty_assertions::assert_eq; + use rstest::rstest; #[allow(deprecated)] use crate::mock_server::bodies::{matcher_from_integration_json, process_object}; @@ -766,6 +801,104 @@ mod test { }))); } + #[rstest] + #[case(json!({}), vec![])] + #[case(json!({ "pact:matcher:type": "regex", "regex": "[a-z]" }), vec![MatchingRule::Regex("[a-z]".to_string())])] + #[case(json!({ "pact:matcher:type": "equality" }), vec![MatchingRule::Equality])] + #[case(json!({ "pact:matcher:type": "include", "value": "[a-z]" }), vec![MatchingRule::Include("[a-z]".to_string())])] + #[case(json!({ "pact:matcher:type": "type" }), vec![MatchingRule::Type])] + #[case(json!({ "pact:matcher:type": "type", "min": 100 }), vec![MatchingRule::MinType(100)])] + #[case(json!({ "pact:matcher:type": "type", "max": 100 }), vec![MatchingRule::MaxType(100)])] + #[case(json!({ "pact:matcher:type": "type", "min": 10, "max": 100 }), vec![MatchingRule::MinMaxType(10, 100)])] + #[case(json!({ "pact:matcher:type": "number" }), vec![MatchingRule::Number])] + #[case(json!({ "pact:matcher:type": "integer" }), vec![MatchingRule::Integer])] + #[case(json!({ "pact:matcher:type": "decimal" }), vec![MatchingRule::Decimal])] + #[case(json!({ "pact:matcher:type": "real" }), vec![MatchingRule::Decimal])] + #[case(json!({ "pact:matcher:type": "min", "min": 100 }), vec![MatchingRule::MinType(100)])] + #[case(json!({ "pact:matcher:type": "max", "max": 100 }), vec![MatchingRule::MaxType(100)])] + #[case(json!({ "pact:matcher:type": "timestamp" }), vec![MatchingRule::Timestamp("".to_string())])] + #[case(json!({ "pact:matcher:type": "timestamp", "format": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "timestamp", "timestamp": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "datetime" }), vec![MatchingRule::Timestamp("".to_string())])] + #[case(json!({ "pact:matcher:type": "datetime", "format": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "datetime", "datetime": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "date" }), vec![MatchingRule::Date("".to_string())])] + #[case(json!({ "pact:matcher:type": "date", "format": "yyyy-MM-dd" }), vec![MatchingRule::Date("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "date", "date": "yyyy-MM-dd" }), vec![MatchingRule::Date("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "time" }), vec![MatchingRule::Time("".to_string())])] + #[case(json!({ "pact:matcher:type": "time", "format": "yyyy-MM-dd" }), vec![MatchingRule::Time("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "time", "time": "yyyy-MM-dd" }), vec![MatchingRule::Time("yyyy-MM-dd".to_string())])] + #[case(json!({ "pact:matcher:type": "null" }), vec![MatchingRule::Null])] + #[case(json!({ "pact:matcher:type": "boolean" }), vec![MatchingRule::Boolean])] + #[case(json!({ "pact:matcher:type": "contentType", "value": "text/plain" }), vec![MatchingRule::ContentType("text/plain".to_string())])] + #[case(json!({ "pact:matcher:type": "content-type", "value": "text/plain" }), vec![MatchingRule::ContentType("text/plain".to_string())])] + #[case(json!({ "pact:matcher:type": "arrayContains", "variants": [] }), vec![MatchingRule::ArrayContains(vec![])])] + #[case(json!({ "pact:matcher:type": "array-contains", "variants": [] }), vec![MatchingRule::ArrayContains(vec![])])] + #[case(json!({ "pact:matcher:type": "values" }), vec![MatchingRule::Values])] + #[case(json!({ "pact:matcher:type": "statusCode" }), vec![MatchingRule::StatusCode(HttpStatus::Success)])] + #[case(json!({ "pact:matcher:type": "statusCode" }), vec![MatchingRule::StatusCode(HttpStatus::StatusCodes(vec![200]))])] + #[case(json!({ "pact:matcher:type": "status-code" }), vec![MatchingRule::StatusCode(HttpStatus::Success)])] + #[case(json!({ "pact:matcher:type": "status-code" }), vec![MatchingRule::StatusCode(HttpStatus::StatusCodes(vec![200]))])] + #[case(json!({ "pact:matcher:type": "notEmpty" }), vec![MatchingRule::NotEmpty])] + #[case(json!({ "pact:matcher:type": "not-empty" }), vec![MatchingRule::NotEmpty])] + #[case(json!({ "pact:matcher:type": "semver" }), vec![MatchingRule::Semver])] + #[case(json!({ "pact:matcher:type": "eachKey" }), vec![MatchingRule::EachKey(MatchingRuleDefinition { + value: "".to_string(), + value_type: ValueType::Unknown, + rules: vec![], + generator: None, + })])] + #[case(json!({ "pact:matcher:type": "each-key" }), vec![MatchingRule::EachKey(MatchingRuleDefinition { + value: "".to_string(), + value_type: ValueType::Unknown, + rules: vec![], + generator: None, + })])] + #[case(json!({ "pact:matcher:type": "eachValue" }), vec![MatchingRule::EachValue(MatchingRuleDefinition { + value: "".to_string(), + value_type: ValueType::Unknown, + rules: vec![], + generator: None, + })])] + #[case(json!({ "pact:matcher:type": "each-value" }), vec![MatchingRule::EachValue(MatchingRuleDefinition { + value: "".to_string(), + value_type: ValueType::Unknown, + rules: vec![], + generator: None, + })])] + #[case(json!({ "pact:matcher:type": [{"pact:matcher:type": "regex", "regex": "[a-z]"}] }), vec![MatchingRule::Regex("[a-z]".to_string())])] + #[case(json!({ "pact:matcher:type": [ + { "pact:matcher:type": "regex", "regex": "[a-z]" }, + { "pact:matcher:type": "equality" }, + { "pact:matcher:type": "include", "value": "[a-z]" } + ] }), vec![MatchingRule::Regex("[a-z]".to_string()), MatchingRule::Equality, MatchingRule::Include("[a-z]".to_string())])] + fn matchers_from_integration_json_ok_test(#[case] json: Value, #[case] value: Vec) { + expect!(matchers_from_integration_json(&json.as_object().unwrap())).to(be_ok().value(value)); + } + + #[rstest] + #[case(json!({ "pact:matcher:type": "Other" }), "Other is not a valid matching rule type")] + #[case(json!({ "pact:matcher:type": "regex" }), "Regex matcher missing 'regex' field")] + #[case(json!({ "pact:matcher:type": "include" }), "Include matcher missing 'value' field")] + #[case(json!({ "pact:matcher:type": "min" }), "Min matcher missing 'min' field")] + #[case(json!({ "pact:matcher:type": "max" }), "Max matcher missing 'max' field")] + #[case(json!({ "pact:matcher:type": "contentType" }), "ContentType matcher missing 'value' field")] + #[case(json!({ "pact:matcher:type": "content-type" }), "ContentType matcher missing 'value' field")] + #[case(json!({ "pact:matcher:type": "arrayContains" }), "ArrayContains matcher missing 'variants' field")] + #[case(json!({ "pact:matcher:type": "array-contains" }), "ArrayContains matcher missing 'variants' field")] + #[case(json!({ "pact:matcher:type": "arrayContains", "variants": "text" }), "ArrayContains matcher 'variants' field is not an Array")] + #[case(json!({ "pact:matcher:type": "array-contains", "variants": "text" }), "ArrayContains matcher 'variants' field is not an Array")] + #[case(json!({ "pact:matcher:type": [ + { "pact:matcher:type": "regex", "regex": "[a-z]" }, + { "pact:matcher:type": "equality" }, + { "pact:matcher:type": "include" } + ]}), "Include matcher missing 'value' field")] + fn matchers_from_integration_json_error_test(#[case] json: Value, #[case] error: &str) { + expect!(matchers_from_integration_json(&json.as_object().unwrap()) + .unwrap_err().to_string()) + .to(be_equal_to(error)); + } + #[test_log::test] fn request_multipart_test() { let mut request = HttpRequest::default(); diff --git a/rust/pact_ffi/src/mock_server/handles.rs b/rust/pact_ffi/src/mock_server/handles.rs index 336d5b7c8..5d202334b 100644 --- a/rust/pact_ffi/src/mock_server/handles.rs +++ b/rust/pact_ffi/src/mock_server/handles.rs @@ -13,7 +13,7 @@ //! pactffi_response_status, //! pactffi_upon_receiving, //! pactffi_with_body, -//! pactffi_with_header, +//! pactffi_with_header_v2, //! pactffi_with_query_parameter_v2, //! pactffi_with_request //! }; @@ -50,14 +50,14 @@ //! // Setup the request //! pactffi_upon_receiving(interaction.clone(), description.as_ptr()); //! pactffi_with_request(interaction.clone(), method .as_ptr(), path_matcher.as_ptr()); -//! pactffi_with_header(interaction.clone(), InteractionPart::Request, content_type.as_ptr(), 0, value_header_with_matcher.as_ptr()); -//! pactffi_with_header(interaction.clone(), InteractionPart::Request, authorization.as_ptr(), 0, auth_header_with_matcher.as_ptr()); +//! pactffi_with_header_v2(interaction.clone(), InteractionPart::Request, content_type.as_ptr(), 0, value_header_with_matcher.as_ptr()); +//! pactffi_with_header_v2(interaction.clone(), InteractionPart::Request, authorization.as_ptr(), 0, auth_header_with_matcher.as_ptr()); //! pactffi_with_query_parameter_v2(interaction.clone(), query.as_ptr(), 0, query_param_matcher.as_ptr()); //! pactffi_with_body(interaction.clone(), InteractionPart::Request, header.as_ptr(), request_body_with_matchers.as_ptr()); //! //! // will respond with... -//! pactffi_with_header(interaction.clone(), InteractionPart::Response, content_type.as_ptr(), 0, value_header_with_matcher.as_ptr()); -//! pactffi_with_header(interaction.clone(), InteractionPart::Response, special_header.as_ptr(), 0, value_header_with_matcher.as_ptr()); +//! pactffi_with_header_v2(interaction.clone(), InteractionPart::Response, content_type.as_ptr(), 0, value_header_with_matcher.as_ptr()); +//! pactffi_with_header_v2(interaction.clone(), InteractionPart::Response, special_header.as_ptr(), 0, value_header_with_matcher.as_ptr()); //! pactffi_with_body(interaction.clone(), InteractionPart::Response, header.as_ptr(), response_body_with_matchers.as_ptr()); //! pactffi_response_status(interaction.clone(), 200); //! @@ -150,7 +150,7 @@ use crate::mock_server::{StringResult, xml}; use crate::mock_server::bodies::{ empty_multipart_body, file_as_multipart_body, - matcher_from_integration_json, + matchers_from_integration_json, MultipartBody, process_array, process_json, @@ -973,10 +973,9 @@ fn from_integration_json_v2( Ok(json) => match json { Value::Object(ref map) => { let result = if map.contains_key("pact:matcher:type") { - debug!("detected pact:matcher:type, will configure a matcher"); - #[allow(deprecated)] - let matching_rule = matcher_from_integration_json(map); - trace!("matching_rule = {matching_rule:?}"); + debug!("detected pact:matcher:type, will configure any matchers"); + let rules = matchers_from_integration_json(map); + trace!("matching_rules = {rules:?}"); let (path, result_value) = match map.get("value") { Some(val) => match val { @@ -989,7 +988,7 @@ fn from_integration_json_v2( None => (path.clone(), Value::Null) }; - if let Some(rule) = &matching_rule { + if let Ok(rules) = &rules { let path = if path_or_status { path.parent().unwrap_or(DocPath::root()) } else { @@ -1000,16 +999,21 @@ fn from_integration_json_v2( // If the index > 0, and there is an existing entry with the base name, we need // to re-key that with an index of 0 let mut parent = path.parent().unwrap_or(DocPath::root()); - if let Entry::Occupied(rule) = matching_rules.rules.entry(parent.clone()) { - let rules = rule.remove(); - matching_rules.rules.insert(parent.push_index(0).clone(), rules); + if let Entry::Occupied(rule_entry) = matching_rules.rules.entry(parent.clone()) { + let rules_list = rule_entry.remove(); + matching_rules.rules.insert(parent.push_index(0).clone(), rules_list); } } - matching_rules.add_rule(path, rule.clone(), RuleLogic::And); + for rule in rules { + matching_rules.add_rule(path.clone(), rule.clone(), RuleLogic::And); + } } else { - matching_rules.add_rule(path, rule.clone(), RuleLogic::And); + for rule in rules { + matching_rules.add_rule(path.clone(), rule.clone(), RuleLogic::And); + } } } + if let Some(gen) = map.get("pact:generator:type") { debug!("detected pact:generator:type, will configure a generators"); if let Some(generator) = Generator::from_map(&json_to_string(gen), map) { diff --git a/rust/pact_ffi/src/mock_server/xml.rs b/rust/pact_ffi/src/mock_server/xml.rs index d59b5b60b..8524d4bd2 100644 --- a/rust/pact_ffi/src/mock_server/xml.rs +++ b/rust/pact_ffi/src/mock_server/xml.rs @@ -1,23 +1,24 @@ //! XML matching support -use pact_models::matchingrules::{MatchingRuleCategory}; -use serde_json::Value::Number; -use sxd_document::dom::{Document, Element, ChildOfElement, Text}; -use serde_json::Value; +use std::collections::HashMap; + +use either::Either; +use maplit::hashmap; use serde_json::map::Map; +use serde_json::Value; +use serde_json::Value::Number; +use sxd_document::dom::{ChildOfElement, Document, Element, Text}; use sxd_document::Package; use sxd_document::writer::format_document; -use pact_models::matchingrules::{RuleLogic}; -use pact_models::generators::{Generators, GeneratorCategory, Generator}; +use tracing::{debug, trace, warn}; + +use pact_models::generators::{Generator, GeneratorCategory, Generators}; use pact_models::json_utils::json_to_string; -use log::*; -use std::collections::HashMap; -use maplit::hashmap; -use either::Either; +use pact_models::matchingrules::MatchingRuleCategory; +use pact_models::matchingrules::RuleLogic; use pact_models::path_exp::DocPath; -#[allow(deprecated)] -use crate::mock_server::bodies::matcher_from_integration_json; +use crate::mock_server::bodies::matchers_from_integration_json; pub fn generate_xml_body(attributes: &Map, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> Result, String> { let package = Package::new(); @@ -70,9 +71,10 @@ fn create_element_from_json<'a>( updated_path.push(&name); let doc_path = DocPath::new(updated_path.join(".").to_string()).unwrap_or(DocPath::root()); - #[allow(deprecated)] - if let Some(rule) = matcher_from_integration_json(object) { - matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + if let Ok(rules) = matchers_from_integration_json(object) { + for rule in rules { + matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + } } if let Some(gen) = object.get("pact:generator:type") { match Generator::from_map(&json_to_string(gen), object) { @@ -124,9 +126,10 @@ fn create_element_from_json<'a>( let doc_path = DocPath::new(&text_path.join(".")).unwrap_or(DocPath::root()); if let Value::Object(matcher) = matcher { - #[allow(deprecated)] - if let Some(rule) = matcher_from_integration_json(matcher) { - matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + if let Ok(rules) = matchers_from_integration_json(matcher) { + for rule in rules { + matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + } } } if let Some(gen) = object.get("pact:generator:type") { @@ -214,8 +217,10 @@ fn add_attributes( Value::Object(matcher_definition) => if matcher_definition.contains_key("pact:matcher:type") { let doc_path = DocPath::new(path).unwrap_or(DocPath::root()); #[allow(deprecated)] - if let Some(rule) = matcher_from_integration_json(matcher_definition) { - matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + if let Ok(rules) = matchers_from_integration_json(matcher_definition) { + for rule in rules { + matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); + } } if let Some(gen) = matcher_definition.get("pact:generator:type") { match Generator::from_map(&json_to_string(gen), matcher_definition) { diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs index e09dbc6d4..78159a096 100644 --- a/rust/pact_ffi/tests/tests.rs +++ b/rust/pact_ffi/tests/tests.rs @@ -9,6 +9,7 @@ use bytes::Bytes; use expectest::prelude::*; use itertools::Itertools; use libc::c_char; +use log::LevelFilter; use maplit::*; use pact_models::bodies::OptionalBody; use pact_models::PactSpecification; @@ -26,7 +27,8 @@ use pact_ffi::mock_server::{ pactffi_create_mock_server, pactffi_create_mock_server_for_pact, pactffi_mock_server_mismatches, - pactffi_write_pact_file + pactffi_write_pact_file, + pactffi_mock_server_logs }; #[allow(deprecated)] use pact_ffi::mock_server::handles::{ @@ -74,6 +76,7 @@ use pact_ffi::verifier::{ pactffi_verifier_set_provider_info, pactffi_verifier_shutdown }; +use pact_ffi::log::pactffi_log_to_buffer; #[test] fn post_to_mock_server_with_mismatches() { @@ -405,7 +408,7 @@ fn http_consumer_feature_test() { let content_type = CString::new("Content-Type").unwrap(); let authorization = CString::new("Authorization").unwrap(); let path_matcher = CString::new("{\"value\":\"/request/1234\",\"pact:matcher:type\":\"regex\", \"regex\":\"\\/request\\/[0-9]+\"}").unwrap(); - let value_header_with_matcher = CString::new("{\"value\":\"application/json\",\"pact:matcher:type\":\"dummy\"}").unwrap(); + let value_header_with_matcher = CString::new("{\"value\":\"application/json\",\"pact:matcher:type\":\"regex\",\"regex\":\"\\\\w+\\/\\\\w+\"}").unwrap(); let auth_header_with_matcher = CString::new("{\"value\":\"Bearer 1234\",\"pact:matcher:type\":\"regex\", \"regex\":\"Bearer [0-9]+\"}").unwrap(); let query_param_matcher = CString::new("{\"value\":\"bar\",\"pact:matcher:type\":\"regex\", \"regex\":\"(bar|baz|bat)\"}").unwrap(); let request_body_with_matchers = CString::new("{\"id\": {\"value\":1,\"pact:matcher:type\":\"type\"}}").unwrap(); @@ -1254,3 +1257,88 @@ fn provider_states_ignoring_parameter_types() { }"# ); } + +// Issue #399 +#[test_log::test] +fn combined_each_key_and_each_value_matcher() { + let consumer_name = CString::new("combined_matcher-consumer").unwrap(); + let provider_name = CString::new("combined_matcher-provider").unwrap(); + let pact_handle = pactffi_new_pact(consumer_name.as_ptr(), provider_name.as_ptr()); + let description = CString::new("combined_matcher").unwrap(); + let interaction = pactffi_new_interaction(pact_handle.clone(), description.as_ptr()); + + let content_type = CString::new("application/json").unwrap(); + let path = CString::new("/query").unwrap(); + let json = json!({ + "results": { + "pact:matcher:type": [ + { + "pact:matcher:type": "each-key", + "value": "AUK-155332", + "rules": [ + { + "pact:matcher:type": "regex", + "regex": "\\w{3}-\\d+" + } + ] + }, { + "pact:matcher:type": "each-value", + "rules": [ + { + "pact:matcher:type": "type" + } + ] + } + ], + "AUK-155332": { + "title": "...", + "description": "...", + "link": "http://....", + "relatesTo": ["BAF-88654"] + } + } + }); + let body = CString::new(json.to_string()).unwrap(); + let address = CString::new("127.0.0.1:0").unwrap(); + let method = CString::new("PUT").unwrap(); + + pactffi_upon_receiving(interaction.clone(), description.as_ptr()); + pactffi_with_request(interaction.clone(), method.as_ptr(), path.as_ptr()); + pactffi_with_body(interaction.clone(), InteractionPart::Request, content_type.as_ptr(), body.as_ptr()); + pactffi_response_status(interaction.clone(), 200); + + let port = pactffi_create_mock_server_for_pact(pact_handle.clone(), address.as_ptr(), false); + + expect!(port).to(be_greater_than(0)); + + let client = Client::default(); + let json_body = json!({ + "results": { + "KGK-9954356": { + "title": "Some title", + "description": "Tells us what this is in more or less detail", + "link": "http://....", + "relatesTo": ["BAF-88654"] + } + } + }); + let result = client.put(format!("http://127.0.0.1:{}/query", port).as_str()) + .header("Content-Type", "application/json") + .body(json_body.to_string()) + .send(); + + let mismatches = pactffi_mock_server_mismatches(port); + println!("{}", unsafe { CStr::from_ptr(mismatches) }.to_string_lossy()); + + pactffi_cleanup_mock_server(port); + pactffi_free_pact_handle(pact_handle); + + match result { + Ok(res) => { + expect!(res.status()).to(be_eq(200)); + }, + Err(err) => { + panic!("expected 200 response but request failed: {}", err); + } + }; +}