use std::{collections::{HashMap, HashSet}, hash::{Hash, Hasher}, mem};
use std::fmt::{Debug, Display};
#[allow(unused_imports)]
use std::str::{self, FromStr, from_utf8};
use log::*;
use maplit::*;
use onig::Regex;
use serde::{Deserialize, Serialize};
use serde_json::{self, json, Value};
use crate::{MatchingContext, merge_result, Mismatch};
use crate::matchers::{match_values, Matches};
use crate::models::json_utils::{json_to_num, json_to_string};
use crate::path_exp::*;
use super::PactSpecification;
use nom::lib::std::cmp::Ordering;
use crate::binary_utils::match_content_type;
use crate::models::generators::{Generators, GeneratorCategory, Generator};
#[cfg(test)]
use expectest::prelude::*;
#[cfg(test)]
use std::collections::hash_map::DefaultHasher;
fn matches_token(path_fragment: &str, path_token: &PathToken) -> usize {
match path_token {
PathToken::Root if path_fragment == "$" => 2,
PathToken::Field(name) if path_fragment == name => 2,
PathToken::Index(index) => match path_fragment.parse::<usize>() {
Ok(i) if *index == i => 2,
_ => 0
},
PathToken::StarIndex => match path_fragment.parse::<usize>() {
Ok(_) => 1,
_ => 0
},
PathToken::Star => 1,
_ => 0
}
}
pub(crate) fn calc_path_weight(path_exp: &str, path: &[&str]) -> (usize, usize) {
let weight = match parse_path_exp(path_exp) {
Ok(path_tokens) => {
trace!("Calculating weight for path tokens '{:?}' and path '{:?}'", path_tokens, path);
if path.len() >= path_tokens.len() {
(
path_tokens.iter().zip(path.iter())
.fold(1, |acc, (token, fragment)| acc * matches_token(fragment, token)),
path_tokens.len()
)
} else {
(0, path_tokens.len())
}
},
Err(err) => {
warn!("Failed to parse path expression - {}", err);
(0, 0)
}
};
trace!("Calculated weight {:?} for path '{}' and '{:?}'", weight, path_exp, path);
weight
}
pub(crate) fn path_length(path_exp: &str) -> usize {
match parse_path_exp(path_exp) {
Ok(path_tokens) => path_tokens.len(),
Err(err) => {
warn!("Failed to parse path expression - {}", err);
0
}
}
}
impl <T: Debug + Display + PartialEq> Matches<Vec<T>> for Vec<T> {
fn matches(&self, actual: &Vec<T>, matcher: &MatchingRule) -> Result<(), String> {
let result = match *matcher {
MatchingRule::Regex(ref regex) => {
match Regex::new(regex) {
Ok(re) => {
let text: String = actual.iter().map(|v| v.to_string()).collect();
if re.is_match(text.as_str()) {
Ok(())
} else {
Err(format!("Expected '{}' to match '{}'", text, regex))
}
}
Err(err) => Err(format!("'{}' is not a valid regular expression - {}", regex, err))
}
}
MatchingRule::Type => Ok(()),
MatchingRule::MinType(min) => {
if actual.len() < min {
Err(format!("Expected list with length {} to have a minimum length of {}", actual.len(), min))
} else {
Ok(())
}
}
MatchingRule::MaxType(max) => {
if actual.len() > max {
Err(format!("Expected list with length {} to have a maximum length of {}", actual.len(), max))
} else {
Ok(())
}
}
MatchingRule::MinMaxType(min, max) => {
if actual.len() < min {
Err(format!("Expected list with length {} to have a minimum length of {}", actual.len(), min))
} else if actual.len() > max {
Err(format!("Expected list with length {} to have a maximum length of {}", actual.len(), max))
} else {
Ok(())
}
}
MatchingRule::Equality | MatchingRule::Values => {
if self == actual {
Ok(())
} else {
Err(format!("Expected {:?} to be equal to {:?}", actual, self))
}
}
_ => Err(format!("Unable to match {:?} using {:?}", self, matcher))
};
log::debug!("Comparing '{:?}' to '{:?}' using {:?} -> {:?}", self, actual, matcher, result);
result
}
}
impl Matches<&[u8]> for Vec<u8> {
fn matches(&self, actual: &&[u8], matcher: &MatchingRule) -> Result<(), String> {
let result = match *matcher {
MatchingRule::Regex(ref regex) => {
match Regex::new(regex) {
Ok(re) => {
let text = from_utf8(actual).unwrap_or_default();
if re.is_match(text) {
Ok(())
} else {
Err(format!("Expected '{}' to match '{}'", text, regex))
}
}
Err(err) => Err(format!("'{}' is not a valid regular expression - {}", regex, err))
}
}
MatchingRule::Type => Ok(()),
MatchingRule::MinType(min) => {
if actual.len() < min {
Err(format!("Expected list with length {} to have a minimum length of {}", actual.len(), min))
} else {
Ok(())
}
}
MatchingRule::MaxType(max) => {
if actual.len() > max {
Err(format!("Expected list with length {} to have a maximum length of {}", actual.len(), max))
} else {
Ok(())
}
}
MatchingRule::MinMaxType(min, max) => {
if actual.len() < min {
Err(format!("Expected list with length {} to have a minimum length of {}", actual.len(), min))
} else if actual.len() > max {
Err(format!("Expected list with length {} to have a maximum length of {}", actual.len(), max))
} else {
Ok(())
}
}
MatchingRule::Equality => {
if self == actual {
Ok(())
} else {
Err(format!("Expected {:?} to be equal to {:?}", actual, self))
}
}
MatchingRule::ContentType(ref expected_content_type) => {
match_content_type(&actual, expected_content_type)
.map_err(|err| format!("Expected data to have a content type of '{}' but was {}", expected_content_type, err))
}
_ => Err(format!("Unable to match {:?} using {:?}", self, matcher))
};
debug!("Comparing list with {} items to one with {} items using {:?} -> {:?}", self.len(), actual.len(), matcher, result);
result
}
}
pub trait DisplayForMismatch {
fn for_mismatch(&self) -> String;
}
impl <T: Display> DisplayForMismatch for HashMap<String, T> {
fn for_mismatch(&self) -> String {
Value::Object(self.iter().map(|(k, v)| (k.clone(), json!(v.to_string()))).collect()).to_string()
}
}
impl <T: Display> DisplayForMismatch for Vec<T> {
fn for_mismatch(&self) -> String {
Value::Array(self.iter().map(|v| json!(v.to_string())).collect()).to_string()
}
}
impl <T: Display> DisplayForMismatch for &[T] {
fn for_mismatch(&self) -> String {
Value::Array(self.iter().map(|v| json!(v.to_string())).collect()).to_string()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
pub enum MatchingRule {
Equality,
Regex(String),
Type,
MinType(usize),
MaxType(usize),
MinMaxType(usize, usize),
Timestamp(String),
Time(String),
Date(String),
Include(String),
Number,
Integer,
Decimal,
Null,
ContentType(String),
ArrayContains(Vec<(usize, MatchingRuleCategory, HashMap<String, Generator>)>),
Values
}
impl MatchingRule {
pub fn from_json(value: &Value) -> Option<MatchingRule> {
match value {
Value::Object(m) => match m.get("match") {
Some(value) => {
let val = json_to_string(value);
match val.as_str() {
"regex" => match m.get(&val) {
Some(s) => Some(MatchingRule::Regex(json_to_string(s))),
None => None
},
"equality" => Some(MatchingRule::Equality),
"include" => match m.get("value") {
Some(s) => Some(MatchingRule::Include(json_to_string(s))),
None => None
},
"type" => match (json_to_num(m.get("min").cloned()), json_to_num(m.get("max").cloned())) {
(Some(min), Some(max)) => Some(MatchingRule::MinMaxType(min, max)),
(Some(min), None) => Some(MatchingRule::MinType(min)),
(None, Some(max)) => Some(MatchingRule::MaxType(max)),
_ => Some(MatchingRule::Type)
},
"number" => Some(MatchingRule::Number),
"integer" => Some(MatchingRule::Integer),
"decimal" => Some(MatchingRule::Decimal),
"real" => Some(MatchingRule::Decimal),
"min" => match json_to_num(m.get(&val).cloned()) {
Some(min) => Some(MatchingRule::MinType(min)),
None => None
},
"max" => match json_to_num(m.get(&val).cloned()) {
Some(max) => Some(MatchingRule::MaxType(max)),
None => None
},
"timestamp" => match m.get("format").or_else(|| m.get(&val)) {
Some(s) => Some(MatchingRule::Timestamp(json_to_string(s))),
None => None
},
"date" => match m.get("format").or_else(|| m.get(&val)) {
Some(s) => Some(MatchingRule::Date(json_to_string(s))),
None => None
},
"time" => match m.get("format").or_else(|| m.get(&val)) {
Some(s) => Some(MatchingRule::Time(json_to_string(s))),
None => None
},
"null" => Some(MatchingRule::Null),
"contentType" => match m.get("value") {
Some(s) => Some(MatchingRule::ContentType(json_to_string(s))),
None => None
},
"arrayContains" => match m.get("variants") {
Some(variants) => match variants {
Value::Array(variants) => {
let values = variants.iter().map(|variant| {
let index = json_to_num(variant.get("index").cloned()).unwrap_or_default();
let mut category = MatchingRuleCategory::empty("body");
if let Some(rules) = variant.get("rules") {
category.add_rules_from_json(rules);
} else {
category.add_rule("", MatchingRule::Equality, &RuleLogic::And);
}
let generators = if let Some(generators_json) = variant.get("generators") {
let mut g = Generators::default();
let cat = GeneratorCategory::BODY;
if let Value::Object(map) = generators_json {
for (k, v) in map {
if let Value::Object(ref map) = v {
g.parse_generator_from_map(&cat, map, Some(k.clone()));
}
}
}
g.categories.get(&cat).cloned().unwrap_or_default()
} else {
HashMap::default()
};
(index, category, generators)
}).collect();
Some(MatchingRule::ArrayContains(values))
}
_ => None
}
None => None
}
"values" => Some(MatchingRule::Values),
_ => None
}
},
None => if let Some(val) = m.get("regex") {
Some(MatchingRule::Regex(json_to_string(val)))
} else if let Some(val) = json_to_num(m.get("min").cloned()) {
Some(MatchingRule::MinType(val))
} else if let Some(val) = json_to_num(m.get("max").cloned()) {
Some(MatchingRule::MaxType(val))
} else if let Some(val) = m.get("timestamp") {
Some(MatchingRule::Timestamp(json_to_string(val)))
} else if let Some(val) = m.get("time") {
Some(MatchingRule::Time(json_to_string(val)))
} else if let Some(val) = m.get("date") {
Some(MatchingRule::Date(json_to_string(val)))
} else {
None
}
},
_ => None
}
}
pub fn to_json(&self) -> Value {
match self {
MatchingRule::Equality => json!({ "match": "equality" }),
MatchingRule::Regex(ref r) => json!({ "match": "regex",
"regex": r.clone() }),
MatchingRule::Type => json!({ "match": "type" }),
MatchingRule::MinType(min) => json!({ "match": "type",
"min": json!(*min as u64) }),
MatchingRule::MaxType(max) => json!({ "match": "type",
"max": json!(*max as u64) }),
MatchingRule::MinMaxType(min, max) => json!({ "match": "type",
"min": json!(*min as u64), "max": json!(*max as u64) }),
MatchingRule::Timestamp(ref t) => json!({ "match": "timestamp",
"timestamp": Value::String(t.clone()) }),
MatchingRule::Time(ref t) => json!({ "match": "time",
"time": Value::String(t.clone()) }),
MatchingRule::Date(ref d) => json!({ "match": "date",
"date": Value::String(d.clone()) }),
MatchingRule::Include(ref s) => json!({ "match": "include",
"value": Value::String(s.clone()) }),
MatchingRule::Number => json!({ "match": "number" }),
MatchingRule::Integer => json!({ "match": "integer" }),
MatchingRule::Decimal => json!({ "match": "decimal" }),
MatchingRule::Null => json!({ "match": "null" }),
MatchingRule::ContentType(ref r) => json!({ "match": "contentType",
"value": Value::String(r.clone()) }),
MatchingRule::ArrayContains(variants) => json!({
"match": "arrayContains",
"variants": variants.iter().map(|(index, rules, generators)| {
let mut json = json!({
"index": index,
"rules": rules.to_v3_json()
});
if !generators.is_empty() {
json["generators"] = Value::Object(generators.iter()
.map(|(k, gen)| {
if let Some(json) = gen.to_json() {
Some((k.clone(), json))
} else {
None
}
})
.filter(|item| item.is_some())
.map(|item| item.unwrap())
.collect())
}
json
}).collect::<Vec<Value>>()
}),
MatchingRule::Values => json!({ "match": "values" })
}
}
pub fn compare_maps<T: Display + Debug>(
&self,
path: &[&str],
expected: &HashMap<String, T>,
actual: &HashMap<String, T>,
context: &MatchingContext,
callback: &mut dyn FnMut(&Vec<&str>, &T, &T
) -> Result<(), Vec<Mismatch>>) -> Result<(), Vec<Mismatch>> {
let mut result = Ok(());
if context.values_matcher_defined(&path) {
debug!("Values matcher is defined for path {:?}", path);
for (key, value) in actual.iter() {
let mut p = path.to_vec();
p.push(key);
if expected.contains_key(key) {
result = merge_result(result, callback(&p, &expected[key], value));
} else if !expected.is_empty() {
result = merge_result(result, callback(&p, &expected.values().next().unwrap(), value));
}
}
} else {
result = merge_result(result, context.match_keys(path, &expected, &actual));
for (key, value) in expected.iter() {
if actual.contains_key(key) {
let mut p = path.to_vec();
p.push(key);
result = merge_result(result, callback(&p, value, &actual[key]));
}
}
}
result
}
pub fn compare_lists<T: Display + Debug + PartialEq + Clone + Sized>(
&self,
path: &[&str],
expected: &Vec<T>,
actual: &Vec<T>,
context: &MatchingContext,
callback: &dyn Fn(&[&str], &T, &T, &MatchingContext) -> Result<(), Vec<Mismatch>>
) -> Result<(), Vec<Mismatch>> {
let mut result = Ok(());
match self {
MatchingRule::ArrayContains(variants) => {
let variants = if variants.is_empty() {
expected.iter().enumerate().map(|(index, _)| {
(index, MatchingRuleCategory::equality("body"), HashMap::default())
}).collect()
} else {
variants.clone()
};
for (index, rules, _) in variants {
match expected.get(index) {
Some(expected_value) => {
let context = context.clone_with(&rules);
let predicate: &dyn Fn(&(usize, &T)) -> bool = &|&(actual_index, value)| {
debug!("Comparing list item {} with value '{:?}' to '{:?}'", actual_index, value, expected_value);
callback(&vec!["$"], expected_value, value, &context).is_ok()
};
if actual.iter().enumerate().find(predicate).is_none() {
result = merge_result(result,Err(vec![ Mismatch::BodyMismatch {
path: path.join("."),
expected: Some(expected_value.to_string().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Variant at index {} ({}) was not found in the actual list", index, expected_value)
} ]));
};
},
None => {
result = merge_result(result,Err(vec![ Mismatch::BodyMismatch {
path: path.join("."),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("ArrayContains: variant {} is missing from the expected list, which has {} items",
index, expected.len())
} ]));
}
}
}
}
_ => {
if let Err(messages) = match_values(path, context, expected, actual) {
for message in messages {
result = merge_result(result,Err(vec![ Mismatch::BodyMismatch {
path: path.join("."),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: message.clone()
} ]));
}
}
let mut expected_list = Vec::new();
if let Some(expected_example) = expected.first() {
expected_list.resize(actual.len(), (*expected_example).clone());
}
for (index, value) in expected_list.iter().enumerate() {
let ps = index.to_string();
log::debug!("Comparing list item {} with value '{:?}' to '{:?}'", index, actual.get(index), value);
let mut p = path.to_vec();
p.push(ps.as_str());
if index < actual.len() {
result = merge_result(result, callback(&p, value, &actual[index], context));
} else if !context.matcher_is_defined(&p) {
result = merge_result(result,Err(vec![ Mismatch::BodyMismatch { path: path.join("."),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Expected {} but was missing", value) } ]))
}
}
}
}
result
}
pub fn has_generators(&self) -> bool {
match self {
MatchingRule::ArrayContains(variants) => variants.iter()
.any(|(_, _, generators)| !generators.is_empty()),
_ => false
}
}
pub fn generators(&self) -> Vec<Generator> {
match self {
MatchingRule::ArrayContains(variants) => vec![Generator::ArrayContains(variants.clone())],
_ => vec![]
}
}
}
impl Hash for MatchingRule {
fn hash<H: Hasher>(&self, state: &mut H) {
mem::discriminant(self).hash(state);
match self {
MatchingRule::Regex(s) => s.hash(state),
MatchingRule::MinType(min) => min.hash(state),
MatchingRule::MaxType(max) => max.hash(state),
MatchingRule::MinMaxType(min, max) => {
min.hash(state);
max.hash(state);
}
MatchingRule::Timestamp(format) => format.hash(state),
MatchingRule::Time(format) => format.hash(state),
MatchingRule::Date(format) => format.hash(state),
MatchingRule::Include(str) => str.hash(state),
MatchingRule::ContentType(str) => str.hash(state),
MatchingRule::ArrayContains(variants) => {
for (index, rules, generators) in variants {
index.hash(state);
rules.hash(state);
for (s, g) in generators {
s.hash(state);
g.hash(state);
}
}
}
_ => ()
}
}
}
impl PartialEq for MatchingRule {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(MatchingRule::Regex(s1), MatchingRule::Regex(s2)) => s1 == s2,
(MatchingRule::MinType(min1), MatchingRule::MinType(min2)) => min1 == min2,
(MatchingRule::MaxType(max1), MatchingRule::MaxType(max2)) => max1 == max2,
(MatchingRule::MinMaxType(min1, max1), MatchingRule::MinMaxType(min2, max2)) => min1 == min2 && max1 == max2,
(MatchingRule::Timestamp(format1), MatchingRule::Timestamp(format2)) => format1 == format2,
(MatchingRule::Time(format1), MatchingRule::Time(format2)) => format1 == format2,
(MatchingRule::Date(format1), MatchingRule::Date(format2)) => format1 == format2,
(MatchingRule::Include(str1), MatchingRule::Include(str2)) => str1 == str2,
(MatchingRule::ContentType(str1), MatchingRule::ContentType(str2)) => str1 == str2,
(MatchingRule::ArrayContains(variants1), MatchingRule::ArrayContains(variants2)) => variants1 == variants2,
_ => mem::discriminant(self) == mem::discriminant(other)
}
}
}
#[cfg(test)]
fn h(rule: &MatchingRule) -> u64 {
let mut hasher = DefaultHasher::new();
rule.hash(&mut hasher);
hasher.finish()
}
#[test]
fn hash_and_partial_eq_for_matching_rule() {
expect!(h(&MatchingRule::Equality)).to(be_equal_to(h(&MatchingRule::Equality)));
expect!(MatchingRule::Equality).to(be_equal_to(MatchingRule::Equality));
expect!(MatchingRule::Equality).to_not(be_equal_to(MatchingRule::Type));
expect!(h(&MatchingRule::Type)).to(be_equal_to(h(&MatchingRule::Type)));
expect!(MatchingRule::Type).to(be_equal_to(MatchingRule::Type));
expect!(h(&MatchingRule::Number)).to(be_equal_to(h(&MatchingRule::Number)));
expect!(MatchingRule::Number).to(be_equal_to(MatchingRule::Number));
expect!(h(&MatchingRule::Integer)).to(be_equal_to(h(&MatchingRule::Integer)));
expect!(MatchingRule::Integer).to(be_equal_to(MatchingRule::Integer));
expect!(h(&MatchingRule::Decimal)).to(be_equal_to(h(&MatchingRule::Decimal)));
expect!(MatchingRule::Decimal).to(be_equal_to(MatchingRule::Decimal));
expect!(h(&MatchingRule::Null)).to(be_equal_to(h(&MatchingRule::Null)));
expect!(MatchingRule::Null).to(be_equal_to(MatchingRule::Null));
let regex1 = MatchingRule::Regex("\\d+".into());
let regex2 = MatchingRule::Regex("\\w+".into());
expect!(h(®ex1)).to(be_equal_to(h(®ex1)));
expect!(®ex1).to(be_equal_to(®ex1));
expect!(h(®ex1)).to_not(be_equal_to(h(®ex2)));
expect!(®ex1).to_not(be_equal_to(®ex2));
let min1 = MatchingRule::MinType(100);
let min2 = MatchingRule::MinType(200);
expect!(h(&min1)).to(be_equal_to(h(&min1)));
expect!(&min1).to(be_equal_to(&min1));
expect!(h(&min1)).to_not(be_equal_to(h(&min2)));
expect!(&min1).to_not(be_equal_to(&min2));
let max1 = MatchingRule::MaxType(100);
let max2 = MatchingRule::MaxType(200);
expect!(h(&max1)).to(be_equal_to(h(&max1)));
expect!(&max1).to(be_equal_to(&max1));
expect!(h(&max1)).to_not(be_equal_to(h(&max2)));
expect!(&max1).to_not(be_equal_to(&max2));
let minmax1 = MatchingRule::MinMaxType(100, 200);
let minmax2 = MatchingRule::MinMaxType(200, 200);
expect!(h(&minmax1)).to(be_equal_to(h(&minmax1)));
expect!(&minmax1).to(be_equal_to(&minmax1));
expect!(h(&minmax1)).to_not(be_equal_to(h(&minmax2)));
expect!(&minmax1).to_not(be_equal_to(&minmax2));
let datetime1 = MatchingRule::Timestamp("yyyy-MM-dd HH:mm:ss".into());
let datetime2 = MatchingRule::Timestamp("yyyy-MM-ddTHH:mm:ss".into());
expect!(h(&datetime1)).to(be_equal_to(h(&datetime1)));
expect!(&datetime1).to(be_equal_to(&datetime1));
expect!(h(&datetime1)).to_not(be_equal_to(h(&datetime2)));
expect!(&datetime1).to_not(be_equal_to(&datetime2));
let date1 = MatchingRule::Date("yyyy-MM-dd".into());
let date2 = MatchingRule::Date("yy-MM-dd".into());
expect!(h(&date1)).to(be_equal_to(h(&date1)));
expect!(&date1).to(be_equal_to(&date1));
expect!(h(&date1)).to_not(be_equal_to(h(&date2)));
expect!(&date1).to_not(be_equal_to(&date2));
let time1 = MatchingRule::Time("HH:mm:ss".into());
let time2 = MatchingRule::Time("hh:mm:ss".into());
expect!(h(&time1)).to(be_equal_to(h(&time1)));
expect!(&time1).to(be_equal_to(&time1));
expect!(h(&time1)).to_not(be_equal_to(h(&time2)));
expect!(&time1).to_not(be_equal_to(&time2));
let inc1 = MatchingRule::Include("string one".into());
let inc2 = MatchingRule::Include("string two".into());
expect!(h(&inc1)).to(be_equal_to(h(&inc1)));
expect!(&inc1).to(be_equal_to(&inc1));
expect!(h(&inc1)).to_not(be_equal_to(h(&inc2)));
expect!(&inc1).to_not(be_equal_to(&inc2));
let content1 = MatchingRule::ContentType("one".into());
let content2 = MatchingRule::ContentType("two".into());
expect!(h(&content1)).to(be_equal_to(h(&content1)));
expect!(&content1).to(be_equal_to(&content1));
expect!(h(&content1)).to_not(be_equal_to(h(&content2)));
expect!(&content1).to_not(be_equal_to(&content2));
let ac1 = MatchingRule::ArrayContains(vec![]);
let ac2 = MatchingRule::ArrayContains(vec![(0, MatchingRuleCategory::empty("body"), hashmap!{})]);
let ac3 = MatchingRule::ArrayContains(vec![(1, MatchingRuleCategory::empty("body"), hashmap!{})]);
let ac4 = MatchingRule::ArrayContains(vec![(0, MatchingRuleCategory::equality("body"), hashmap!{})]);
let ac5 = MatchingRule::ArrayContains(vec![(0, MatchingRuleCategory::empty("body"), hashmap!{ "A".to_string() => Generator::RandomBoolean })]);
let ac6 = MatchingRule::ArrayContains(vec![
(0, MatchingRuleCategory::empty("body"), hashmap!{ "A".to_string() => Generator::RandomBoolean }),
(1, MatchingRuleCategory::empty("body"), hashmap!{ "A".to_string() => Generator::RandomDecimal(10) })
]);
let ac7 = MatchingRule::ArrayContains(vec![
(0, MatchingRuleCategory::empty("body"), hashmap!{ "A".to_string() => Generator::RandomBoolean }),
(1, MatchingRuleCategory::equality("body"), hashmap!{ "A".to_string() => Generator::RandomDecimal(10) })
]);
expect!(h(&ac1)).to(be_equal_to(h(&ac1)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac1)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac2)).to(be_equal_to(h(&ac2)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac1)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac2)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac3)).to(be_equal_to(h(&ac3)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac1)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac3)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac4)).to(be_equal_to(h(&ac4)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac1)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac4)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac5)).to(be_equal_to(h(&ac5)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac1)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac5)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac6)).to(be_equal_to(h(&ac6)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac1)));
expect!(h(&ac6)).to_not(be_equal_to(h(&ac7)));
expect!(h(&ac7)).to(be_equal_to(h(&ac7)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac2)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac3)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac4)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac5)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac6)));
expect!(h(&ac7)).to_not(be_equal_to(h(&ac1)));
expect!(&ac1).to(be_equal_to(&ac1));
expect!(&ac1).to_not(be_equal_to(&ac2));
expect!(&ac1).to_not(be_equal_to(&ac3));
expect!(&ac1).to_not(be_equal_to(&ac4));
expect!(&ac1).to_not(be_equal_to(&ac5));
expect!(&ac1).to_not(be_equal_to(&ac6));
expect!(&ac1).to_not(be_equal_to(&ac7));
expect!(&ac2).to(be_equal_to(&ac2));
expect!(&ac2).to_not(be_equal_to(&ac1));
expect!(&ac2).to_not(be_equal_to(&ac3));
expect!(&ac2).to_not(be_equal_to(&ac4));
expect!(&ac2).to_not(be_equal_to(&ac5));
expect!(&ac2).to_not(be_equal_to(&ac6));
expect!(&ac2).to_not(be_equal_to(&ac7));
expect!(&ac3).to(be_equal_to(&ac3));
expect!(&ac3).to_not(be_equal_to(&ac2));
expect!(&ac3).to_not(be_equal_to(&ac1));
expect!(&ac3).to_not(be_equal_to(&ac4));
expect!(&ac3).to_not(be_equal_to(&ac5));
expect!(&ac3).to_not(be_equal_to(&ac6));
expect!(&ac3).to_not(be_equal_to(&ac7));
expect!(&ac4).to(be_equal_to(&ac4));
expect!(&ac4).to_not(be_equal_to(&ac2));
expect!(&ac4).to_not(be_equal_to(&ac3));
expect!(&ac4).to_not(be_equal_to(&ac1));
expect!(&ac4).to_not(be_equal_to(&ac5));
expect!(&ac4).to_not(be_equal_to(&ac6));
expect!(&ac4).to_not(be_equal_to(&ac7));
expect!(&ac5).to(be_equal_to(&ac5));
expect!(&ac5).to_not(be_equal_to(&ac2));
expect!(&ac5).to_not(be_equal_to(&ac3));
expect!(&ac5).to_not(be_equal_to(&ac4));
expect!(&ac5).to_not(be_equal_to(&ac1));
expect!(&ac5).to_not(be_equal_to(&ac6));
expect!(&ac5).to_not(be_equal_to(&ac7));
expect!(&ac6).to(be_equal_to(&ac6));
expect!(&ac6).to_not(be_equal_to(&ac2));
expect!(&ac6).to_not(be_equal_to(&ac3));
expect!(&ac6).to_not(be_equal_to(&ac4));
expect!(&ac6).to_not(be_equal_to(&ac5));
expect!(&ac6).to_not(be_equal_to(&ac1));
expect!(&ac6).to_not(be_equal_to(&ac7));
expect!(&ac7).to(be_equal_to(&ac7));
expect!(&ac7).to_not(be_equal_to(&ac2));
expect!(&ac7).to_not(be_equal_to(&ac3));
expect!(&ac7).to_not(be_equal_to(&ac4));
expect!(&ac7).to_not(be_equal_to(&ac5));
expect!(&ac7).to_not(be_equal_to(&ac6));
expect!(&ac7).to_not(be_equal_to(&ac1));
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Eq, Hash, PartialOrd, Ord)]
pub enum RuleLogic {
And,
Or
}
impl RuleLogic {
fn to_json(&self) -> Value {
Value::String(match self {
RuleLogic::And => "AND",
RuleLogic::Or => "OR"
}.into())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
pub struct RuleList {
pub rules: Vec<MatchingRule>,
pub rule_logic: RuleLogic
}
impl RuleList {
pub fn empty(rule_logic: &RuleLogic) -> RuleList {
RuleList {
rules: Vec::new(),
rule_logic: rule_logic.clone()
}
}
pub fn equality() -> RuleList {
RuleList {
rules: vec![ MatchingRule::Equality ],
rule_logic: RuleLogic::And
}
}
pub fn new(rule: MatchingRule) -> RuleList {
RuleList {
rules: vec![ rule ],
rule_logic: RuleLogic::And
}
}
fn to_v3_json(&self) -> Value {
json!({
s!("combine"): self.rule_logic.to_json(),
s!("matchers"): Value::Array(self.rules.iter().map(|matcher| matcher.to_json()).collect())
})
}
fn to_v2_json(&self) -> Value {
match self.rules.get(0) {
Some(rule) => rule.to_json(),
None => json!({})
}
}
pub fn type_matcher_defined(&self) -> bool {
self.rules.iter().any(|rule| match rule {
MatchingRule::Type => true,
MatchingRule::MinType(_) => true,
MatchingRule::MaxType(_) => true,
MatchingRule::MinMaxType(_, _) => true,
_ => false
})
}
pub fn values_matcher_defined(&self) -> bool {
self.rules.iter().any(|rule| match rule {
MatchingRule::Values => true,
_ => false
})
}
pub fn add_rule(&mut self, rule: &MatchingRule) {
self.rules.push(rule.clone())
}
}
impl Hash for RuleList {
fn hash<H: Hasher>(&self, state: &mut H) {
self.rule_logic.hash(state);
for rule in &self.rules {
rule.hash(state);
}
}
}
impl PartialEq for RuleList {
fn eq(&self, other: &Self) -> bool {
self.rule_logic == other.rule_logic &&
self.rules == other.rules
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, Default)]
pub struct MatchingRuleCategory {
pub name: String,
pub rules: HashMap<String, RuleList>
}
impl MatchingRuleCategory {
pub fn empty<S>(name: S) -> MatchingRuleCategory
where S: Into<String>
{
MatchingRuleCategory {
name: name.into(),
rules: hashmap! {},
}
}
pub fn equality<S>(name: S) -> MatchingRuleCategory
where S: Into<String>
{
MatchingRuleCategory {
name: name.into(),
rules: hashmap! {
"".to_string() => RuleList::equality()
}
}
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn is_not_empty(&self) -> bool {
!self.rules.is_empty()
}
pub fn rule_from_json(&mut self, key: &str, matcher_json: &Value, rule_logic: &RuleLogic) {
match MatchingRule::from_json(matcher_json) {
Some(matching_rule) => {
let rules = self.rules.entry(key.to_string()).or_insert_with(|| RuleList::empty(rule_logic));
rules.rules.push(matching_rule);
},
None => log::warn!("Could not parse matcher {:?}", matcher_json)
}
}
pub fn add_rule<S>(&mut self, key: S, matcher: MatchingRule, rule_logic: &RuleLogic)
where S: Into<String> {
let rules = self.rules.entry(key.into()).or_insert_with(|| RuleList::empty(rule_logic));
rules.rules.push(matcher);
}
pub fn filter<F>(&self, predicate: F) -> MatchingRuleCategory
where F : Fn(&(&String, &RuleList)) -> bool {
MatchingRuleCategory {
name: self.name.clone(),
rules: self.rules.iter().filter(predicate)
.map(|(path, rules)| (path.clone(), rules.clone())).collect()
}
}
fn max_by_path(&self, path: &[&str]) -> Option<RuleList> {
self.rules.iter().map(|(k, v)| (k, v, calc_path_weight(k.as_str(), path)))
.filter(|&(_, _, w)| w.0 > 0)
.max_by_key(|&(_, _, w)| w.0 * w.1)
.map(|(_, v, _)| v.clone())
}
pub fn to_v3_json(&self) -> Value {
Value::Object(self.rules.iter().fold(serde_json::Map::new(), |mut map, (category, rulelist)| {
map.insert(category.clone(), rulelist.to_v3_json());
map
}))
}
pub fn to_v2_json(&self) -> HashMap<String, Value> {
let mut map = hashmap!{};
if self.name == "body" {
for (k, v) in self.rules.clone() {
map.insert(k.replace("$", "$.body"), v.to_v2_json());
}
} else {
for (k, v) in self.rules.clone() {
map.insert(format!("$.{}.{}", self.name, k), v.to_v2_json());
}
}
map
}
pub fn type_matcher_defined(&self) -> bool {
self.rules.values().any(|rule_list| rule_list.type_matcher_defined())
}
pub fn values_matcher_defined(&self) -> bool {
self.rules.values().any(|rule_list| rule_list.values_matcher_defined())
}
pub fn matcher_is_defined(&self, path: &[&str]) -> bool {
let result = !self.resolve_matchers_for_path(path).is_empty();
trace!("matcher_is_defined: for category {} and path {:?} -> {}", self.name, path, result);
result
}
pub fn resolve_matchers_for_path(&self, path: &[&str]) -> MatchingRuleCategory {
if self.name == "body" || self.name == "header" || self.name == "query" ||
self.name == "content" || self.name == "metadata" {
self.filter(|(val, _)| {
calc_path_weight(val, path).0 > 0
})
} else {
self.clone()
}
}
pub fn select_best_matcher(&self, path: &[&str]) -> Option<RuleList> {
if self.name == "body" || self.name == "content" {
self.max_by_path(path)
} else {
self.resolve_matchers_for_path(path).as_rule_list()
}
}
pub fn as_rule_list(&self) -> Option<RuleList> {
self.rules.values().next().cloned()
}
pub fn add_rules_from_json(&mut self, rules: &Value) {
if self.name == "path" && rules.get("matchers").is_some() {
let rule_logic = match rules.get("combine") {
Some(val) => if json_to_string(val).to_uppercase() == "OR" {
RuleLogic::Or
} else {
RuleLogic::And
},
None => RuleLogic::And
};
if let Some(matchers) = rules.get("matchers") {
if let Value::Array(array) = matchers {
for matcher in array {
self.rule_from_json("", &matcher, &rule_logic)
}
}
}
} else if let Value::Object(m) = rules {
if m.contains_key("matchers") {
self.add_rule_list("", rules);
} else {
for (k, v) in m {
self.add_rule_list(k, v);
}
}
}
}
fn add_rule_list(&mut self, k: &str, v: &Value) {
let rule_logic = match v.get("combine") {
Some(val) => if json_to_string(val).to_uppercase() == "OR" {
RuleLogic::Or
} else {
RuleLogic::And
},
None => RuleLogic::And
};
match v.get("matchers") {
Some(matchers) => match matchers {
&Value::Array(ref array) => for matcher in array {
self.rule_from_json(k, &matcher, &rule_logic)
},
_ => ()
},
None => ()
}
}
pub fn generators(&self) -> HashMap<String, Generator> {
let mut generators = hashmap!{};
for (base_path, rules) in &self.rules {
for rule in &rules.rules {
if rule.has_generators() {
for generator in rule.generators() {
generators.insert(base_path.to_owned(), generator);
}
}
}
}
generators
}
pub fn rename(&self, name: &str) -> Self {
MatchingRuleCategory {
name: name.into(),
.. self.clone()
}
}
}
impl Hash for MatchingRuleCategory {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
for (k, v) in self.rules.clone() {
k.hash(state);
v.hash(state);
}
}
}
impl PartialEq for MatchingRuleCategory {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.rules == other.rules
}
fn ne(&self, other: &Self) -> bool {
self.name != other.name || self.rules != other.rules
}
}
impl PartialOrd for MatchingRuleCategory {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.name.partial_cmp(&other.name)
}
}
impl Ord for MatchingRuleCategory {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
#[serde(transparent)]
pub struct MatchingRules {
pub rules: HashMap<String, MatchingRuleCategory>
}
impl MatchingRules {
pub fn is_empty(&self) -> bool {
self.rules.values().all(|category| category.is_empty())
}
pub fn is_not_empty(&self) -> bool {
self.rules.values().any(|category| category.is_not_empty())
}
pub fn add_category<S>(&mut self, category: S) -> &mut MatchingRuleCategory
where S: Into<String>
{
let category = category.into();
if !self.rules.contains_key(&category) {
self.rules.insert(category.clone(), MatchingRuleCategory::empty(category.clone()));
}
self.rules.get_mut(&category).unwrap()
}
pub fn categories(&self) -> HashSet<String> {
self.rules.keys().cloned().collect()
}
pub fn rules_for_category(&self, category: &str) -> Option<MatchingRuleCategory> {
self.rules.get(category).cloned()
}
pub fn matcher_is_defined(&self, category: &str, path: &Vec<&str>) -> bool {
let result = match self.resolve_matchers(category, path) {
Some(ref category) => !category.is_empty(),
None => false
};
trace!("matcher_is_defined for category {} and path {:?} -> {}", category, path, result);
result
}
pub fn wildcard_matcher_is_defined(&self, category: &str, path: &Vec<&str>) -> bool {
match self.resolve_wildcard_matchers(category, path) {
Some(ref category) => !category.filter(|&(val, _)| val.ends_with(".*")).is_empty(),
None => false
}
}
pub fn type_matcher_defined(&self, category: &str, path: &Vec<&str>) -> bool {
let result = match self.resolve_matchers(category, path) {
Some(ref category) => category.type_matcher_defined(),
None => false
};
trace!("type_matcher_defined for category {} and path {:?} -> {}", category, path, result);
result
}
pub fn resolve_matchers(&self, category: &str, path: &Vec<&str>) -> Option<MatchingRuleCategory> {
self.rules_for_category(category)
.map(|category| category.resolve_matchers_for_path(path))
}
pub fn resolve_body_matchers_by_path(&self, path: &Vec<&str>) -> Option<RuleList> {
match self.rules_for_category(&s!("body")) {
Some(category) => category.max_by_path(path),
None => None
}
}
fn resolve_wildcard_matchers(&self, category: &str, path: &Vec<&str>) -> Option<MatchingRuleCategory> {
if category == "body" {
self.rules_for_category(category).map(|category| category.filter(|&(val, _)| {
calc_path_weight(val, path).0 > 0 && path_length(val) == path.len()
}))
} else if category == "header" || category == "query" {
self.rules_for_category(category).map(|category| category.filter(|&(val, _)| {
path.len() == 1 && path[0] == *val
}))
} else {
self.rules_for_category(category)
}
}
fn load_from_v2_map(&mut self, map: &serde_json::Map<String, Value>) {
for (key, v) in map {
let path = key.split('.').map(|p| s!(p)).collect::<Vec<String>>();
if key.starts_with("$.body") {
if key == "$.body" {
self.add_v2_rule(s!("body"), s!("$"), v);
} else {
self.add_v2_rule(s!("body"), format!("${}", s!(key[6..])), v);
}
} else if key.starts_with("$.headers") {
self.add_v2_rule(s!("header"), path[2].clone(), v);
} else {
self.add_v2_rule(path[1].clone(), if path.len() > 2 { path[2].clone() } else { s!("") }, v);
}
}
}
fn load_from_v3_map(&mut self, map: &serde_json::Map<String, Value>) {
for (k, v) in map {
self.add_rules(k, v);
}
}
fn add_rules(&mut self, category_name: &String, rules: &Value) {
let category = self.add_category(category_name.clone());
category.add_rules_from_json(rules)
}
fn add_v2_rule(&mut self, category_name: String, sub_category: String, rule: &Value) {
let category = self.add_category(category_name);
category.rule_from_json(sub_category.as_str(), rule, &RuleLogic::And);
}
fn to_v3_json(&self) -> Value {
Value::Object(self.rules.iter().fold(serde_json::Map::new(), |mut map, (name, sub_category)| {
if name == "path" {
if let Some(rules) = sub_category.rules.get("") {
map.insert(name.clone(), rules.to_v3_json());
}
} else {
map.insert(name.clone(), sub_category.to_v3_json());
}
map
}))
}
fn to_v2_json(&self) -> Value {
Value::Object(self.rules.iter().fold(serde_json::Map::new(), |mut map, (_, category)| {
for (key, value) in category.to_v2_json() {
map.insert(key.clone(), value);
}
map
}))
}
pub fn rename(&self, old_name: &str, new_name: &str) -> Self {
MatchingRules {
rules: self.rules.iter().map(|(key, value)| {
if key == old_name {
(new_name.to_string(), value.rename(new_name))
} else {
(key.clone(), value.clone())
}
}).collect()
}
}
}
impl Hash for MatchingRules {
fn hash<H: Hasher>(&self, state: &mut H) {
for (k, v) in self.rules.iter() {
k.hash(state);
v.hash(state);
}
}
}
impl PartialEq for MatchingRules {
fn eq(&self, other: &Self) -> bool {
self.rules == other.rules
}
fn ne(&self, other: &Self) -> bool {
self.rules != other.rules
}
}
impl Default for MatchingRules {
fn default() -> Self {
MatchingRules {
rules: hashmap!{}
}
}
}
pub fn matchers_from_json(value: &Value, deprecated_name: &Option<String>) -> MatchingRules {
let matchers_json = match (value.get("matchingRules"), deprecated_name.clone().and_then(|name| value.get(&name))) {
(Some(v), _) => Some(v),
(None, Some(v)) => Some(v),
(None, None) => None
};
let mut matching_rules = MatchingRules::default();
match matchers_json {
Some(value) => match value {
&Value::Object(ref m) => {
if m.keys().next().unwrap_or(&String::default()).starts_with("$") {
matching_rules.load_from_v2_map(m)
} else {
matching_rules.load_from_v3_map(m)
}
},
_ => ()
},
None => ()
}
matching_rules
}
pub fn matchers_to_json(matchers: &MatchingRules, spec_version: &PactSpecification) -> Value {
match spec_version {
&PactSpecification::V3 | &PactSpecification::V4 => matchers.to_v3_json(),
_ => matchers.to_v2_json()
}
}
#[macro_export]
macro_rules! matchingrules {
( $( $name:expr => {
$( $subname:expr => [ $( $matcher:expr ), * ] ),*
}), * ) => {{
let mut _rules = $crate::models::matchingrules::MatchingRules::default();
$({
let mut _category = _rules.add_category($name);
$({
$({
_category.add_rule(&$subname.to_string(), $matcher, &$crate::models::matchingrules::RuleLogic::And);
})*
})*
})*
_rules
}};
}
#[macro_export]
macro_rules! matchingrules_list {
( $name:expr ; $( $subname:expr => [ $( $matcher:expr ), * ] ),* ) => {{
let mut _category = $crate::models::matchingrules::MatchingRuleCategory::empty($name);
$(
$(
_category.add_rule($subname, $matcher, &$crate::models::matchingrules::RuleLogic::And);
)*
)*
_category
}};
( $name:expr ; [ $( $matcher:expr ), * ] ) => {{
let mut _category = $crate::models::matchingrules::MatchingRuleCategory::empty($name);
$(
_category.add_rule("", $matcher, &$crate::models::matchingrules::RuleLogic::And);
)*
_category
}};
}
#[cfg(test)]
mod tests {
use expectest::expect;
use expectest::prelude::*;
use serde_json::Value;
use speculate::speculate;
use super::*;
use super::super::*;
use crate::models::generators::*;
#[test]
fn rules_are_empty_when_there_are_no_categories() {
expect!(MatchingRules::default().is_empty()).to(be_true());
}
#[test]
fn rules_are_empty_when_there_are_only_empty_categories() {
expect!(MatchingRules {
rules: hashmap!{
s!("body") => MatchingRuleCategory::empty("body"),
s!("header") => MatchingRuleCategory::empty("header"),
s!("query") => MatchingRuleCategory::empty("query")
}
}.is_empty()).to(be_true());
}
#[test]
fn rules_are_not_empty_when_there_is_a_nonempty_category() {
expect!(MatchingRules {
rules: hashmap!{
"body".into() => MatchingRuleCategory::empty("body"),
"header".into() => MatchingRuleCategory::empty("headers"),
"query".into() => MatchingRuleCategory {
name: "query".into(),
rules: hashmap!{
"".into() => RuleList {
rules: vec![ MatchingRule::Equality ],
rule_logic: RuleLogic::And
}
}
},
}
}.is_empty()).to(be_false());
}
#[test]
fn matchers_from_json_test() {
expect!(matchers_from_json(&Value::Null, &None).rules.iter()).to(be_empty());
}
#[test]
fn loads_v2_matching_rules() {
let matching_rules_json = Value::from_str(r#"{"matchingRules": {
"$.path": { "match": "regex", "regex": "\\w+" },
"$.query.Q1": { "match": "regex", "regex": "\\d+" },
"$.header.HEADERY": {"match": "include", "value": "ValueA"},
"$.body.animals": {"min": 1, "match": "type"},
"$.body.animals[*].*": {"match": "type"},
"$.body.animals[*].children": {"min": 1},
"$.body.animals[*].children[*].*": {"match": "type"}
}}"#).unwrap();
let matching_rules = matchers_from_json(&matching_rules_json, &None);
expect!(matching_rules.rules.iter()).to_not(be_empty());
expect!(matching_rules.categories()).to(be_equal_to(hashset!{ s!("path"), s!("query"), s!("header"), s!("body") }));
expect!(matching_rules.rules_for_category(&s!("path"))).to(be_some().value(MatchingRuleCategory {
name: s!("path"),
rules: hashmap! { s!("") => RuleList { rules: vec![ MatchingRule::Regex(s!("\\w+")) ], rule_logic: RuleLogic::And } }
}));
expect!(matching_rules.rules_for_category(&s!("query"))).to(be_some().value(MatchingRuleCategory {
name: s!("query"),
rules: hashmap!{ s!("Q1") => RuleList { rules: vec![ MatchingRule::Regex(s!("\\d+")) ], rule_logic: RuleLogic::And } }
}));
expect!(matching_rules.rules_for_category(&s!("header"))).to(be_some().value(MatchingRuleCategory {
name: s!("header"),
rules: hashmap!{ s!("HEADERY") => RuleList { rules: vec![
MatchingRule::Include(s!("ValueA")) ], rule_logic: RuleLogic::And } }
}));
expect!(matching_rules.rules_for_category(&s!("body"))).to(be_some().value(MatchingRuleCategory {
name: s!("body"),
rules: hashmap!{
s!("$.animals") => RuleList { rules: vec![ MatchingRule::MinType(1) ], rule_logic: RuleLogic::And },
s!("$.animals[*].*") => RuleList { rules: vec![ MatchingRule::Type ], rule_logic: RuleLogic::And },
s!("$.animals[*].children") => RuleList { rules: vec![ MatchingRule::MinType(1) ], rule_logic: RuleLogic::And },
s!("$.animals[*].children[*].*") => RuleList { rules: vec![ MatchingRule::Type ], rule_logic: RuleLogic::And }
}
}));
}
#[test]
fn loads_v3_matching_rules() {
let matching_rules_json = Value::from_str(r#"{"matchingRules": {
"path": {
"matchers": [
{ "match": "regex", "regex": "\\w+" }
]
},
"query": {
"Q1": {
"matchers": [
{ "match": "regex", "regex": "\\d+" }
]
}
},
"header": {
"HEADERY": {
"combine": "OR",
"matchers": [
{"match": "include", "value": "ValueA"},
{"match": "include", "value": "ValueB"}
]
}
},
"body": {
"$.animals": {
"matchers": [{"min": 1, "match": "type"}]
},
"$.animals[*].*": {
"matchers": [{"match": "type"}]
},
"$.animals[*].children": {
"matchers": [{"min": 1}]
},
"$.animals[*].children[*].*": {
"matchers": [{"match": "type"}]
}
}
}}"#).unwrap();
let matching_rules = matchers_from_json(&matching_rules_json, &None);
expect!(matching_rules.rules.iter()).to_not(be_empty());
expect!(matching_rules.categories()).to(be_equal_to(hashset!{ s!("path"), s!("query"), s!("header"), s!("body") }));
expect!(matching_rules.rules_for_category(&s!("path"))).to(be_some().value(MatchingRuleCategory {
name: s!("path"),
rules: hashmap! { s!("") => RuleList { rules: vec![ MatchingRule::Regex(s!("\\w+")) ], rule_logic: RuleLogic::And } }
}));
expect!(matching_rules.rules_for_category(&s!("query"))).to(be_some().value(MatchingRuleCategory {
name: s!("query"),
rules: hashmap!{ s!("Q1") => RuleList { rules: vec![ MatchingRule::Regex(s!("\\d+")) ], rule_logic: RuleLogic::And } }
}));
expect!(matching_rules.rules_for_category(&s!("header"))).to(be_some().value(MatchingRuleCategory {
name: s!("header"),
rules: hashmap!{ s!("HEADERY") => RuleList { rules: vec![
MatchingRule::Include(s!("ValueA")),
MatchingRule::Include(s!("ValueB")) ], rule_logic: RuleLogic::Or } }
}));
expect!(matching_rules.rules_for_category(&s!("body"))).to(be_some().value(MatchingRuleCategory {
name: s!("body"),
rules: hashmap!{
s!("$.animals") => RuleList { rules: vec![ MatchingRule::MinType(1) ], rule_logic: RuleLogic::And },
s!("$.animals[*].*") => RuleList { rules: vec![ MatchingRule::Type ], rule_logic: RuleLogic::And },
s!("$.animals[*].children") => RuleList { rules: vec![ MatchingRule::MinType(1) ], rule_logic: RuleLogic::And },
s!("$.animals[*].children[*].*") => RuleList { rules: vec![ MatchingRule::Type ], rule_logic: RuleLogic::And }
}
}));
}
#[test]
fn correctly_loads_v3_matching_rules_with_incorrect_path_format() {
let matching_rules_json = Value::from_str(r#"{"matchingRules": {
"path": {
"": {
"matchers": [
{ "match": "regex", "regex": "\\w+" }
]
}
}
}}"#).unwrap();
let matching_rules = matchers_from_json(&matching_rules_json, &None);
expect!(matching_rules.rules.iter()).to_not(be_empty());
expect!(matching_rules.categories()).to(be_equal_to(hashset!{ s!("path") }));
expect!(matching_rules.rules_for_category(&s!("path"))).to(be_some().value(MatchingRuleCategory {
name: s!("path"),
rules: hashmap! { s!("") => RuleList { rules: vec![ MatchingRule::Regex(s!("\\w+")) ], rule_logic: RuleLogic::And } }
}));
}
speculate! {
describe "generating matcher JSON" {
before {
let matchers = matchingrules!{
"body" => {
"$.a.b" => [ MatchingRule::Type ]
},
"path" => { "" => [ MatchingRule::Regex(s!("/path/\\d+")) ] },
"query" => {
"a" => [ MatchingRule::Regex(s!("\\w+")) ]
},
"header" => {
"item1" => [ MatchingRule::Regex(s!("5")) ]
}
};
}
it "generates V2 matcher format" {
expect!(matchers.to_v2_json().to_string()).to(be_equal_to(
"{\"$.body.a.b\":{\"match\":\"type\"},\
\"$.header.item1\":{\"match\":\"regex\",\"regex\":\"5\"},\
\"$.path.\":{\"match\":\"regex\",\"regex\":\"/path/\\\\d+\"},\
\"$.query.a\":{\"match\":\"regex\",\"regex\":\"\\\\w+\"}}"
));
}
it "generates V3 matcher format" {
expect!(matchers.to_v3_json().to_string()).to(be_equal_to(
"{\"body\":{\"$.a.b\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\
\"header\":{\"item1\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"regex\",\"regex\":\"5\"}]}},\
\"path\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"regex\",\"regex\":\"/path/\\\\d+\"}]},\
\"query\":{\"a\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"regex\",\"regex\":\"\\\\w+\"}]}}}"
));
}
}
}
#[test]
fn matching_rule_from_json_test() {
expect!(MatchingRule::from_json(&Value::from_str("\"test string\"").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("null").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("[]").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("true").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("false").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("100").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("100.10").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"stuff\": 100}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"stuff\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"regex\": \"[0-9]\"}").unwrap())).to(
be_some().value(MatchingRule::Regex(s!("[0-9]"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"min\": 100}").unwrap())).to(
be_some().value(MatchingRule::MinType(100)));
expect!(MatchingRule::from_json(&Value::from_str("{\"max\": 100}").unwrap())).to(
be_some().value(MatchingRule::MaxType(100)));
expect!(MatchingRule::from_json(&Value::from_str("{\"timestamp\": \"yyyy\"}").unwrap())).to(
be_some().value(MatchingRule::Timestamp(s!("yyyy"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"date\": \"yyyy\"}").unwrap())).to(
be_some().value(MatchingRule::Date(s!("yyyy"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"time\": \"hh:mm\"}").unwrap())).to(
be_some().value(MatchingRule::Time(s!("hh:mm"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"regex\", \"regex\": \"[0-9]\"}").unwrap())).to(
be_some().value(MatchingRule::Regex(s!("[0-9]"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"regex\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"equality\"}").unwrap())).to(
be_some().value(MatchingRule::Equality));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"include\", \"value\": \"A\"}").unwrap())).to(
be_some().value(MatchingRule::Include(s!("A"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"include\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"type\", \"min\": 1}").unwrap())).to(
be_some().value(MatchingRule::MinType(1)));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"type\", \"max\": \"1\"}").unwrap())).to(
be_some().value(MatchingRule::MaxType(1)));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"type\", \"min\": 1, \"max\": \"1\"}").unwrap())).to(
be_some().value(MatchingRule::MinMaxType(1, 1)));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"type\"}").unwrap())).to(
be_some().value(MatchingRule::Type));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"type\", \"value\": 100}").unwrap())).to(
be_some().value(MatchingRule::Type));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"min\", \"min\": 1}").unwrap())).to(
be_some().value(MatchingRule::MinType(1)));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"max\", \"max\": \"1\"}").unwrap())).to(
be_some().value(MatchingRule::MaxType(1)));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"min\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"max\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"number\"}").unwrap())).to(
be_some().value(MatchingRule::Number));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"integer\"}").unwrap())).to(
be_some().value(MatchingRule::Integer));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"decimal\"}").unwrap())).to(
be_some().value(MatchingRule::Decimal));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"real\"}").unwrap())).to(
be_some().value(MatchingRule::Decimal));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"timestamp\", \"timestamp\": \"A\"}").unwrap())).to(
be_some().value(MatchingRule::Timestamp(s!("A"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"timestamp\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"time\", \"time\": \"A\"}").unwrap())).to(
be_some().value(MatchingRule::Time(s!("A"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"time\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"date\", \"date\": \"A\"}").unwrap())).to(
be_some().value(MatchingRule::Date(s!("A"))));
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"date\"}").unwrap())).to(be_none());
expect!(MatchingRule::from_json(&Value::from_str("{\"match\": \"null\"}").unwrap())).to(
be_some().value(MatchingRule::Null));
let json = json!({
"match": "arrayContains",
"variants": []
});
expect!(MatchingRule::from_json(&json)).to(be_some().value(MatchingRule::ArrayContains(vec![])));
let json = json!({
"match": "arrayContains",
"variants": [
{
"index": 0,
"rules": {
"matchers": [ { "match": "equality" } ]
}
}
]
});
expect!(MatchingRule::from_json(&json)).to(be_some().value(
MatchingRule::ArrayContains(
vec![
(0, matchingrules_list! { "body"; [ MatchingRule::Equality ] }, HashMap::default())
])
));
let json = json!({
"match": "arrayContains",
"variants": [
{
"index": 0,
"rules": {
"matchers": [ { "match": "equality" } ]
},
"generators": {
"a": { "type": "Uuid" }
}
}
]
});
let generators = hashmap!{ "a".to_string() => Generator::Uuid };
expect!(MatchingRule::from_json(&json)).to(be_some().value(
MatchingRule::ArrayContains(
vec![
(0, matchingrules_list! { "body"; [ MatchingRule::Equality ] }, generators)
])
));
}
#[test]
fn matcher_is_defined_returns_false_when_there_are_no_matchers() {
let matchers = matchingrules!{};
expect!(matchers.matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_false());
}
#[test]
fn matcher_is_defined_returns_false_when_the_path_does_not_have_a_matcher_entry() {
let matchers = matchingrules!{
"body" => { }
};
expect!(matchers.matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_false());
}
#[test]
fn matcher_is_defined_returns_true_when_the_path_does_have_a_matcher_entry() {
let matchers = matchingrules! {
"body" => {
"$.a.b" => [ MatchingRule::Type ]
}
};
expect!(matchers.matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_true());
}
#[test]
fn matcher_is_defined_returns_false_when_the_path_is_empty() {
let matchers = matchingrules! {
"body" => {
"$.a.b" => [ MatchingRule::Type ]
}
};
expect!(matchers.matcher_is_defined("body", &vec![])).to(be_false());
}
#[test]
fn matcher_is_defined_returns_true_when_the_parent_of_the_path_does_have_a_matcher_entry() {
let matchers = matchingrules!{
"body" => {
"$.a.b" => [ MatchingRule::Type ]
}
};
expect!(matchers.matcher_is_defined("body", &vec!["$", "a", "b", "c"])).to(be_true());
}
#[test]
fn wildcard_matcher_is_defined_returns_false_when_there_are_no_matchers() {
let matchers = matchingrules!{};
expect!(matchers.wildcard_matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_false());
}
#[test]
fn wildcard_matcher_is_defined_returns_false_when_the_path_does_not_have_a_matcher_entry() {
let matchers = matchingrules!{
"body" => { }
};
expect!(matchers.wildcard_matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_false());
}
#[test]
fn wildcard_matcher_is_defined_returns_false_when_the_path_does_have_a_matcher_entry_and_it_is_not_a_wildcard() {
let matchers = matchingrules!{
"body" => {
"$.a.b" => [ MatchingRule::Type ],
"$.*" => [ MatchingRule::Type ]
}
};
expect!(matchers.wildcard_matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_false());
}
#[test]
fn wildcard_matcher_is_defined_returns_true_when_the_path_does_have_a_matcher_entry_and_it_is_a_widcard() {
let matchers = matchingrules!{
"body" => {
"$.a.*" => [ MatchingRule::Type ]
}
};
expect!(matchers.wildcard_matcher_is_defined("body", &vec!["$", "a", "b"])).to(be_true());
}
#[test]
fn wildcard_matcher_is_defined_returns_false_when_the_parent_of_the_path_does_have_a_matcher_entry() {
let matchers = matchingrules!{
"body" => {
"$.a.*" => [ MatchingRule::Type ]
}
};
expect!(matchers.wildcard_matcher_is_defined("body", &vec!["$", "a", "b", "c"])).to(be_false());
}
#[test]
fn matches_token_test_with_root() {
expect!(matches_token("$", &PathToken::Root)).to(be_equal_to(2));
expect!(matches_token("path", &PathToken::Root)).to(be_equal_to(0));
expect!(matches_token("*", &PathToken::Root)).to(be_equal_to(0));
}
#[test]
fn matches_token_test_with_field() {
expect!(matches_token("$", &PathToken::Field(s!("path")))).to(be_equal_to(0));
expect!(matches_token("path", &PathToken::Field(s!("path")))).to(be_equal_to(2));
}
#[test]
fn matches_token_test_with_index() {
expect!(matches_token("$", &PathToken::Index(2))).to(be_equal_to(0));
expect!(matches_token("path", &PathToken::Index(2))).to(be_equal_to(0));
expect!(matches_token("*", &PathToken::Index(2))).to(be_equal_to(0));
expect!(matches_token("1", &PathToken::Index(2))).to(be_equal_to(0));
expect!(matches_token("2", &PathToken::Index(2))).to(be_equal_to(2));
}
#[test]
fn matches_token_test_with_index_wildcard() {
expect!(matches_token("$", &PathToken::StarIndex)).to(be_equal_to(0));
expect!(matches_token("path", &PathToken::StarIndex)).to(be_equal_to(0));
expect!(matches_token("*", &PathToken::StarIndex)).to(be_equal_to(0));
expect!(matches_token("1", &PathToken::StarIndex)).to(be_equal_to(1));
}
#[test]
fn matches_token_test_with_wildcard() {
expect!(matches_token("$", &PathToken::Star)).to(be_equal_to(1));
expect!(matches_token("path", &PathToken::Star)).to(be_equal_to(1));
expect!(matches_token("*", &PathToken::Star)).to(be_equal_to(1));
expect!(matches_token("1", &PathToken::Star)).to(be_equal_to(1));
}
#[test]
fn matches_path_matches_root_path_element() {
expect!(calc_path_weight("$", &vec!["$"]).0 > 0).to(be_true());
expect!(calc_path_weight("$", &vec![]).0 > 0).to(be_false());
}
#[test]
fn matches_path_matches_field_name() {
expect!(calc_path_weight("$.name", &vec!["$", "name"]).0 > 0).to(be_true());
expect!(calc_path_weight("$['name']", &vec!["$", "name"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name.other", &vec!["$", "name", "other"]).0 > 0).to(be_true());
expect!(calc_path_weight("$['name'].other", &vec!["$", "name", "other"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name", &vec!["$", "other"]).0 > 0).to(be_false());
expect!(calc_path_weight("$.name", &vec!["$", "name", "other"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.other", &vec!["$", "name", "other"]).0 > 0).to(be_false());
expect!(calc_path_weight("$.name.other", &vec!["$", "name"]).0 > 0).to(be_false());
}
#[test]
fn matches_path_matches_array_indices() {
expect!(calc_path_weight("$[0]", &vec!["$", "0"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name[1]", &vec!["$", "name", "1"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name", &vec!["$", "0"]).0 > 0).to(be_false());
expect!(calc_path_weight("$.name[1]", &vec!["$", "name", "0"]).0 > 0).to(be_false());
expect!(calc_path_weight("$[1].name", &vec!["$", "name", "1"]).0 > 0).to(be_false());
}
#[test]
fn matches_path_matches_with_wildcard() {
expect!(calc_path_weight("$[*]", &vec!["$", "0"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.*", &vec!["$", "name"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.*.name", &vec!["$", "some", "name"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name[*]", &vec!["$", "name", "0"]).0 > 0).to(be_true());
expect!(calc_path_weight("$.name[*].name", &vec!["$", "name", "1", "name"]).0 > 0).to(be_true());
expect!(calc_path_weight("$[*]", &vec!["$", "name"]).0 > 0).to(be_false());
}
#[test]
fn min_and_max_values_get_serialised_to_json_as_numbers() {
expect!(MatchingRule::MinType(1).to_json().to_string()).to(be_equal_to("{\"match\":\"type\",\"min\":1}"));
expect!(MatchingRule::MaxType(1).to_json().to_string()).to(be_equal_to("{\"match\":\"type\",\"max\":1}"));
expect!(MatchingRule::MinMaxType(1, 10).to_json().to_string()).to(be_equal_to("{\"match\":\"type\",\"max\":10,\"min\":1}"));
}
}