Skip to content

Commit 7d4f581

Browse files
committed
chore(v2-matching-engine): Support root matching rules for bodies
1 parent 9bcbce3 commit 7d4f581

File tree

5 files changed

+132
-22
lines changed

5 files changed

+132
-22
lines changed

rust/pact_matching/src/engine/interpreter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! This module provides the interpreter that can execute a matching plan AST
22
3-
use std::collections::{HashMap, HashSet, VecDeque};
3+
use std::collections::{HashSet, VecDeque};
44
use std::iter::once;
55

66
use anyhow::anyhow;

rust/pact_matching/src/engine/mod.rs

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use ansi_term::Colour::{Green, Red};
99
use anyhow::anyhow;
1010
use base64::Engine;
1111
use base64::engine::general_purpose::STANDARD as BASE64;
12+
use bytes::Bytes;
1213
use itertools::Itertools;
1314
#[cfg(feature = "xml")] use kiss_xml::dom::Element;
1415
use maplit::hashmap;
@@ -250,6 +251,14 @@ impl NodeValue {
250251
}
251252
}
252253

254+
/// If this value is a byte array, returns it, otherwise returns None
255+
pub fn as_bytes(&self) -> Option<&Vec<u8>> {
256+
match self {
257+
NodeValue::BARRAY(b) => Some(b),
258+
_ => None
259+
}
260+
}
261+
253262
/// Calculates an AND of two values
254263
pub fn and(&self, other: &Self) -> Self {
255264
match self {
@@ -386,8 +395,14 @@ impl From<&Element> for NodeValue {
386395

387396
impl Matches<NodeValue> for NodeValue {
388397
fn matches_with(&self, actual: NodeValue, matcher: &MatchingRule, cascaded: bool) -> anyhow::Result<()> {
398+
self.matches_with(&actual, matcher, cascaded)
399+
}
400+
}
401+
402+
impl Matches<&NodeValue> for NodeValue {
403+
fn matches_with(&self, actual: &NodeValue, matcher: &MatchingRule, cascaded: bool) -> anyhow::Result<()> {
389404
match self {
390-
NodeValue::NULL => Value::Null.matches_with(actual.as_json().unwrap_or_default(), matcher, cascaded),
405+
NodeValue::NULL => actual.matches_with(actual, matcher, cascaded),
391406
NodeValue::STRING(s) => if let Some(actual_str) = actual.as_string() {
392407
s.matches_with(actual_str, matcher, cascaded)
393408
} else if let Some(list) = actual.as_slist() {
@@ -425,6 +440,7 @@ impl Matches<NodeValue> for NodeValue {
425440
} else {
426441
Err(anyhow!("Was expecting an XML value but got {}", actual))
427442
},
443+
NodeValue::BARRAY(b) => Bytes::new().matches_with(Bytes::copy_from_slice(b.as_slice()), matcher, cascaded),
428444
_ => Err(anyhow!("Matching rules can not be applied to {} values", self.str_form()))
429445
}
430446
}
@@ -1298,6 +1314,22 @@ impl ExecutionPlan {
12981314
}
12991315
}
13001316

1317+
impl From<ExecutionPlanNode> for ExecutionPlan {
1318+
fn from(value: ExecutionPlanNode) -> Self {
1319+
ExecutionPlan {
1320+
plan_root: value
1321+
}
1322+
}
1323+
}
1324+
1325+
impl From<&ExecutionPlanNode> for ExecutionPlan {
1326+
fn from(value: &ExecutionPlanNode) -> Self {
1327+
ExecutionPlan {
1328+
plan_root: value.clone()
1329+
}
1330+
}
1331+
}
1332+
13011333
impl Into<Vec<Mismatch>> for ExecutionPlan {
13021334
fn into(self) -> Vec<Mismatch> {
13031335
let mut result = vec![];
@@ -1862,35 +1894,49 @@ fn setup_body_plan<T: HttpPart>(
18621894
) -> anyhow::Result<ExecutionPlanNode> {
18631895
// TODO: Look at the matching rules and generators here
18641896
let mut plan_node = ExecutionPlanNode::container("body");
1897+
let body_path = DocPath::body();
18651898

18661899
match &expected.body() {
18671900
OptionalBody::Missing => {}
18681901
OptionalBody::Empty | OptionalBody::Null => {
18691902
plan_node.add(ExecutionPlanNode::action("expect:empty")
1870-
.add(ExecutionPlanNode::resolve_value(DocPath::new("$.body")?)));
1903+
.add(ExecutionPlanNode::resolve_value(body_path)));
18711904
}
18721905
OptionalBody::Present(content, _, _) => {
18731906
let content_type = expected.content_type().unwrap_or_else(|| TEXT.clone());
1874-
let mut content_type_check_node = ExecutionPlanNode::action("if");
1875-
content_type_check_node
1876-
.add(
1877-
ExecutionPlanNode::action("match:equality")
1878-
.add(ExecutionPlanNode::value_node(content_type.to_string()))
1879-
.add(ExecutionPlanNode::resolve_value(DocPath::new("$.content-type")?))
1880-
.add(ExecutionPlanNode::value_node(NodeValue::NULL))
1881-
.add(
1882-
ExecutionPlanNode::action("error")
1883-
.add(ExecutionPlanNode::value_node(NodeValue::STRING("Body type error - ".to_string())))
1884-
.add(ExecutionPlanNode::action("apply"))
1885-
)
1886-
);
1887-
if let Some(plan_builder) = get_body_plan_builder(&content_type) {
1888-
content_type_check_node.add(plan_builder.build_plan(content, context)?);
1907+
let root_matcher = expected.matching_rules()
1908+
.rules_for_category("body")
1909+
.map(|category| category.rules.get(&DocPath::root()).cloned())
1910+
.flatten();
1911+
if let Some(root_matcher) = root_matcher && root_matcher.can_match(&content_type) {
1912+
plan_node.add(build_matching_rule_node(
1913+
&ExecutionPlanNode::value_node(NodeValue::NULL),
1914+
&ExecutionPlanNode::resolve_value(body_path),
1915+
&root_matcher,
1916+
false
1917+
));
18891918
} else {
1890-
let plan_builder = PlainTextBuilder::new();
1891-
content_type_check_node.add(plan_builder.build_plan(content, context)?);
1919+
let mut content_type_check_node = ExecutionPlanNode::action("if");
1920+
content_type_check_node
1921+
.add(
1922+
ExecutionPlanNode::action("match:equality")
1923+
.add(ExecutionPlanNode::value_node(content_type.to_string()))
1924+
.add(ExecutionPlanNode::resolve_value(DocPath::new("$.content-type")?))
1925+
.add(ExecutionPlanNode::value_node(NodeValue::NULL))
1926+
.add(
1927+
ExecutionPlanNode::action("error")
1928+
.add(ExecutionPlanNode::value_node(NodeValue::STRING("Body type error - ".to_string())))
1929+
.add(ExecutionPlanNode::action("apply"))
1930+
)
1931+
);
1932+
if let Some(plan_builder) = get_body_plan_builder(&content_type) {
1933+
content_type_check_node.add(plan_builder.build_plan(content, context)?);
1934+
} else {
1935+
let plan_builder = PlainTextBuilder::new();
1936+
content_type_check_node.add(plan_builder.build_plan(content, context)?);
1937+
}
1938+
plan_node.add(content_type_check_node);
18921939
}
1893-
plan_node.add(content_type_check_node);
18941940
}
18951941
}
18961942

rust/pact_matching/src/engine/tests/mod.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use crate::engine::{
1919
execute_response_plan,
2020
NodeResult,
2121
NodeValue,
22-
PlanMatchingContext
22+
PlanMatchingContext,
23+
setup_body_plan
2324
};
2425
use crate::Mismatch::{self, BodyMismatch, MethodMismatch};
2526

@@ -765,3 +766,39 @@ fn match_status_with_matching_rule() -> anyhow::Result<()> {
765766

766767
Ok(())
767768
}
769+
770+
#[test_log::test]
771+
fn body_with_root_matcher() {
772+
let matching_rules = matchingrules! {
773+
"body" => { "$" => [ MatchingRule::Regex(".*[0-9]+.*".to_string()) ] }
774+
};
775+
let mut context = PlanMatchingContext::default();
776+
let response = HttpResponse {
777+
body: OptionalBody::from("This is a 100+ body"),
778+
matching_rules,
779+
.. Default::default()
780+
};
781+
let body_plan = setup_body_plan(&response, &context).unwrap();
782+
let mut buffer = String::new();
783+
body_plan.pretty_form(&mut buffer, 0);
784+
assert_eq!(r#":body (
785+
%match:regex (
786+
NULL,
787+
$.body,
788+
json:{"regex":".*[0-9]+.*"}
789+
)
790+
)"#, buffer);
791+
792+
let plan = body_plan.into();
793+
let executed_plan = execute_response_plan(&plan, &response, &mut context).unwrap();
794+
assert_eq!(r#"(
795+
:body (
796+
%match:regex (
797+
NULL => NULL,
798+
$.body => BYTES(19, VGhpcyBpcyBhIDEwMCsgYm9keQ==),
799+
json:{"regex":".*[0-9]+.*"} => json:{"regex":".*[0-9]+.*"}
800+
) => BOOL(true)
801+
) => BOOL(true)
802+
)
803+
"#, executed_plan.pretty_form());
804+
}

rust/pact_models/src/matchingrules/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use serde_json::{json, Map, Value};
1414
use tracing::{error, trace};
1515

1616
use crate::{HttpStatus, PactSpecification};
17+
use crate::content_types::ContentType;
1718
use crate::generators::{Generator, GeneratorCategory, Generators};
1819
use crate::json_utils::{json_to_num, json_to_string};
1920
use crate::matchingrules::expressions::{MatchingReference, MatchingRuleDefinition, ValueType};
@@ -578,6 +579,19 @@ impl MatchingRule {
578579
_ => self.clone()
579580
}
580581
}
582+
583+
/// If the matching rule can be applied data in the form of the content type
584+
pub fn can_match(&self, content_type: &ContentType) -> bool {
585+
match self {
586+
MatchingRule::NotEmpty => true,
587+
MatchingRule::ArrayContains(_) => true, // why? This is set in Pact-JVM
588+
MatchingRule::ContentType(_) => true,
589+
MatchingRule::Equality => true,
590+
MatchingRule::Include(_) => true,
591+
MatchingRule::Regex(_) => content_type.is_text(),
592+
_ => false
593+
}
594+
}
581595
}
582596

583597
impl Hash for MatchingRule {
@@ -793,6 +807,11 @@ impl RuleList {
793807
cascaded: self.cascaded
794808
}
795809
}
810+
811+
/// If all the matching rules in this list can match data on the form of the content type
812+
pub fn can_match(&self, content_type: &ContentType) -> bool {
813+
self.rules.iter().all(|rule| rule.can_match(content_type))
814+
}
796815
}
797816

798817
impl Hash for RuleList {

rust/pact_models/src/path_exp.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ impl DocPath {
129129
}
130130
}
131131

132+
/// Construct a new DocPath for `$.body`
133+
pub fn body() -> Self {
134+
Self {
135+
path_tokens: vec![PathToken::Root, PathToken::Field("body".to_string())],
136+
expr: "$.body".into(),
137+
}
138+
}
139+
132140
/// Construct a new DocPath from a list of tokens
133141
pub fn from_tokens<I>(tokens: I) -> Self
134142
where I: IntoIterator<Item = PathToken> {

0 commit comments

Comments
 (0)