Skip to content

Commit

Permalink
feat(ffi): Update intermediate JSON to support matching rule expressi…
Browse files Browse the repository at this point in the history
…ons #399
  • Loading branch information
rholshausen committed Mar 20, 2024
1 parent 0907745 commit 2140663
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 89 deletions.
130 changes: 77 additions & 53 deletions rust/pact_ffi/src/mock_server/bodies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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())
Expand All @@ -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()
Expand Down Expand Up @@ -122,35 +127,31 @@ 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)
})
};

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);
}
}
Expand Down Expand Up @@ -185,41 +186,62 @@ pub fn matcher_from_integration_json(m: &Map<String, Value>) -> Option<MatchingR
}

/// Builds a list of `MatchingRule` from a `Value` struct used by language integrations
pub fn matchers_from_integration_json(m: &Map<String, Value>) -> anyhow::Result<Vec<MatchingRule>> {
pub fn matchers_from_integration_json(m: &Map<String, Value>) -> anyhow::Result<(Vec<MatchingRule>, Option<Generator>)> {
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))
}
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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<MatchingRule>) {
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]
Expand Down
54 changes: 26 additions & 28 deletions rust/pact_ffi/src/mock_server/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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)
},
Expand Down Expand Up @@ -970,25 +970,25 @@ 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())
},
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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -1062,8 +1062,8 @@ fn from_integration_json_v2(
pub(crate) fn process_xml(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> Result<Vec<u8>, 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))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 18 additions & 1 deletion rust/pact_ffi/src/mock_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ 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;
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};
Expand Down Expand Up @@ -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
}
}
}
Loading

0 comments on commit 2140663

Please sign in to comment.