Skip to content

Commit f95f7e3

Browse files
committed
chore(v2-matching-engine): Fixes ported over from Pact-JVM
1 parent a3e375e commit f95f7e3

8 files changed

Lines changed: 313 additions & 151 deletions

File tree

rust/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/pact_matching/src/engine/bodies.rs

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ impl JsonPlanBuilder {
9595
root_node: &mut ExecutionPlanNode
9696
) {
9797
trace!(%json, %path, ">>> process_body_node");
98+
99+
let rewritten_path = remove_marker(&path);
100+
98101
match &json {
99102
Value::Array(items) => {
100103
if context.matcher_is_defined(path) {
@@ -107,7 +110,11 @@ impl JsonPlanBuilder {
107110

108111
if let Some(template) = items.first() {
109112
let mut for_each_node = ExecutionPlanNode::action("for-each");
110-
let item_path = path.join("[*]");
113+
let marker = format!("{}*", path.last_field().unwrap_or_default());
114+
for_each_node.add(ExecutionPlanNode::value_node(marker.as_str()));
115+
let item_path = path.parent()
116+
.unwrap_or_else(|| path.clone())
117+
.join_field(marker);
111118
for_each_node.add(ExecutionPlanNode::resolve_current_value(path));
112119
let mut item_node = ExecutionPlanNode::container(&item_path);
113120
match template {
@@ -120,7 +127,11 @@ impl JsonPlanBuilder {
120127
ExecutionPlanNode::action("check:exists")
121128
.add(ExecutionPlanNode::resolve_current_value(&item_path))
122129
);
123-
if context.matcher_is_defined(&item_path) {
130+
131+
let matchers = context.select_best_matcher(&item_path)
132+
.and_rules(&context.select_best_matcher(&rewritten_path))
133+
.remove_duplicates();
134+
if !matchers.is_empty() {
124135
let matchers = context.select_best_matcher(&item_path);
125136
presence_check.add(ExecutionPlanNode::annotation(format!("[*] {}", matchers.generate_description(false))));
126137
presence_check.add(build_matching_rule_node(&ExecutionPlanNode::value_node(template),
@@ -166,7 +177,10 @@ impl JsonPlanBuilder {
166177
ExecutionPlanNode::action("check:exists")
167178
.add(ExecutionPlanNode::resolve_current_value(&item_path))
168179
);
169-
if context.matcher_is_defined(&item_path) {
180+
let matchers = context.select_best_matcher(&item_path)
181+
.and_rules(&context.select_best_matcher(&rewritten_path))
182+
.remove_duplicates();
183+
if !matchers.is_empty() {
170184
let matchers = context.select_best_matcher(&item_path);
171185
presence_check.add(ExecutionPlanNode::annotation(format!("[{}] {}", index, matchers.generate_description(false))));
172186
presence_check.add(build_matching_rule_node(&ExecutionPlanNode::value_node(item),
@@ -179,6 +193,11 @@ impl JsonPlanBuilder {
179193
.add(ExecutionPlanNode::value_node(NodeValue::NULL))
180194
);
181195
}
196+
presence_check.add(
197+
ExecutionPlanNode::action("error")
198+
.add(ExecutionPlanNode::value_node(format!("Expected a value for '{}' but it was missing",
199+
item_path.as_json_pointer().unwrap())))
200+
);
182201
item_node.add(presence_check);
183202
root_node.add(item_node);
184203
}
@@ -187,7 +206,9 @@ impl JsonPlanBuilder {
187206
}
188207
}
189208
Value::Object(entries) => {
190-
let rules = context.select_best_matcher(path);
209+
let rules = context.select_best_matcher(&path)
210+
.and_rules(&context.select_best_matcher(&rewritten_path))
211+
.remove_duplicates();
191212
if !rules.is_empty() && should_apply_to_map_entries(&rules) {
192213
root_node.add(ExecutionPlanNode::annotation(rules.generate_description(true)));
193214
root_node.add(build_matching_rule_node(&ExecutionPlanNode::value_node(json.clone()),
@@ -230,8 +251,10 @@ impl JsonPlanBuilder {
230251
}
231252
}
232253
_ => {
233-
if context.matcher_is_defined(path) {
234-
let matchers = context.select_best_matcher(path);
254+
let matchers = context.select_best_matcher(&path)
255+
.and_rules(&context.select_best_matcher(&rewritten_path))
256+
.remove_duplicates();
257+
if !matchers.is_empty() {
235258
root_node.add(ExecutionPlanNode::annotation(format!("{} {}", path.last_field().unwrap_or_default(), matchers.generate_description(false))));
236259
root_node.add(build_matching_rule_node(&ExecutionPlanNode::value_node(json),
237260
&ExecutionPlanNode::resolve_current_value(path), &matchers, false));
@@ -305,7 +328,7 @@ impl XMLPlanBuilder {
305328
node: &mut ExecutionPlanNode
306329
) {
307330
let name = name(element);
308-
let element_path = if path.ends_with(format!("{}[*]", name).as_str()) {
331+
let element_path = if path.ends_with(format!("['{}*']", name).as_str()) {
309332
path.clone()
310333
} else if let Some(index) = index {
311334
path.join_field(&name).join_index(index)
@@ -397,13 +420,14 @@ impl XMLPlanBuilder {
397420
.filter(|m| m.is_length_type_matcher());
398421
if !rules.is_empty() {
399422
parent_node.add(ExecutionPlanNode::annotation(format!("{} {}",
400-
path.last_field().unwrap_or_default(),
423+
p.last_field().unwrap_or_default(),
401424
rules.generate_description(true))));
402425
parent_node.add(build_matching_rule_node(&ExecutionPlanNode::value_node(elements[0]),
403-
&ExecutionPlanNode::resolve_current_value(path), &rules, true));
426+
&ExecutionPlanNode::resolve_current_value(&p), &rules, true));
404427
}
405428

406429
let mut for_each_node = ExecutionPlanNode::action("for-each");
430+
for_each_node.add(ExecutionPlanNode::value_node(format!("{}*", child_name)));
407431
for_each_node.add(ExecutionPlanNode::resolve_current_value(&p));
408432
let item_path = p.join("[*]");
409433

@@ -423,12 +447,11 @@ impl XMLPlanBuilder {
423447
) {
424448
let text_nodes = text_nodes(element);
425449
let p = path.join("#text");
450+
let no_markers = remove_marker(&p);
426451
let no_indices = drop_indices(&p);
427-
let matchers = context.select_best_matcher(&p)
452+
let matchers = context.select_best_matcher_from(&no_markers, &no_indices)
428453
.filter(|matcher| !matcher.is_type_matcher())
429-
.and_rules(&context.select_best_matcher(&no_indices)
430-
.filter(|matcher| !matcher.is_type_matcher())
431-
).remove_duplicates();
454+
.remove_duplicates();
432455
if !matchers.is_empty() {
433456
node.add(ExecutionPlanNode::annotation(format!("{} {}", p.last_field().unwrap_or_default(),
434457
matchers.generate_description(false))));
@@ -538,7 +561,33 @@ fn drop_indices(path: &DocPath) -> DocPath {
538561
PathToken::Index(_) | PathToken::StarIndex => false,
539562
_ => true
540563
})
541-
.cloned())
564+
.map(|token| {
565+
if let PathToken::Field(name) = token {
566+
if name.ends_with('*') {
567+
PathToken::Field(name.trim_end_matches('*').to_string())
568+
} else {
569+
token.clone()
570+
}
571+
} else {
572+
token.clone()
573+
}
574+
}))
575+
}
576+
577+
fn remove_marker(path: &DocPath) -> DocPath {
578+
DocPath::from_tokens(path.tokens()
579+
.iter()
580+
.flat_map(|token| {
581+
if let PathToken::Field(name) = token {
582+
if name.ends_with('*') {
583+
vec![PathToken::Field(name.trim_end_matches('*').to_string()), PathToken::Index(0)]
584+
} else {
585+
vec![token.clone()]
586+
}
587+
} else {
588+
vec![token.clone()]
589+
}
590+
}))
542591
}
543592

544593
#[cfg(feature = "xml")]
@@ -739,6 +788,9 @@ mod tests {
739788
json:100,
740789
~>$[0],
741790
NULL
791+
),
792+
%error (
793+
'Expected a value for \'/0\' but it was missing'
742794
)
743795
)
744796
),
@@ -751,6 +803,9 @@ mod tests {
751803
json:200,
752804
~>$[1],
753805
NULL
806+
),
807+
%error (
808+
'Expected a value for \'/1\' but it was missing'
754809
)
755810
)
756811
),
@@ -763,6 +818,9 @@ mod tests {
763818
json:300,
764819
~>$[2],
765820
NULL
821+
),
822+
%error (
823+
'Expected a value for \'/2\' but it was missing'
766824
)
767825
)
768826
)
@@ -938,22 +996,23 @@ mod tests {
938996
json:{"min":2}
939997
),
940998
%for-each (
999+
'item*',
9411000
~>$.item,
942-
:$.item[*] (
1001+
:$['item*'] (
9431002
%json:expect:entries (
9441003
'OBJECT',
9451004
['a'],
946-
~>$.item[*]
1005+
~>$['item*']
9471006
),
9481007
%expect:only-entries (
9491008
['a'],
950-
~>$.item[*]
1009+
~>$['item*']
9511010
),
952-
:$.item[*].a (
1011+
:$['item*'].a (
9531012
#{'a must match by type'},
9541013
%match:type (
9551014
json:100,
956-
~>$.item[*].a,
1015+
~>$['item*'].a,
9571016
json:{}
9581017
)
9591018
)

rust/pact_matching/src/engine/context.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ impl PlanMatchingContext {
100100
self.matching_rules.select_best_matcher(path_slice.as_slice())
101101
}
102102

103+
/// Select the best matcher taking into account two paths
104+
pub fn select_best_matcher_from(&self, path1: &DocPath, path2: &DocPath) -> RuleList {
105+
let path1_tokens = path1.to_vec();
106+
let path1_list = path1_tokens.iter()
107+
.map(|s| s.as_str())
108+
.collect_vec();
109+
let mut result1 = self.matching_rules.rules.iter()
110+
.map(|(k, v)| (k, v, k.path_weight(&path1_list)))
111+
.filter(|&(_, _, (w, _))| w > 0)
112+
.collect_vec();
113+
114+
let path2_tokens = path2.to_vec();
115+
let path2_list = path2_tokens
116+
.iter()
117+
.map(|s| s.as_str())
118+
.collect_vec();
119+
let result2 = self.matching_rules.rules.iter()
120+
.map(|(k, v)| (k, v, k.path_weight(&path2_list)))
121+
.filter(|&(_, _, (w, _))| w > 0)
122+
.collect_vec();
123+
124+
result1.extend_from_slice(&result2);
125+
result1.iter()
126+
.max_by_key(|&(_, _, (w, t))| w * t)
127+
.map(|(_, v, (_, t))| v.as_cascaded(*t != path1_list.len()))
128+
.unwrap_or_default()
129+
}
130+
103131
/// If there is a type matcher defined at the path in this context
104132
pub fn type_matcher_defined(&self, path: &DocPath) -> bool {
105133
let path = path.to_vec();

0 commit comments

Comments
 (0)