From 85dc3e95a3b01693c523d1e664d9f2991781f79a Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 19 Mar 2024 13:57:54 +1100 Subject: [PATCH 1/4] feat(ffi): Update intermediate JSON to support combining matching rules #399 --- rust/pact_ffi/IntegrationJson.md | 83 +++++++++ rust/pact_ffi/src/mock_server/bodies.rs | 221 ++++++++++++++++++----- rust/pact_ffi/src/mock_server/handles.rs | 36 ++-- rust/pact_ffi/src/mock_server/xml.rs | 45 +++-- rust/pact_ffi/tests/tests.rs | 92 +++++++++- 5 files changed, 395 insertions(+), 82 deletions(-) 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); + } + }; +} From 09077457e97828d522def08c85b76d86041ab715 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 19 Mar 2024 14:16:55 +1100 Subject: [PATCH 2/4] chore(ffi): Fix for failing musl build #399 --- rust/pact_ffi/src/mock_server/bodies.rs | 6 ++++-- rust/pact_ffi/tests/tests.rs | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rust/pact_ffi/src/mock_server/bodies.rs b/rust/pact_ffi/src/mock_server/bodies.rs index d6523c6ae..bb93a0d7e 100644 --- a/rust/pact_ffi/src/mock_server/bodies.rs +++ b/rust/pact_ffi/src/mock_server/bodies.rs @@ -195,8 +195,9 @@ pub fn matchers_from_integration_json(m: &Map) -> anyhow::Result< Some(t) => { let val = json_to_string(t); let rule = MatchingRule::create(val.as_str(), &v) - .inspect_err(|err| { + .map_err(|err| { error!("Failed to create matching rule from JSON '{:?}': {}", m, err); + err })?; rules.push(rule); } @@ -212,8 +213,9 @@ pub fn matchers_from_integration_json(m: &Map) -> anyhow::Result< let val = json_to_string(value); MatchingRule::create(val.as_str(), &Value::Object(m.clone())) .map(|r| vec![r]) - .inspect_err(|err| { + .map_err(|err| { error!("Failed to create matching rule from JSON '{:?}': {}", m, err); + err }) } }, diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs index 78159a096..818ecfe83 100644 --- a/rust/pact_ffi/tests/tests.rs +++ b/rust/pact_ffi/tests/tests.rs @@ -9,7 +9,6 @@ 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; @@ -27,8 +26,7 @@ use pact_ffi::mock_server::{ pactffi_create_mock_server, pactffi_create_mock_server_for_pact, pactffi_mock_server_mismatches, - pactffi_write_pact_file, - pactffi_mock_server_logs + pactffi_write_pact_file }; #[allow(deprecated)] use pact_ffi::mock_server::handles::{ @@ -76,7 +74,6 @@ 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() { From 21406631636a253786b294c55bc99be353da71dc Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Wed, 20 Mar 2024 11:59:46 +1100 Subject: [PATCH 3/4] feat(ffi): Update intermediate JSON to support matching rule expressions #399 --- rust/pact_ffi/src/mock_server/bodies.rs | 130 ++++++++++++++--------- rust/pact_ffi/src/mock_server/handles.rs | 54 +++++----- rust/pact_ffi/src/mock_server/mod.rs | 19 +++- rust/pact_ffi/src/mock_server/xml.rs | 31 ++++-- rust/pact_ffi/tests/tests.rs | 67 ++++++++++++ 5 files changed, 212 insertions(+), 89 deletions(-) diff --git a/rust/pact_ffi/src/mock_server/bodies.rs b/rust/pact_ffi/src/mock_server/bodies.rs index bb93a0d7e..98fd86db2 100644 --- a/rust/pact_ffi/src/mock_server/bodies.rs +++ b/rust/pact_ffi/src/mock_server/bodies.rs @@ -4,17 +4,22 @@ use std::path::Path; use anyhow::{anyhow, bail}; use bytes::{Bytes, BytesMut}; +use either::Either; use lazy_static::lazy_static; +use regex::Regex; +use serde_json::{Map, Value}; +use tracing::{debug, error, trace, warn}; + use pact_models::bodies::OptionalBody; use pact_models::content_types::ContentTypeHint; use pact_models::generators::{Generator, GeneratorCategory, Generators}; use pact_models::json_utils::json_to_string; -use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleLogic}; +use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory, RuleLogic}; +use pact_models::matchingrules::expressions::{is_matcher_def, parse_matcher_def}; use pact_models::path_exp::DocPath; use pact_models::v4::http_parts::{HttpRequest, HttpResponse}; -use regex::Regex; -use serde_json::{Map, Value}; -use tracing::{debug, error, trace, warn}; + +use crate::mock_server::generator_category; const CONTENT_TYPE_HEADER: &str = "Content-Type"; @@ -41,8 +46,8 @@ pub fn process_array( item_path.push_index(index); } match val { - Value::Object(ref map) => process_object(map, matching_rules, generators, item_path, skip_matchers), - Value::Array(ref array) => process_array(array, matching_rules, generators, item_path, false, skip_matchers), + Value::Object(map) => process_object(map, matching_rules, generators, item_path, skip_matchers), + Value::Array(array) => process_array(array.as_slice(), matching_rules, generators, item_path, false, skip_matchers), _ => val.clone() } }).collect()) @@ -60,7 +65,7 @@ pub fn process_object( debug!("Path = {path}"); let result = if let Some(matcher_type) = obj.get("pact:matcher:type") { debug!("detected pact:matcher:type, will configure a matcher"); - process_matcher(obj, matching_rules, generators, &path, type_matcher, matcher_type) + process_matcher(obj, matching_rules, generators, &path, type_matcher, &matcher_type.clone()) } else { debug!("Configuring a normal object"); Value::Object(obj.iter() @@ -122,17 +127,23 @@ fn process_matcher( _ => Err(anyhow!("ArrayContains 'variants' attribute is missing or not an array")) } } else { - matchers_from_integration_json(obj).map(|rules| { + matchers_from_integration_json(obj).map(|(rules, generator)| { let has_values_matcher = rules.iter().any(MatchingRule::is_values_matcher); let json_value = match obj.get("value") { Some(inner) => match inner { - 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), + Value::Object(ref map) => process_object(map, matching_rules, generators, path.clone(), has_values_matcher), + Value::Array(ref array) => process_array(array, matching_rules, generators, path.clone(), true, skip_matchers), _ => inner.clone() }, None => Value::Null }; + + if let Some(generator) = generator { + let category = generator_category(matching_rules); + generators.add_generator_with_subcategory(category, path.clone(), generator); + } + (rules, json_value) }) }; @@ -140,17 +151,7 @@ fn process_matcher( if let Some(gen) = obj.get("pact:generator:type") { debug!("detected pact:generator:type, will configure a generators"); if let Some(generator) = Generator::from_map(&json_to_string(gen), obj) { - let category = match matching_rules.name { - Category::BODY => &GeneratorCategory::BODY, - Category::HEADER => &GeneratorCategory::HEADER, - Category::PATH => &GeneratorCategory::PATH, - Category::QUERY => &GeneratorCategory::QUERY, - Category::METADATA => &GeneratorCategory::METADATA, - _ => { - warn!("invalid generator category {} provided, defaulting to body", matching_rules.name); - &GeneratorCategory::BODY - } - }; + let category = generator_category(matching_rules); generators.add_generator_with_subcategory(category, path.clone(), generator); } } @@ -185,41 +186,62 @@ pub fn matcher_from_integration_json(m: &Map) -> Option) -> anyhow::Result> { +pub fn matchers_from_integration_json(m: &Map) -> anyhow::Result<(Vec, Option)> { 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) - .map_err(|err| { - error!("Failed to create matching rule from JSON '{:?}': {}", m, err); - err - })?; - rules.push(rule); + Some(value) => { + let json_str = value.to_string(); + match value { + Value::Array(arr) => { + let mut rules = vec![]; + for v in arr.clone() { + match v.get("pact:matcher:type") { + Some(t) => { + let val = json_to_string(t); + let rule = MatchingRule::create(val.as_str(), &v) + .map_err(|err| { + error!("Failed to create matching rule from JSON '{:?}': {}", m, err); + err + })?; + rules.push(rule); + } + None => { + error!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", json_str); + bail!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", json_str); + } } - 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, None)) + } + _ => { + let val = json_to_string(value); + if val != "eachKey" && val != "eachValue" && val != "notEmpty" && is_matcher_def(val.as_str()) { + let mut rules = vec![]; + let def = parse_matcher_def(val.as_str())?; + for rule in def.rules { + match rule { + Either::Left(rule) => rules.push(rule), + Either::Right(reference) => if m.contains_key(reference.name.as_str()) { + rules.push(MatchingRule::Type); + // TODO: We need to somehow drop the reference otherwise the matching will try compare it + } else { + error!("Failed to create matching rules from JSON '{:?}': reference '{}' was not found", json_str, reference.name); + bail!("Failed to create matching rules from JSON '{:?}': reference '{}' was not found", json_str, reference.name); + } + } } + Ok((rules, def.generator)) + } else { + MatchingRule::create(val.as_str(), &Value::Object(m.clone())) + .map(|r| (vec![r], None)) + .map_err(|err| { + error!("Failed to create matching rule from JSON '{:?}': {}", json_str, err); + err + }) } } - Ok(rules) - } - _ => { - let val = json_to_string(value); - MatchingRule::create(val.as_str(), &Value::Object(m.clone())) - .map(|r| vec![r]) - .map_err(|err| { - error!("Failed to create matching rule from JSON '{:?}': {}", m, err); - err - }) } }, - _ => Ok(vec![]) + _ => Ok((vec![], None)) } } @@ -403,18 +425,20 @@ fn format_multipart_error(e: std::io::Error) -> String { mod test { use expectest::prelude::*; use maplit::hashmap; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::json; + use pact_models::{generators, HttpStatus, matchingrules_list}; use pact_models::content_types::ContentType; use pact_models::generators::{Generator, Generators}; use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory}; use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType}; 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}; + use super::*; #[test] @@ -875,7 +899,7 @@ mod test { { "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)); + expect!(matchers_from_integration_json(&json.as_object().unwrap())).to(be_ok().value((value, None))); } #[rstest] diff --git a/rust/pact_ffi/src/mock_server/handles.rs b/rust/pact_ffi/src/mock_server/handles.rs index 5d202334b..e1a759cc7 100644 --- a/rust/pact_ffi/src/mock_server/handles.rs +++ b/rust/pact_ffi/src/mock_server/handles.rs @@ -120,7 +120,7 @@ use maplit::*; use pact_models::{Consumer, PactSpecification, Provider}; use pact_models::bodies::OptionalBody; use pact_models::content_types::{ContentType, detect_content_type_from_string, JSON, TEXT, XML}; -use pact_models::generators::{Generator, GeneratorCategory, Generators}; +use pact_models::generators::{Generator, Generators}; use pact_models::headers::parse_header; use pact_models::http_parts::HttpPart; use pact_models::interaction::Interaction; @@ -145,7 +145,7 @@ use futures::executor::block_on; use crate::{convert_cstr, ffi_fn, safe_str}; use crate::error::set_error_msg; -use crate::mock_server::{StringResult, xml}; +use crate::mock_server::{generator_category, StringResult, xml}; #[allow(deprecated)] use crate::mock_server::bodies::{ empty_multipart_body, @@ -939,8 +939,8 @@ fn from_integration_json( match serde_json::from_str(value) { Ok(json) => match json { - Value::Object(ref map) => { - let json: Value = process_object(map, category, generators, path, false); + Value::Object(map) => { + let json: Value = process_object(&map, category, generators, path, false); // These are simple JSON primitives (strings), so we must unescape them json_to_string(&json) }, @@ -970,17 +970,17 @@ fn from_integration_json_v2( let query_or_header = [Category::QUERY, Category::HEADER].contains(&matching_rules.name); match serde_json::from_str(value) { - Ok(json) => match json { - Value::Object(ref map) => { + Ok(mut json) => match &mut json { + Value::Object(map) => { let result = if map.contains_key("pact:matcher:type") { 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") { + let (path, result_value) = match map.get_mut("value") { Some(val) => match val { Value::Array(array) => { - let array = process_array(&array, matching_rules, generators, path.clone(), true, false); + let array = process_array(array.as_mut_slice(), matching_rules, generators, path.clone(), true, false); (path.clone(), array) }, _ => (path.clone(), val.clone()) @@ -988,7 +988,7 @@ fn from_integration_json_v2( None => (path.clone(), Value::Null) }; - if let Ok(rules) = &rules { + if let Ok((rules, generator)) = &rules { let path = if path_or_status { path.parent().unwrap_or(DocPath::root()) } else { @@ -1012,22 +1012,22 @@ fn from_integration_json_v2( matching_rules.add_rule(path.clone(), rule.clone(), RuleLogic::And); } } + + if let Some(generator) = generator { + let category = generator_category(matching_rules); + let path = if path_or_status { + path.parent().unwrap_or(DocPath::root()) + } else { + path.clone() + }; + generators.add_generator_with_subcategory(category, path.clone(), generator.clone()); + } } 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) { - let category = match matching_rules.name { - Category::BODY => &GeneratorCategory::BODY, - Category::HEADER => &GeneratorCategory::HEADER, - Category::PATH => &GeneratorCategory::PATH, - Category::QUERY => &GeneratorCategory::QUERY, - Category::STATUS => &GeneratorCategory::STATUS, - _ => { - warn!("invalid generator category {} provided, defaulting to body", matching_rules.name); - &GeneratorCategory::BODY - } - }; + let category = generator_category(matching_rules); let path = if path_or_status { path.parent().unwrap_or(DocPath::root()) } else { @@ -1062,8 +1062,8 @@ fn from_integration_json_v2( pub(crate) fn process_xml(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> Result, String> { trace!("process_xml"); match serde_json::from_str(&body) { - Ok(json) => match json { - Value::Object(ref map) => xml::generate_xml_body(map, matching_rules, generators), + Ok(mut json) => match &mut json { + Value::Object(map) => xml::generate_xml_body(map, matching_rules, generators), _ => Err(format!("JSON document is invalid (expected an Object), have {}", json)) }, Err(err) => Err(format!("Failed to parse XML builder document: {}", err)) @@ -2543,12 +2543,10 @@ pub extern fn pactffi_message_with_metadata_v2(message_handle: MessageHandle, ke let generators = &mut message.contents.generators; let value = match serde_json::from_str(value) { Ok(json) => match json { - Value::Object(ref map) => process_object(map, matching_rules, generators, DocPath::new(key).unwrap(), false), - Value::Array(ref array) => process_array(array, matching_rules, generators, DocPath::new(key).unwrap(), false, false), + Value::Object(map) => process_object(&map, matching_rules, generators, DocPath::new(key).unwrap(), false), + Value::Array(array) => process_array(array.as_slice(), matching_rules, generators, DocPath::new(key).unwrap(), false, false), Value::Null => Value::Null, - Value::String(string) => Value::String(string), - Value::Bool(bool) => Value::Bool(bool), - Value::Number(number) => Value::Number(number), + _ => json }, Err(err) => { warn!("Failed to parse metadata value '{}' as JSON - {}. Will treat it as string", value, err); @@ -3748,7 +3746,7 @@ mod tests { })); } - #[test] + #[test_log::test] fn status_with_matcher_and_generator() { let pact_handle = PactHandle::new("TestPC3", "TestPP"); let description = CString::new("status_with_matcher_and_generator").unwrap(); diff --git a/rust/pact_ffi/src/mock_server/mod.rs b/rust/pact_ffi/src/mock_server/mod.rs index 18a8ad70d..f9ea7ae4d 100644 --- a/rust/pact_ffi/src/mock_server/mod.rs +++ b/rust/pact_ffi/src/mock_server/mod.rs @@ -60,7 +60,7 @@ use pact_models::time_utils::{parse_pattern, to_chrono_pattern}; use rand::prelude::*; use serde_json::Value; use tokio_rustls::rustls::ServerConfig; -use tracing::error; +use tracing::{error, warn}; use uuid::Uuid; use pact_matching::logging::fetch_buffer_contents; @@ -68,6 +68,8 @@ use pact_matching::metrics::{MetricEvent, send_metrics}; use pact_mock_server::{MANAGER, mock_server_mismatches, MockServerError, tls::TlsConfigBuilder, WritePactFileErr}; use pact_mock_server::mock_server::MockServerConfig; use pact_mock_server::server_manager::ServerManager; +use pact_models::generators::GeneratorCategory; +use pact_models::matchingrules::{Category, MatchingRuleCategory}; use crate::{convert_cstr, ffi_fn, safe_str}; use crate::mock_server::handles::{PactHandle, path_from_dir}; @@ -700,3 +702,18 @@ pub unsafe extern fn pactffi_free_string(s: *mut c_char) { } drop(CString::from_raw(s)); } + +pub(crate) fn generator_category(matching_rules: &mut MatchingRuleCategory) -> &GeneratorCategory { + match matching_rules.name { + Category::BODY => &GeneratorCategory::BODY, + Category::HEADER => &GeneratorCategory::HEADER, + Category::PATH => &GeneratorCategory::PATH, + Category::QUERY => &GeneratorCategory::QUERY, + Category::METADATA => &GeneratorCategory::METADATA, + Category::STATUS => &GeneratorCategory::STATUS, + _ => { + warn!("invalid generator category {} provided, defaulting to body", matching_rules.name); + &GeneratorCategory::BODY + } + } +} diff --git a/rust/pact_ffi/src/mock_server/xml.rs b/rust/pact_ffi/src/mock_server/xml.rs index 8524d4bd2..4b2f23f86 100644 --- a/rust/pact_ffi/src/mock_server/xml.rs +++ b/rust/pact_ffi/src/mock_server/xml.rs @@ -20,7 +20,11 @@ use pact_models::path_exp::DocPath; 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> { +pub fn generate_xml_body( + attributes: &Map, + matching_rules: &mut MatchingRuleCategory, + generators: &mut Generators +) -> Result, String> { let package = Package::new(); let doc = package.as_document(); @@ -71,14 +75,19 @@ fn create_element_from_json<'a>( updated_path.push(&name); let doc_path = DocPath::new(updated_path.join(".").to_string()).unwrap_or(DocPath::root()); - if let Ok(rules) = matchers_from_integration_json(object) { + if let Ok((rules, generator)) = matchers_from_integration_json(object) { for rule in rules { matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); } + + if let Some(generator) = generator { + generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path.clone(), generator); + } } + if let Some(gen) = object.get("pact:generator:type") { match Generator::from_map(&json_to_string(gen), object) { - Some(generator) => generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path, generator), + Some(generator) => generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path.clone(), generator), _ => () }; } @@ -126,10 +135,14 @@ fn create_element_from_json<'a>( let doc_path = DocPath::new(&text_path.join(".")).unwrap_or(DocPath::root()); if let Value::Object(matcher) = matcher { - if let Ok(rules) = matchers_from_integration_json(matcher) { + if let Ok((rules, generator)) = matchers_from_integration_json(matcher) { for rule in rules { matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); } + + if let Some(generator) = generator { + generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path.clone(), generator); + } } } if let Some(gen) = object.get("pact:generator:type") { @@ -216,12 +229,16 @@ fn add_attributes( let value = match v { 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 Ok(rules) = matchers_from_integration_json(matcher_definition) { + if let Ok((rules, generator)) = matchers_from_integration_json(matcher_definition) { for rule in rules { matching_rules.add_rule(doc_path.clone(), rule, RuleLogic::And); } + + if let Some(generator) = generator { + generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path.clone(), generator); + } } + if let Some(gen) = matcher_definition.get("pact:generator:type") { match Generator::from_map(&json_to_string(gen), matcher_definition) { Some(generator) => generators.add_generator_with_subcategory(&GeneratorCategory::BODY, doc_path, generator), @@ -254,4 +271,4 @@ fn duplicate_element<'a>(doc: Document<'a>, el: &Element<'a>) -> Element<'a> { } } element -} \ No newline at end of file +} diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs index 818ecfe83..018168496 100644 --- a/rust/pact_ffi/tests/tests.rs +++ b/rust/pact_ffi/tests/tests.rs @@ -1339,3 +1339,70 @@ fn combined_each_key_and_each_value_matcher() { } }; } + +// Issue #399 +#[test_log::test] +fn matching_definition_expressions_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("matching_definition_expressions").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": "eachKey(matching(regex, '\\w{3}-\\d+', 'AUK-155332')), eachValue(matching(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); + } + }; +} From d3c18d21b9fe9569726f618a35b2eb7670ab0d99 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Wed, 20 Mar 2024 14:47:54 +1100 Subject: [PATCH 4/4] chore(ffi): Remove unecessary mutable references #399 --- rust/pact_ffi/src/mock_server/handles.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/pact_ffi/src/mock_server/handles.rs b/rust/pact_ffi/src/mock_server/handles.rs index e1a759cc7..392fbd756 100644 --- a/rust/pact_ffi/src/mock_server/handles.rs +++ b/rust/pact_ffi/src/mock_server/handles.rs @@ -970,17 +970,17 @@ fn from_integration_json_v2( let query_or_header = [Category::QUERY, Category::HEADER].contains(&matching_rules.name); match serde_json::from_str(value) { - Ok(mut json) => match &mut json { + Ok(json) => match &json { Value::Object(map) => { let result = if map.contains_key("pact:matcher:type") { 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_mut("value") { + let (path, result_value) = match map.get("value") { Some(val) => match val { Value::Array(array) => { - let array = process_array(array.as_mut_slice(), matching_rules, generators, path.clone(), true, false); + let array = process_array(array.as_slice(), matching_rules, generators, path.clone(), true, false); (path.clone(), array) }, _ => (path.clone(), val.clone()) @@ -1062,7 +1062,7 @@ fn from_integration_json_v2( pub(crate) fn process_xml(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> Result, String> { trace!("process_xml"); match serde_json::from_str(&body) { - Ok(mut json) => match &mut json { + Ok(json) => match &json { Value::Object(map) => xml::generate_xml_body(map, matching_rules, generators), _ => Err(format!("JSON document is invalid (expected an Object), have {}", json)) },