1use std::iter::repeat;
4use std::marker::PhantomData;
5use itertools::{Either, Itertools};
6
7use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory, RuleLogic};
8use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType};
9use pact_models::path_exp::DocPath;
10use regex::Regex;
11use serde_json::Value;
12
13use super::json_pattern::JsonPattern;
14use super::Pattern;
15use super::string_pattern::StringPattern;
16
17macro_rules! impl_from_for_pattern {
18 ($from:ty, $pattern:ident) => {
19 impl From<$from> for $pattern {
20 fn from(pattern: $from) -> Self {
21 $pattern::pattern(pattern)
22 }
23 }
24 }
25}
26
27#[derive(Debug)]
29pub struct Like<Nested: Pattern> {
30 example: Nested,
31}
32
33impl<Nested: Pattern> Like<Nested> {
34 pub fn new<E: Into<Nested>>(example: E) -> Self {
36 Like { example: example.into() }
37 }
38}
39
40impl<Nested: Pattern> Pattern for Like<Nested> {
41 type Matches = Nested::Matches;
42
43 fn to_example(&self) -> Self::Matches {
44 self.example.to_example()
45 }
46
47 fn to_example_bytes(&self) -> Vec<u8> {
48 self.example.to_example_bytes()
49 }
50
51 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
52 rules_out.add_rule(path.clone(), MatchingRule::Type, RuleLogic::And);
53 self.example.extract_matching_rules(path, rules_out);
54 }
55}
56
57impl_from_for_pattern!(Like<JsonPattern>, JsonPattern);
58impl_from_for_pattern!(Like<StringPattern>, StringPattern);
59
60#[test]
61fn like_is_pattern() {
62 use maplit::*;
63 use pact_matching::s;
64 use serde_json::*;
65
66 let matchable = Like::<JsonPattern>::new(json_pattern!("hello"));
67 assert_eq!(matchable.to_example(), json!("hello"));
68 let mut rules = MatchingRuleCategory::empty("body");
69 matchable.extract_matching_rules(DocPath::root(), &mut rules);
70 assert_eq!(rules.to_v2_json(), hashmap!(s!("$.body") => json!({"match": "type"})));
71}
72
73#[test]
74fn like_into() {
75 let _: JsonPattern = Like::new(json_pattern!("hello")).into();
77 let _: StringPattern = Like::new("hello".to_owned()).into();
80}
81
82#[macro_export]
100macro_rules! like {
101 ($($json_pattern:tt)+) => {
102 $crate::patterns::Like::new(json_pattern!($($json_pattern)+))
103 }
104}
105
106#[derive(Debug)]
108pub struct EachLike {
109 example_element: JsonPattern,
110 min_len: usize,
111}
112
113impl EachLike {
114 pub fn new(example_element: JsonPattern) -> EachLike {
116 EachLike {
117 example_element,
118 min_len: 1,
119 }
120 }
121
122 pub fn with_min_len(mut self, min_len: usize) -> EachLike {
124 self.min_len = min_len;
125 self
126 }
127}
128
129impl_from_for_pattern!(EachLike, JsonPattern);
130
131impl Pattern for EachLike {
132 type Matches = serde_json::Value;
133
134 fn to_example(&self) -> serde_json::Value {
135 let element = self.example_element.to_example();
136 serde_json::Value::Array(repeat(element).take(self.min_len).collect())
137 }
138
139 fn to_example_bytes(&self) -> Vec<u8> {
140 let value = self.to_example();
141 let s = value.as_str().unwrap_or_default();
142 s.as_bytes().to_vec()
143 }
144
145 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
146 rules_out.add_rule(
147 path.clone(),
148 MatchingRule::MinType(self.min_len),
149 RuleLogic::And
150 );
151
152 let mut fields_path = path.clone();
153 fields_path.push_star_index().push_star();
154 rules_out.add_rule(
155 fields_path,
156 MatchingRule::Type,
157 RuleLogic::And
158 );
159
160 let mut example_path = path.clone();
161 example_path.push_star_index();
162 self.example_element.extract_matching_rules(
163 example_path,
164 rules_out,
165 );
166 }
167}
168
169#[test]
170fn each_like_is_pattern() {
171 use maplit::*;
172 use pact_matching::s;
173 use serde_json::*;
174
175 let elem = Like::new(json_pattern!("hello"));
176 let matchable = EachLike::new(json_pattern!(elem)).with_min_len(2);
177 assert_eq!(matchable.to_example(), json!(["hello", "hello"]));
178
179 let mut rules = MatchingRuleCategory::empty("body");
180 matchable.extract_matching_rules(DocPath::root(), &mut rules);
181 let expected_rules = hashmap!(
182 s!("$.body") => json!({"match": "type", "min": 2}),
185 s!("$.body[*].*") => json!({"match": "type"}),
189 s!("$.body[*]") => json!({"match": "type"}),
191 );
192 assert_eq!(rules.to_v2_json(), expected_rules);
193}
194
195#[macro_export]
202#[doc(hidden)]
203macro_rules! each_like_helper {
204 (@parse [$($found:tt)*] ) => {
207 each_like_helper!(@expand [$($found)*] [])
208 };
209
210 (@parse [$($found:tt)*] , $($rest:tt)* ) => {
213 each_like_helper!(@expand [$($found)*] [$($rest)*])
214 };
215
216 (@parse [$($found:tt)*] $next:tt $($rest:tt)* ) => {
219 each_like_helper!(@parse [$($found)* $next] $($rest)*)
220 };
221
222 (@expand [$($pattern:tt)*] []) => {
224 $crate::patterns::EachLike::new(json_pattern!($($pattern)*))
225 };
226
227 (@expand [$($pattern:tt)*] [min = $min_len:expr]) => {
229 $crate::patterns::EachLike::new(json_pattern!($($pattern)*))
230 .with_min_len($min_len)
231 };
232
233 ($($tokens:tt)+) => (each_like_helper!(@parse [] $($tokens)+));
235}
236
237#[macro_export]
259macro_rules! each_like {
260 ($($token:tt)+) => { each_like_helper!($($token)+) };
261}
262
263#[test]
264fn each_like_macro_parsing() {
265 use serde_json::*;
266
267 #[derive(serde::Serialize)]
268 struct Point {
269 x: i32,
270 y: i32
271 }
272
273 let simple = each_like!(json!(Point { x: 1, y: 2 }));
275 assert_eq!(simple.example_element.to_example(), json!({ "x": 1, "y": 2 }));
276 assert_eq!(simple.min_len, 1);
277
278 let with_min = each_like!(json!(Point { x: 1, y: 2 }), min = 2 + 1);
281 assert_eq!(with_min.example_element.to_example(), json!({ "x": 1, "y": 2 }));
282 assert_eq!(with_min.min_len, 3);
283}
284
285#[derive(Debug)]
287pub struct Term<Nested: Pattern> {
288 example: String,
290 regex: Regex,
292 phantom: PhantomData<Nested>,
296}
297
298impl<Nested: Pattern> Term<Nested> {
299 pub fn new<S: Into<String>>(regex: Regex, example: S) -> Self {
302 Term {
303 example: example.into(),
304 regex,
305 phantom: PhantomData,
306 }
307 }
308}
309
310impl<Nested> Pattern for Term<Nested>
311where
312 Nested: Pattern,
313 Nested::Matches: From<String>,
314{
315 type Matches = Nested::Matches;
316
317 fn to_example(&self) -> Self::Matches {
318 From::from(self.example.clone())
319 }
320
321 fn to_example_bytes(&self) -> Vec<u8> {
322 self.example.clone().into_bytes()
323 }
324
325 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
326 rules_out.add_rule(path, MatchingRule::Regex(self.regex.to_string()),
327 RuleLogic::And);
328 }
329}
330
331impl_from_for_pattern!(Term<JsonPattern>, JsonPattern);
332impl_from_for_pattern!(Term<StringPattern>, StringPattern);
333
334#[test]
335fn term_is_pattern() {
336 use maplit::*;
337 use serde_json::*;
338
339 let matchable = Term::<JsonPattern>::new(Regex::new("[Hh]ello").unwrap(), "hello");
340 assert_eq!(matchable.to_example(), json!("hello"));
341
342 let mut rules = MatchingRuleCategory::empty("body");
343 matchable.extract_matching_rules(DocPath::root(), &mut rules);
344 let expected_rules = hashmap!(
345 "$.body".to_string() => json!({ "match": "regex", "regex": "[Hh]ello" })
346 );
347 assert_eq!(rules.to_v2_json(), expected_rules);
348}
349
350#[test]
351fn term_into() {
352 let _: JsonPattern = Term::new(Regex::new("[Hh]ello").unwrap(), "hello").into();
354 let _: StringPattern = Term::new(Regex::new("[Hh]ello").unwrap(), "hello").into();
355}
356
357#[doc(hidden)]
361pub fn build_regex<S: AsRef<str>>(regex_str: S) -> Regex {
362 let regex_str = regex_str.as_ref();
363 match Regex::new(regex_str) {
364 Ok(regex) => regex,
365 Err(msg) => panic!("could not parse regex {:?}: {}", regex_str, msg),
366 }
367}
368
369#[macro_export]
384macro_rules! term {
385 ($regex:expr, $example:expr) => {
386 {
387 $crate::patterns::Term::new($crate::patterns::build_regex($regex), $example)
388 }
389 }
390}
391
392#[macro_export]
407macro_rules! matching_regex {
408 ($regex:expr, $example:expr) => {
409 {
410 $crate::patterns::Term::new($crate::patterns::build_regex($regex), $example)
411 }
412 }
413}
414
415#[derive(Debug)]
417pub struct ObjectMatching {
418 example: JsonPattern,
419 rules: Vec<MatchingRule>
420}
421
422impl ObjectMatching {
423 pub fn new(example: JsonPattern, rules: Vec<MatchingRule>) -> Self {
425 Self {
426 example,
427 rules
428 }
429 }
430}
431
432impl Pattern for ObjectMatching {
433 type Matches = Value;
434
435 fn to_example(&self) -> Self::Matches {
436 self.example.to_example()
437 }
438
439 fn to_example_bytes(&self) -> Vec<u8> {
440 self.example.to_example_bytes()
441 }
442
443 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
444 for rule in &self.rules {
445 rules_out.add_rule(path.clone(), rule.clone(), RuleLogic::And);
446 }
447
448 let child_path = path.join("*");
449 let mut child_rules = MatchingRuleCategory::empty("body");
450 self.example.extract_matching_rules(DocPath::root(), &mut child_rules);
451 for (path, rules) in child_rules.rules {
452 let path_tokens = path.tokens().iter().dropping(2);
453 let mut rule_path = child_path.clone();
454 for segment in path_tokens {
455 rule_path.push(segment.clone());
456 }
457 for rule in &rules.rules {
458 rules_out.add_rule(rule_path.clone(), rule.clone(), rules.rule_logic);
459 }
460 }
461 }
462}
463
464impl_from_for_pattern!(ObjectMatching, JsonPattern);
465
466#[test]
467fn object_matching_is_pattern() {
468 use serde_json::*;
469 use expectest::prelude::*;
470 use pact_models::matchingrules_list;
471
472 let matchable = ObjectMatching::new(
473 json_pattern!({
474 "key1": "a string we don't care about",
475 "key2": "1",
476 }),
477 vec![
478 MatchingRule::EachKey(MatchingRuleDefinition::new(
479 "key1".to_string(), ValueType::String, MatchingRule::Regex("[a-z]{3,}[0-9]".to_string()),
480 None, "".to_string()
481 )),
482 MatchingRule::EachValue(MatchingRuleDefinition::new(
483 "some string".to_string(), ValueType::Unknown, MatchingRule::Type, None, "".to_string()
484 ))
485 ]
486 );
487 assert_eq!(matchable.to_example(), json!({
488 "key1": "a string we don't care about",
489 "key2": "1",
490 }));
491 let mut rules = MatchingRuleCategory::empty("body");
492 matchable.extract_matching_rules(DocPath::root(), &mut rules);
493 expect!(rules).to(be_equal_to(matchingrules_list! {
494 "body"; "$" => [
495 MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
496 MatchingRule::Regex("[a-z]{3,}[0-9]".to_string()), None, "".to_string())),
497 MatchingRule::EachValue(MatchingRuleDefinition::new("some string".to_string(), ValueType::Unknown,
498 MatchingRule::Type, None, "".to_string()))
499 ]
500 }));
501}
502
503#[test]
504fn object_matching_into() {
505 let _: JsonPattern = ObjectMatching::new(json_pattern!({}), vec![]).into();
507}
508
509#[macro_export]
530macro_rules! object_matching {
531 ($example:expr, [ $( $rule:expr ),* ]) => {{
532 let mut _rules: Vec<pact_models::matchingrules::MatchingRule> = vec![];
533
534 $(
535 _rules.push($rule.into());
536 )*
537
538 $crate::patterns::ObjectMatching::new(json_pattern!($example), _rules)
539 }}
540}
541
542#[test]
543fn object_matching_test() {
544 use expectest::prelude::*;
545 use pact_models::matchingrules_list;
546 use serde_json::json;
547 use pretty_assertions::assert_eq;
548
549 let matchable = object_matching!(
550 json_pattern!({
551 "key1": "a string",
552 "key2": "1",
553 }),
554 [
555 each_key(matching_regex!("[a-z]{3}[0-9]", "key1")),
556 each_value(like!("value1"))
557 ]
558 );
559 expect!(matchable.to_example()).to(be_equal_to(json!({
560 "key1": "a string",
561 "key2": "1"
562 })));
563
564 let mut rules = MatchingRuleCategory::empty("body");
565 matchable.extract_matching_rules(DocPath::root(), &mut rules);
566 assert_eq!(matchingrules_list! {
567 "body"; "$" => [
568 MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
569 MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string())),
570 MatchingRule::EachValue(MatchingRuleDefinition::new("\"value1\"".to_string(),
571 ValueType::String, MatchingRule::Type, None, "".to_string()))
572 ]
573 }, rules);
574}
575
576#[test]
577fn object_matching_supports_nested_matching_rules() {
578 use expectest::prelude::*;
579 use pact_models::matchingrules_list;
580 use serde_json::json;
581 use pretty_assertions::assert_eq;
582
583 let matchable = object_matching!(
584 json_pattern!({
585 "key1": {
586 "id": matching_regex!("[0-9]+", "1000"),
587 "desc": like!("description")
588 }
589 }),
590 [
591 each_key(matching_regex!("[a-z]{3}[0-9]", "key1"))
592 ]
593 );
594 expect!(matchable.to_example()).to(be_equal_to(json!({
595 "key1": {
596 "id": "1000",
597 "desc": "description"
598 }
599 })));
600
601 let mut rules = MatchingRuleCategory::empty("body");
602 matchable.extract_matching_rules(DocPath::root(), &mut rules);
603 assert_eq!(matchingrules_list! {
604 "body"; "$" => [
605 MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
606 MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string()))
607 ],
608 "$.*.id" => [ MatchingRule::Regex("[0-9]+".to_string()) ],
609 "$.*.desc" => [ MatchingRule::Type ]
610 }, rules);
611}
612
613#[derive(Debug)]
615pub struct EachKey {
616 pattern: StringPattern
618}
619
620impl EachKey {
621 pub fn new<Nested: Into<StringPattern>>(pattern: Nested) -> Self {
623 EachKey {
624 pattern: pattern.into()
625 }
626 }
627}
628
629impl Pattern for EachKey {
630 type Matches = String;
631
632 fn to_example(&self) -> Self::Matches {
633 self.pattern.to_example()
634 }
635
636 fn to_example_bytes(&self) -> Vec<u8> {
637 self.to_example().into_bytes()
638 }
639
640 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
641 rules_out.add_rule(path, self.into(), RuleLogic::And);
642 }
643}
644
645impl Into<MatchingRule> for EachKey {
646 fn into(self) -> MatchingRule {
647 (&self).into()
648 }
649}
650
651impl Into<MatchingRule> for &EachKey {
652 fn into(self) -> MatchingRule {
653 let mut tmp = MatchingRuleCategory::empty("body");
654 self.pattern.extract_matching_rules(DocPath::root(), &mut tmp);
655 MatchingRule::EachKey(MatchingRuleDefinition {
656 value: self.to_example(),
657 value_type: ValueType::String,
658 rules: tmp.rules.values()
659 .flat_map(|list| list.rules.iter())
660 .map(|rule| Either::Left(rule.clone()))
661 .collect(),
662 generator: None,
663 expression: "".to_string()
664 })
665 }
666}
667
668#[test]
669fn each_key_is_pattern() {
670 use expectest::prelude::*;
671 use pact_models::matchingrules_list;
672
673 let matchable = EachKey::new(
674 matching_regex!("\\d+", "100")
675 );
676 expect!(matchable.to_example()).to(be_equal_to("100"));
677
678 let mut rules = MatchingRuleCategory::empty("body");
679 matchable.extract_matching_rules(DocPath::root(), &mut rules);
680 expect!(rules).to(be_equal_to(matchingrules_list! {
681 "body"; "$" => [
682 MatchingRule::EachKey(MatchingRuleDefinition::new("100".to_string(), ValueType::String,
683 MatchingRule::Regex("\\d+".to_string()), None, "".to_string()))
684 ]
685 }));
686}
687
688pub fn each_key<P>(pattern: P) -> EachKey where P: Into<StringPattern> {
701 EachKey::new(pattern.into())
702}
703
704#[test]
705fn each_key_test() {
706 use expectest::prelude::*;
707 use pact_models::matchingrules_list;
708
709 let matchable = each_key(matching_regex!("[a-z]{3}[0-9]", "key1"));
710 expect!(matchable.to_example()).to(be_equal_to("key1"));
711
712 let mut rules = MatchingRuleCategory::empty("body");
713 matchable.extract_matching_rules(DocPath::root(), &mut rules);
714 expect!(rules).to(be_equal_to(matchingrules_list! {
715 "body"; "$" => [
716 MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
717 MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string()))
718 ]
719 }));
720}
721
722#[derive(Debug)]
724pub struct EachValue {
725 rule: JsonPattern
727}
728
729impl EachValue {
730 pub fn new<P: Into<JsonPattern>>(pattern: P) -> Self {
732 EachValue {
733 rule: pattern.into()
734 }
735 }
736}
737
738impl Pattern for EachValue
739{
740 type Matches = Value;
741
742 fn to_example(&self) -> Self::Matches {
743 self.rule.to_example()
744 }
745
746 fn to_example_bytes(&self) -> Vec<u8> {
747 self.to_example().to_string().into_bytes()
748 }
749
750 fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
751 rules_out.add_rule(path, self.into(), RuleLogic::And);
752 }
753}
754
755impl Into<MatchingRule> for EachValue {
756 fn into(self) -> MatchingRule {
757 (&self).into()
758 }
759}
760
761impl Into<MatchingRule> for &EachValue {
762 fn into(self) -> MatchingRule {
763 let mut tmp = MatchingRuleCategory::empty("body");
764 self.rule.extract_matching_rules(DocPath::root(), &mut tmp);
765 MatchingRule::EachValue(MatchingRuleDefinition {
766 value: self.to_example().to_string(),
767 value_type: ValueType::String,
768 rules: tmp.rules.values()
769 .flat_map(|list| list.rules.iter())
770 .map(|rule| Either::Left(rule.clone()))
771 .collect(),
772 generator: None,
773 expression: "".to_string()
774 })
775 }
776}
777
778#[test]
779fn each_value_is_pattern() {
780 use expectest::prelude::*;
781 use pact_models::matchingrules_list;
782
783 let matchable = EachValue::new(
784 matching_regex!("\\d+", "100")
785 );
786 expect!(matchable.to_example()).to(be_equal_to("100"));
787
788 let mut rules = MatchingRuleCategory::empty("body");
789 matchable.extract_matching_rules(DocPath::root(), &mut rules);
790 expect!(rules).to(be_equal_to(matchingrules_list! {
791 "body"; "$" => [
792 MatchingRule::EachValue(MatchingRuleDefinition::new("\"100\"".to_string(), ValueType::String,
793 MatchingRule::Regex("\\d+".to_string()), None, "".to_string()))
794 ]
795 }));
796}
797
798pub fn each_value<P: Into<JsonPattern>>(pattern: P) -> EachValue {
811 EachValue::new(pattern.into())
812}
813
814#[test]
815fn each_value_test() {
816 use expectest::prelude::*;
817 use pact_models::matchingrules_list;
818
819 let result = each_value(matching_regex!("[a-z]{5}[0-9]", "value1"));
820 expect!(result.to_example()).to(be_equal_to("value1"));
821
822 let mut rules = MatchingRuleCategory::empty("body");
823 result.extract_matching_rules(DocPath::root(), &mut rules);
824 expect!(rules).to(be_equal_to(matchingrules_list! {
825 "body"; "$" => [
826 MatchingRule::EachValue(MatchingRuleDefinition::new("\"value1\"".to_string(), ValueType::String,
827 MatchingRule::Regex("[a-z]{5}[0-9]".to_string()), None, "".to_string()))
828 ]
829 }));
830}