pact_consumer/patterns/
special_rules.rs

1//! Special matching rules, including `Like`, `Term`, etc.
2
3use 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/// Match values based on their data types.
28#[derive(Debug)]
29pub struct Like<Nested: Pattern> {
30    example: Nested,
31}
32
33impl<Nested: Pattern> Like<Nested> {
34    /// Match all values which have the same type as `example`.
35    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    // Make sure we can convert `Like` into different pattern types.
76    let _: JsonPattern = Like::new(json_pattern!("hello")).into();
77    // We don't particularly care about having a nice syntax for
78    // `StringPattern`, because it's rarely useful in practice.
79    let _: StringPattern = Like::new("hello".to_owned()).into();
80}
81
82/// Generates the specified value, matches any value of the same data type. This
83/// is intended for use inside `json_pattern!`, and it interprets its arguments
84/// as a `json_pattern!`.
85///
86/// ```
87/// use pact_consumer::*;
88///
89/// # fn main() {
90/// json_pattern!({
91///   "id": like!(10),
92///   "metadata": like!({}),
93/// });
94/// # }
95/// ```
96///
97/// If you're building `StringPattern` values, you'll need to call
98/// `Like::new` manually instead.
99#[macro_export]
100macro_rules! like {
101    ($($json_pattern:tt)+) => {
102        $crate::patterns::Like::new(json_pattern!($($json_pattern)+))
103    }
104}
105
106/// Match an array with the specified "shape".
107#[derive(Debug)]
108pub struct EachLike {
109    example_element: JsonPattern,
110    min_len: usize,
111}
112
113impl EachLike {
114    /// Match arrays containing elements like `example_element`.
115    pub fn new(example_element: JsonPattern) -> EachLike {
116        EachLike {
117            example_element,
118            min_len: 1,
119        }
120    }
121
122    /// Use this after `new` to set a minimum length for the matching array.
123    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        // Ruby omits the `type` here, but the Rust `pact_matching` library
183        // claims to want `"type"` when `"min"` is used.
184        s!("$.body") => json!({"match": "type", "min": 2}),
185        // TODO: Ruby always generates this; I'm not sure what it's intended to
186        // do. It looks like it makes child objects in the array match their
187        // fields by type automatically?
188        s!("$.body[*].*") => json!({"match": "type"}),
189        // This is inserted by our nested `Like` rule.
190        s!("$.body[*]") => json!({"match": "type"}),
191    );
192    assert_eq!(rules.to_v2_json(), expected_rules);
193}
194
195// A hidden macro which does the hard work of expanding `each_like!`. We don't
196// include this in the docs because it reveals a bunch of implementation
197// details.
198//
199// This is a classic Rust "tt muncher" macro that uses special rules starting
200// with `@` to build a recursive token examiner.
201#[macro_export]
202#[doc(hidden)]
203macro_rules! each_like_helper {
204    // Parsing base case #1: We made it all the way to the end of our tokens
205    // without seeing a top-level comma.
206    (@parse [$($found:tt)*] ) => {
207        each_like_helper!(@expand [$($found)*] [])
208    };
209
210    // Parsing base case #2: We saw a top-level comma, so we're done parsing
211    // the JSON pattern.
212    (@parse [$($found:tt)*] , $($rest:tt)* ) => {
213        each_like_helper!(@expand [$($found)*] [$($rest)*])
214    };
215
216    // Parsing recursive case (must come last): We have some other token, so
217    // add it to what we've found and continue.
218    (@parse [$($found:tt)*] $next:tt $($rest:tt)* ) => {
219        each_like_helper!(@parse [$($found)* $next] $($rest)*)
220    };
221
222    // We're done parsing, and we didn't find `min`.
223    (@expand [$($pattern:tt)*] []) => {
224        $crate::patterns::EachLike::new(json_pattern!($($pattern)*))
225    };
226
227    // We're done parsing, and we did find `min`.
228    (@expand [$($pattern:tt)*] [min = $min_len:expr]) => {
229        $crate::patterns::EachLike::new(json_pattern!($($pattern)*))
230            .with_min_len($min_len)
231    };
232
233    // Entry point. Must come last, because it matches anything.
234    ($($tokens:tt)+) => (each_like_helper!(@parse [] $($tokens)+));
235}
236
237/// Generates the specified value, matches any value of the same data type. This
238/// is intended for use inside `json_pattern!`, and it interprets its arguments
239/// as a `json_pattern!`.
240///
241/// ```
242/// use pact_consumer::*;
243///
244/// # fn main() {
245/// json_pattern!({
246///   // Expect an array of strings.
247///   "tags": each_like!("tag"),
248///
249///   // Expect an array of objects, each of which has a name key containing
250///   // a string (but match the actual names by type). Require a minimum of
251///   // two elements.
252///   "people": each_like!({
253///     "name": "J. Smith",
254///   }, min=2),
255/// });
256/// # }
257/// ```
258#[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    // This is something users might plausibly want to do.
274    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    // And now with `min`, which requires quite a bit of complicated macro
279    // code to parse.
280    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/// Match and generate strings that match a regular expression.
286#[derive(Debug)]
287pub struct Term<Nested: Pattern> {
288    /// The example string we generate when asked.
289    example: String,
290    /// The regex we use to match.
291    regex: Regex,
292    /// Since we always store `example` as a string, we need to mention our
293    /// `Nested` type somewhere. We can do that using the zero-length
294    /// `PhantomData` type.
295    phantom: PhantomData<Nested>,
296}
297
298impl<Nested: Pattern> Term<Nested> {
299    /// Construct a new `Term`, given a regex and the example string to
300    /// generate.
301    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    // Make sure we can convert `Term` into different pattern types.
353    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/// Internal helper function called by `term!` to build a regex. Panics if the
358/// regex is invalid. (We use this partly because it's hard to refer to the
359/// `regex` crate from inside a public macro unless our caller imports it.)
360#[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/// A pattern which matches the regular expression `$regex` (specified as a
370/// string) literal, and which generates `$example`. This is an alias for `matching_regex!`
371///
372/// ```
373/// use pact_consumer::*;
374///
375/// # fn main() {
376/// json_pattern!({
377///   // Match a string consisting of numbers and lower case letters, and
378///   // generate `"10a"`.
379///   "id_string": term!("^[0-9a-z]+$", "10a")
380/// });
381/// # }
382/// ```
383#[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/// A pattern which matches the regular expression `$regex` (specified as a
393/// string) literal, and which generates `$example`.
394///
395/// ```
396/// use pact_consumer::*;
397///
398/// # fn main() {
399/// json_pattern!({
400///   // Match a string consisting of numbers and lower case letters, and
401///   // generate `"10a"`
402///   "id_string": matching_regex!("^[0-9a-z]+$", "10a")
403/// });
404/// # }
405/// ```
406#[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/// Match keys and values in an Object based on associated matching rules
416#[derive(Debug)]
417pub struct ObjectMatching {
418    example: JsonPattern,
419    rules: Vec<MatchingRule>
420}
421
422impl ObjectMatching {
423  /// Create a new ObjectMatching pattern with the provided pattern and list of rules
424  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    // Make sure we can convert `ObjectMatching` into different pattern types.
506    let _: JsonPattern = ObjectMatching::new(json_pattern!({}), vec![]).into();
507}
508
509/// A pattern which can take a JSON pattern and then apply a number of matching rules to the
510/// resulting JSON object.
511///
512/// ```
513/// use pact_consumer::*;
514/// use pact_consumer::prelude::{each_key, each_value};
515///
516/// # fn main() {
517/// object_matching!(
518///   json_pattern!({
519///       "key1": "a string",
520///       "key2": "1",
521///   }),
522///   [
523///       each_key(matching_regex!("[a-z]{3}[0-9]", "key1")),
524///       each_value(like!("value1"))
525///   ]
526/// );
527/// # }
528/// ```
529#[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/// Apply an associated rule to each key of an Object
614#[derive(Debug)]
615pub struct EachKey {
616  /// The pattern we use to match.
617  pattern: StringPattern
618}
619
620impl EachKey {
621  /// Construct a new `EachKey`, given a pattern and example key.
622  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
688/// A pattern which applies another pattern to each key of an object, and which generates an
689/// example key. A regex matcher is the only matcher that makes sense to use on keys.
690///
691/// ```
692/// use pact_consumer::*;
693///
694/// # fn main() {
695/// // Each key must match the given regex, and an example key is supplied.
696/// use pact_consumer::patterns::each_key;
697/// each_key(matching_regex!("[a-z]{3}[0-9]", "key1"));
698/// # }
699/// ```
700pub 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/// Apply an associated rule to each value of an Object
723#[derive(Debug)]
724pub struct EachValue {
725  /// The regex we use to match.
726  rule: JsonPattern
727}
728
729impl EachValue {
730  /// Construct a new `EachValue`, given a pattern and example JSON.
731  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
798/// A pattern which applies another pattern to each value of an object, and which generates an
799/// example value.
800///
801/// ```
802/// use pact_consumer::*;
803/// use pact_consumer::prelude::each_value;
804///
805/// # fn main() {
806/// // Each value must match the given regex, and an example value is supplied.
807/// each_value(matching_regex!("[a-z]{3}[0-9]", "value1"));
808/// # }
809/// ```
810pub 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}