use std::iter::repeat;
use std::marker::PhantomData;
use itertools::{Either, Itertools};
use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory, RuleLogic};
use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType};
use pact_models::path_exp::DocPath;
use regex::Regex;
use serde_json::Value;
use super::json_pattern::JsonPattern;
use super::Pattern;
use super::string_pattern::StringPattern;
macro_rules! impl_from_for_pattern {
($from:ty, $pattern:ident) => {
impl From<$from> for $pattern {
fn from(pattern: $from) -> Self {
$pattern::pattern(pattern)
}
}
}
}
#[derive(Debug)]
pub struct Like<Nested: Pattern> {
example: Nested,
}
impl<Nested: Pattern> Like<Nested> {
pub fn new<E: Into<Nested>>(example: E) -> Self {
Like { example: example.into() }
}
}
impl<Nested: Pattern> Pattern for Like<Nested> {
type Matches = Nested::Matches;
fn to_example(&self) -> Self::Matches {
self.example.to_example()
}
fn to_example_bytes(&self) -> Vec<u8> {
self.example.to_example_bytes()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
rules_out.add_rule(path.clone(), MatchingRule::Type, RuleLogic::And);
self.example.extract_matching_rules(path, rules_out);
}
}
impl_from_for_pattern!(Like<JsonPattern>, JsonPattern);
impl_from_for_pattern!(Like<StringPattern>, StringPattern);
#[test]
fn like_is_pattern() {
use maplit::*;
use pact_matching::s;
use serde_json::*;
let matchable = Like::<JsonPattern>::new(json_pattern!("hello"));
assert_eq!(matchable.to_example(), json!("hello"));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
assert_eq!(rules.to_v2_json(), hashmap!(s!("$.body") => json!({"match": "type"})));
}
#[test]
fn like_into() {
let _: JsonPattern = Like::new(json_pattern!("hello")).into();
let _: StringPattern = Like::new("hello".to_owned()).into();
}
#[macro_export]
macro_rules! like {
($($json_pattern:tt)+) => {
$crate::patterns::Like::new(json_pattern!($($json_pattern)+))
}
}
#[derive(Debug)]
pub struct EachLike {
example_element: JsonPattern,
min_len: usize,
}
impl EachLike {
pub fn new(example_element: JsonPattern) -> EachLike {
EachLike {
example_element,
min_len: 1,
}
}
pub fn with_min_len(mut self, min_len: usize) -> EachLike {
self.min_len = min_len;
self
}
}
impl_from_for_pattern!(EachLike, JsonPattern);
impl Pattern for EachLike {
type Matches = serde_json::Value;
fn to_example(&self) -> serde_json::Value {
let element = self.example_element.to_example();
serde_json::Value::Array(repeat(element).take(self.min_len).collect())
}
fn to_example_bytes(&self) -> Vec<u8> {
let value = self.to_example();
let s = value.as_str().unwrap_or_default();
s.as_bytes().to_vec()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
rules_out.add_rule(
path.clone(),
MatchingRule::MinType(self.min_len),
RuleLogic::And
);
let mut fields_path = path.clone();
fields_path.push_star_index().push_star();
rules_out.add_rule(
fields_path,
MatchingRule::Type,
RuleLogic::And
);
let mut example_path = path.clone();
example_path.push_star_index();
self.example_element.extract_matching_rules(
example_path,
rules_out,
);
}
}
#[test]
fn each_like_is_pattern() {
use maplit::*;
use pact_matching::s;
use serde_json::*;
let elem = Like::new(json_pattern!("hello"));
let matchable = EachLike::new(json_pattern!(elem)).with_min_len(2);
assert_eq!(matchable.to_example(), json!(["hello", "hello"]));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
let expected_rules = hashmap!(
s!("$.body") => json!({"match": "type", "min": 2}),
s!("$.body[*].*") => json!({"match": "type"}),
s!("$.body[*]") => json!({"match": "type"}),
);
assert_eq!(rules.to_v2_json(), expected_rules);
}
#[macro_export]
#[doc(hidden)]
macro_rules! each_like_helper {
(@parse [$($found:tt)*] ) => {
each_like_helper!(@expand [$($found)*] [])
};
(@parse [$($found:tt)*] , $($rest:tt)* ) => {
each_like_helper!(@expand [$($found)*] [$($rest)*])
};
(@parse [$($found:tt)*] $next:tt $($rest:tt)* ) => {
each_like_helper!(@parse [$($found)* $next] $($rest)*)
};
(@expand [$($pattern:tt)*] []) => {
$crate::patterns::EachLike::new(json_pattern!($($pattern)*))
};
(@expand [$($pattern:tt)*] [min = $min_len:expr]) => {
$crate::patterns::EachLike::new(json_pattern!($($pattern)*))
.with_min_len($min_len)
};
($($tokens:tt)+) => (each_like_helper!(@parse [] $($tokens)+));
}
#[macro_export]
macro_rules! each_like {
($($token:tt)+) => { each_like_helper!($($token)+) };
}
#[test]
fn each_like_macro_parsing() {
use serde_json::*;
#[derive(serde::Serialize)]
struct Point {
x: i32,
y: i32
}
let simple = each_like!(json!(Point { x: 1, y: 2 }));
assert_eq!(simple.example_element.to_example(), json!({ "x": 1, "y": 2 }));
assert_eq!(simple.min_len, 1);
let with_min = each_like!(json!(Point { x: 1, y: 2 }), min = 2 + 1);
assert_eq!(with_min.example_element.to_example(), json!({ "x": 1, "y": 2 }));
assert_eq!(with_min.min_len, 3);
}
#[derive(Debug)]
pub struct Term<Nested: Pattern> {
example: String,
regex: Regex,
phantom: PhantomData<Nested>,
}
impl<Nested: Pattern> Term<Nested> {
pub fn new<S: Into<String>>(regex: Regex, example: S) -> Self {
Term {
example: example.into(),
regex,
phantom: PhantomData,
}
}
}
impl<Nested> Pattern for Term<Nested>
where
Nested: Pattern,
Nested::Matches: From<String>,
{
type Matches = Nested::Matches;
fn to_example(&self) -> Self::Matches {
From::from(self.example.clone())
}
fn to_example_bytes(&self) -> Vec<u8> {
self.example.clone().into_bytes()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
rules_out.add_rule(path, MatchingRule::Regex(self.regex.to_string()),
RuleLogic::And);
}
}
impl_from_for_pattern!(Term<JsonPattern>, JsonPattern);
impl_from_for_pattern!(Term<StringPattern>, StringPattern);
#[test]
fn term_is_pattern() {
use maplit::*;
use serde_json::*;
let matchable = Term::<JsonPattern>::new(Regex::new("[Hh]ello").unwrap(), "hello");
assert_eq!(matchable.to_example(), json!("hello"));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
let expected_rules = hashmap!(
"$.body".to_string() => json!({ "match": "regex", "regex": "[Hh]ello" })
);
assert_eq!(rules.to_v2_json(), expected_rules);
}
#[test]
fn term_into() {
let _: JsonPattern = Term::new(Regex::new("[Hh]ello").unwrap(), "hello").into();
let _: StringPattern = Term::new(Regex::new("[Hh]ello").unwrap(), "hello").into();
}
#[doc(hidden)]
pub fn build_regex<S: AsRef<str>>(regex_str: S) -> Regex {
let regex_str = regex_str.as_ref();
match Regex::new(regex_str) {
Ok(regex) => regex,
Err(msg) => panic!("could not parse regex {:?}: {}", regex_str, msg),
}
}
#[macro_export]
macro_rules! term {
($regex:expr, $example:expr) => {
{
$crate::patterns::Term::new($crate::patterns::build_regex($regex), $example)
}
}
}
#[macro_export]
macro_rules! matching_regex {
($regex:expr, $example:expr) => {
{
$crate::patterns::Term::new($crate::patterns::build_regex($regex), $example)
}
}
}
#[derive(Debug)]
pub struct ObjectMatching {
example: JsonPattern,
rules: Vec<MatchingRule>
}
impl ObjectMatching {
pub fn new(example: JsonPattern, rules: Vec<MatchingRule>) -> Self {
Self {
example,
rules
}
}
}
impl Pattern for ObjectMatching {
type Matches = Value;
fn to_example(&self) -> Self::Matches {
self.example.to_example()
}
fn to_example_bytes(&self) -> Vec<u8> {
self.example.to_example_bytes()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
for rule in &self.rules {
rules_out.add_rule(path.clone(), rule.clone(), RuleLogic::And);
}
let child_path = path.join("*");
let mut child_rules = MatchingRuleCategory::empty("body");
self.example.extract_matching_rules(DocPath::root(), &mut child_rules);
for (path, rules) in child_rules.rules {
let path_tokens = path.tokens().iter().dropping(2);
let mut rule_path = child_path.clone();
for segment in path_tokens {
rule_path.push(segment.clone());
}
for rule in &rules.rules {
rules_out.add_rule(rule_path.clone(), rule.clone(), rules.rule_logic);
}
}
}
}
impl_from_for_pattern!(ObjectMatching, JsonPattern);
#[test]
fn object_matching_is_pattern() {
use serde_json::*;
use expectest::prelude::*;
use pact_models::matchingrules_list;
let matchable = ObjectMatching::new(
json_pattern!({
"key1": "a string we don't care about",
"key2": "1",
}),
vec![
MatchingRule::EachKey(MatchingRuleDefinition::new(
"key1".to_string(), ValueType::String, MatchingRule::Regex("[a-z]{3,}[0-9]".to_string()),
None, "".to_string()
)),
MatchingRule::EachValue(MatchingRuleDefinition::new(
"some string".to_string(), ValueType::Unknown, MatchingRule::Type, None, "".to_string()
))
]
);
assert_eq!(matchable.to_example(), json!({
"key1": "a string we don't care about",
"key2": "1",
}));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
expect!(rules).to(be_equal_to(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
MatchingRule::Regex("[a-z]{3,}[0-9]".to_string()), None, "".to_string())),
MatchingRule::EachValue(MatchingRuleDefinition::new("some string".to_string(), ValueType::Unknown,
MatchingRule::Type, None, "".to_string()))
]
}));
}
#[test]
fn object_matching_into() {
let _: JsonPattern = ObjectMatching::new(json_pattern!({}), vec![]).into();
}
#[macro_export]
macro_rules! object_matching {
($example:expr, [ $( $rule:expr ),* ]) => {{
let mut _rules: Vec<pact_models::matchingrules::MatchingRule> = vec![];
$(
_rules.push($rule.into());
)*
$crate::patterns::ObjectMatching::new(json_pattern!($example), _rules)
}}
}
#[test]
fn object_matching_test() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
use serde_json::json;
use pretty_assertions::assert_eq;
let matchable = object_matching!(
json_pattern!({
"key1": "a string",
"key2": "1",
}),
[
each_key(matching_regex!("[a-z]{3}[0-9]", "key1")),
each_value(like!("value1"))
]
);
expect!(matchable.to_example()).to(be_equal_to(json!({
"key1": "a string",
"key2": "1"
})));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
assert_eq!(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string())),
MatchingRule::EachValue(MatchingRuleDefinition::new("\"value1\"".to_string(),
ValueType::String, MatchingRule::Type, None, "".to_string()))
]
}, rules);
}
#[test]
fn object_matching_supports_nested_matching_rules() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
use serde_json::json;
use pretty_assertions::assert_eq;
let matchable = object_matching!(
json_pattern!({
"key1": {
"id": matching_regex!("[0-9]+", "1000"),
"desc": like!("description")
}
}),
[
each_key(matching_regex!("[a-z]{3}[0-9]", "key1"))
]
);
expect!(matchable.to_example()).to(be_equal_to(json!({
"key1": {
"id": "1000",
"desc": "description"
}
})));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
assert_eq!(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string()))
],
"$.*.id" => [ MatchingRule::Regex("[0-9]+".to_string()) ],
"$.*.desc" => [ MatchingRule::Type ]
}, rules);
}
#[derive(Debug)]
pub struct EachKey {
pattern: StringPattern
}
impl EachKey {
pub fn new<Nested: Into<StringPattern>>(pattern: Nested) -> Self {
EachKey {
pattern: pattern.into()
}
}
}
impl Pattern for EachKey {
type Matches = String;
fn to_example(&self) -> Self::Matches {
self.pattern.to_example()
}
fn to_example_bytes(&self) -> Vec<u8> {
self.to_example().into_bytes()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
rules_out.add_rule(path, self.into(), RuleLogic::And);
}
}
impl Into<MatchingRule> for EachKey {
fn into(self) -> MatchingRule {
(&self).into()
}
}
impl Into<MatchingRule> for &EachKey {
fn into(self) -> MatchingRule {
let mut tmp = MatchingRuleCategory::empty("body");
self.pattern.extract_matching_rules(DocPath::root(), &mut tmp);
MatchingRule::EachKey(MatchingRuleDefinition {
value: self.to_example(),
value_type: ValueType::String,
rules: tmp.rules.values()
.flat_map(|list| list.rules.iter())
.map(|rule| Either::Left(rule.clone()))
.collect(),
generator: None,
expression: "".to_string()
})
}
}
#[test]
fn each_key_is_pattern() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
let matchable = EachKey::new(
matching_regex!("\\d+", "100")
);
expect!(matchable.to_example()).to(be_equal_to("100"));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
expect!(rules).to(be_equal_to(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachKey(MatchingRuleDefinition::new("100".to_string(), ValueType::String,
MatchingRule::Regex("\\d+".to_string()), None, "".to_string()))
]
}));
}
pub fn each_key<P>(pattern: P) -> EachKey where P: Into<StringPattern> {
EachKey::new(pattern.into())
}
#[test]
fn each_key_test() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
let matchable = each_key(matching_regex!("[a-z]{3}[0-9]", "key1"));
expect!(matchable.to_example()).to(be_equal_to("key1"));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
expect!(rules).to(be_equal_to(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachKey(MatchingRuleDefinition::new("key1".to_string(), ValueType::String,
MatchingRule::Regex("[a-z]{3}[0-9]".to_string()), None, "".to_string()))
]
}));
}
#[derive(Debug)]
pub struct EachValue {
rule: JsonPattern
}
impl EachValue {
pub fn new<P: Into<JsonPattern>>(pattern: P) -> Self {
EachValue {
rule: pattern.into()
}
}
}
impl Pattern for EachValue
{
type Matches = Value;
fn to_example(&self) -> Self::Matches {
self.rule.to_example()
}
fn to_example_bytes(&self) -> Vec<u8> {
self.to_example().to_string().into_bytes()
}
fn extract_matching_rules(&self, path: DocPath, rules_out: &mut MatchingRuleCategory) {
rules_out.add_rule(path, self.into(), RuleLogic::And);
}
}
impl Into<MatchingRule> for EachValue {
fn into(self) -> MatchingRule {
(&self).into()
}
}
impl Into<MatchingRule> for &EachValue {
fn into(self) -> MatchingRule {
let mut tmp = MatchingRuleCategory::empty("body");
self.rule.extract_matching_rules(DocPath::root(), &mut tmp);
MatchingRule::EachValue(MatchingRuleDefinition {
value: self.to_example().to_string(),
value_type: ValueType::String,
rules: tmp.rules.values()
.flat_map(|list| list.rules.iter())
.map(|rule| Either::Left(rule.clone()))
.collect(),
generator: None,
expression: "".to_string()
})
}
}
#[test]
fn each_value_is_pattern() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
let matchable = EachValue::new(
matching_regex!("\\d+", "100")
);
expect!(matchable.to_example()).to(be_equal_to("100"));
let mut rules = MatchingRuleCategory::empty("body");
matchable.extract_matching_rules(DocPath::root(), &mut rules);
expect!(rules).to(be_equal_to(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachValue(MatchingRuleDefinition::new("\"100\"".to_string(), ValueType::String,
MatchingRule::Regex("\\d+".to_string()), None, "".to_string()))
]
}));
}
pub fn each_value<P: Into<JsonPattern>>(pattern: P) -> EachValue {
EachValue::new(pattern.into())
}
#[test]
fn each_value_test() {
use expectest::prelude::*;
use pact_models::matchingrules_list;
let result = each_value(matching_regex!("[a-z]{5}[0-9]", "value1"));
expect!(result.to_example()).to(be_equal_to("value1"));
let mut rules = MatchingRuleCategory::empty("body");
result.extract_matching_rules(DocPath::root(), &mut rules);
expect!(rules).to(be_equal_to(matchingrules_list! {
"body"; "$" => [
MatchingRule::EachValue(MatchingRuleDefinition::new("\"value1\"".to_string(), ValueType::String,
MatchingRule::Regex("[a-z]{5}[0-9]".to_string()), None, "".to_string()))
]
}));
}