use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config};
use bytes::Bytes;
use serde_json::Value;
use std::{borrow::Cow, convert::TryInto, ops::Deref, sync::Arc};
use crate::{
common::{
data::{HttpMockRegex, HttpMockRequest},
util::HttpMockBytes,
},
server::matchers::comparison::{
distance_for, distance_for_prefix, distance_for_substring, distance_for_suffix,
equal_weight_distance_for, hostname_equals, regex_unmatched_length, string_contains,
string_distance, string_equals, string_has_prefix, string_has_suffix,
},
};
use crate::server::matchers::comparison;
pub trait ValueComparator<S: ?Sized, T: ?Sized> {
fn matches(&self, mock_value: &Option<&S>, req_value: &Option<&T>) -> bool;
fn name(&self) -> &str;
fn distance(&self, mock_value: &Option<&S>, req_value: &Option<&T>) -> usize;
}
pub struct JSONExactMatchComparator {}
impl JSONExactMatchComparator {
pub fn new() -> Self {
Self {}
}
}
impl ValueComparator<Value, Value> for JSONExactMatchComparator {
fn matches(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> bool {
match (mock_value, req_value) {
(None, _) => true,
(Some(_), None) => false,
(Some(mv), Some(rv)) => {
let config = Config::new(CompareMode::Strict);
assert_json_matches_no_panic(rv, mv, config).is_ok()
}
}
}
fn name(&self) -> &str {
"equals"
}
fn distance(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> usize {
let mv_bytes = mock_value.map_or(Vec::new(), |v| v.to_string().into_bytes());
let rv_bytes = req_value.map_or(Vec::new(), |v| v.to_string().into_bytes());
distance_for(&mv_bytes, &rv_bytes)
}
}
pub struct JSONContainsMatchComparator {
pub negated: bool,
}
impl JSONContainsMatchComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<Value, Value> for JSONContainsMatchComparator {
fn matches(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> bool {
match (mock_value, req_value) {
(None, _) => true,
(Some(_), None) => self.negated,
(Some(mv), Some(rv)) => {
let config = Config::new(CompareMode::Inclusive);
let matches = assert_json_matches_no_panic(rv, mv, config).is_ok();
if self.negated {
!matches
} else {
matches
}
}
}
}
fn name(&self) -> &str {
if self.negated {
return "excludes";
}
return "includes";
}
fn distance(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> usize {
let mv_bytes = mock_value.map_or(Vec::new(), |v| v.to_string().into_bytes());
let rv_bytes = req_value.map_or(Vec::new(), |v| v.to_string().into_bytes());
let distance = equal_weight_distance_for(&mv_bytes, &rv_bytes);
if self.negated {
std::cmp::max(mv_bytes.len(), rv_bytes.len()) - distance
} else {
distance
}
}
}
pub struct HostEqualsComparator {
negated: bool,
}
impl HostEqualsComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<String, String> for HostEqualsComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
hostname_equals(self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "not equal to";
}
return "equals";
}
fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize {
if self.matches(mock_value, req_value) {
return 0;
}
string_distance(false, self.negated, mock_value, req_value)
}
}
pub struct StringEqualsComparator {
case_sensitive: bool,
negated: bool,
}
impl StringEqualsComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringEqualsComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
string_equals(self.case_sensitive, self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "not equal to";
}
return "equals";
}
fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize {
string_distance(self.case_sensitive, self.negated, mock_value, req_value)
}
}
pub struct StringContainsComparator {
case_sensitive: bool,
negated: bool,
}
impl StringContainsComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringContainsComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
string_contains(self.case_sensitive, self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "excludes";
}
return "includes";
}
fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize {
let mock_slice = mock_value.as_ref().map(|s| s.as_str());
let req_slice = req_value.as_ref().map(|s| s.as_str());
distance_for_substring(self.case_sensitive, self.negated, &mock_slice, &req_slice)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance_mock_shorter_than_req() {
let binding1 = "hello".to_string();
let mock_value = Some(&binding1);
let binding2 = "hello world".to_string();
let req_value = Some(&binding2);
let comparator = StringContainsComparator {
case_sensitive: true,
negated: false,
};
assert_eq!(comparator.distance(&mock_value, &req_value), 6); }
#[test]
fn test_distance_mock_equal_size_different() {
let binding1 = "hello".to_string();
let mock_value = Some(&binding1);
let binding2 = "world".to_string();
let req_value = Some(&binding2);
let comparator = StringContainsComparator {
case_sensitive: true,
negated: false,
};
assert_eq!(comparator.distance(&mock_value, &req_value), 4); }
#[test]
fn test_distance_exact_match() {
let binding1 = "hello".to_string();
let mock_value = Some(&binding1);
let binding2 = "hello".to_string();
let req_value = Some(&binding2);
let comparator = StringContainsComparator {
case_sensitive: true,
negated: false,
};
assert_eq!(comparator.distance(&mock_value, &req_value), 0); }
}
pub struct StringPrefixMatchComparator {
case_sensitive: bool,
negated: bool,
}
impl StringPrefixMatchComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringPrefixMatchComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
string_has_prefix(self.case_sensitive, self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "prefix not";
}
return "has prefix";
}
fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize {
distance_for_prefix(self.case_sensitive, self.negated, mock_value, req_value)
}
}
pub struct StringSuffixMatchComparator {
case_sensitive: bool,
negated: bool,
}
impl StringSuffixMatchComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringSuffixMatchComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
string_has_suffix(self.case_sensitive, self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "suffix not";
}
return "has suffix";
}
fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize {
distance_for_suffix(self.case_sensitive, self.negated, mock_value, req_value)
}
}
pub struct StringPatternMatchComparator {
case_sensitive: bool,
negated: bool,
}
impl StringPatternMatchComparator {
pub fn new(negated: bool, case_sensitive: bool) -> Self {
Self {
negated,
case_sensitive,
}
}
}
impl ValueComparator<HttpMockRegex, String> for StringPatternMatchComparator {
fn matches(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> bool {
comparison::string_matches_regex(self.negated, self.case_sensitive, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "does not match regex";
}
return "matches regex";
}
fn distance(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> usize {
comparison::regex_string_distance(self.negated, self.case_sensitive, mock_value, req_value)
}
}
pub struct HttpMockBytesPatternComparator {}
impl HttpMockBytesPatternComparator {
pub fn new() -> Self {
Self {}
}
}
impl ValueComparator<HttpMockRegex, HttpMockBytes> for HttpMockBytesPatternComparator {
fn matches(
&self,
mock_value: &Option<&HttpMockRegex>,
req_value: &Option<&HttpMockBytes>,
) -> bool {
match (mock_value, req_value) {
(None, Some(_)) => true,
(Some(_), None) => false,
(Some(mv), Some(rv)) => mv.0.is_match(&rv.to_maybe_lossy_str()),
(None, None) => true,
}
}
fn name(&self) -> &str {
"matches regex"
}
fn distance(
&self,
mock_value: &Option<&HttpMockRegex>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let rv = match req_value {
Some(s) => s.to_maybe_lossy_str(),
None => Cow::Borrowed(""),
};
let default_pattern = HttpMockRegex(regex::Regex::new(".*").unwrap());
let mv = mock_value.unwrap_or(&default_pattern);
regex_unmatched_length(&rv, &mv)
}
}
pub struct StringRegexMatchComparator {}
impl StringRegexMatchComparator {
pub fn new() -> Self {
Self {}
}
}
impl ValueComparator<HttpMockRegex, String> for StringRegexMatchComparator {
fn matches(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> bool {
return match (mock_value, req_value) {
(None, Some(_)) => true,
(Some(_), None) => false,
(Some(mv), Some(rv)) => mv.0.is_match(&rv),
(None, None) => true,
};
}
fn name(&self) -> &str {
"matches regex"
}
fn distance(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> usize {
let rv = req_value.map_or("", |s| s.as_str());
let mut mv = &HttpMockRegex(regex::Regex::new(".*").unwrap());
if mock_value.is_some() {
mv = mock_value.unwrap()
};
regex_unmatched_length(rv, &mv)
}
}
pub struct U16ExactMatchComparator {
negated: bool,
}
impl U16ExactMatchComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<u16, u16> for U16ExactMatchComparator {
fn matches(&self, mock_value: &Option<&u16>, req_value: &Option<&u16>) -> bool {
comparison::integer_equals(self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "not equal to";
}
return "equals";
}
fn distance(&self, mock_value: &Option<&u16>, req_value: &Option<&u16>) -> usize {
comparison::distance_for_usize(mock_value, req_value)
}
}
pub struct BytesExactMatchComparator {
negated: bool,
}
impl BytesExactMatchComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<HttpMockBytes, HttpMockBytes> for BytesExactMatchComparator {
fn matches(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> bool {
return comparison::bytes_equal(self.negated, &mock_value, &req_value);
}
fn name(&self) -> &str {
if self.negated {
return "not equal to";
}
return "equals";
}
fn distance(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let mock_slice = mock_value
.as_ref()
.map(|mv| mv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
let req_slice = req_value
.as_ref()
.map(|rv| rv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
distance_for(mock_slice.as_ref(), req_slice.as_ref())
}
}
pub struct BytesIncludesComparator {
negated: bool,
}
impl BytesIncludesComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<HttpMockBytes, HttpMockBytes> for BytesIncludesComparator {
fn matches(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> bool {
comparison::bytes_includes(self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "excludes";
}
return "includes";
}
fn distance(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let mock_slice = mock_value
.as_ref()
.map(|mv| mv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
let req_slice = req_value
.as_ref()
.map(|rv| rv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
distance_for(mock_slice.as_ref(), req_slice.as_ref())
}
}
pub struct BytesPrefixComparator {
negated: bool,
}
impl BytesPrefixComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<HttpMockBytes, HttpMockBytes> for BytesPrefixComparator {
fn matches(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> bool {
comparison::bytes_prefix(self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "prefix not";
}
"has prefix"
}
fn distance(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let mock_slice = mock_value
.as_ref()
.map(|mv| mv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
let req_slice = req_value
.as_ref()
.map(|rv| rv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
if mock_value.is_none() || mock_slice.is_empty() {
return 0;
}
if req_value.is_none() || req_slice.is_empty() {
return mock_slice.len();
}
let compared_window = std::cmp::min(mock_slice.len(), req_slice.len());
let distance = equal_weight_distance_for(
&mock_slice[..compared_window],
&req_slice[..compared_window],
);
if self.negated {
return compared_window - distance;
}
return distance;
}
}
pub struct BytesSuffixComparator {
negated: bool,
}
impl BytesSuffixComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<HttpMockBytes, HttpMockBytes> for BytesSuffixComparator {
fn matches(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> bool {
comparison::bytes_suffix(self.negated, &mock_value, &req_value)
}
fn name(&self) -> &str {
if self.negated {
return "suffix not";
}
return "has suffix";
}
fn distance(
&self,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let mock_slice = mock_value
.as_ref()
.map(|mv| mv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
let req_slice = req_value
.as_ref()
.map(|rv| rv.to_bytes().clone())
.unwrap_or_else(|| Bytes::new());
if mock_value.is_none() || mock_slice.is_empty() {
return 0;
}
if req_value.is_none() || req_slice.is_empty() {
return mock_slice.len();
}
let compared_window = std::cmp::min(mock_slice.len(), req_slice.len());
let distance = equal_weight_distance_for(
&mock_slice[..compared_window],
&req_slice[req_slice.len() - compared_window..],
);
if self.negated {
return compared_window - distance;
}
return distance;
}
}
pub struct AnyValueComparator {}
impl AnyValueComparator {
pub fn new() -> Self {
Self {}
}
}
impl<T, U> ValueComparator<T, U> for AnyValueComparator {
fn matches(&self, _: &Option<&T>, _: &Option<&U>) -> bool {
true
}
fn name(&self) -> &str {
"any"
}
fn distance(&self, _: &Option<&T>, _: &Option<&U>) -> usize {
0
}
}
pub struct FunctionMatchesRequestComparator {
negated: bool,
}
impl FunctionMatchesRequestComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<Arc<dyn Fn(&HttpMockRequest) -> bool + 'static + Sync + Send>, HttpMockRequest>
for FunctionMatchesRequestComparator
{
fn matches(
&self,
mock_value: &Option<&Arc<dyn Fn(&HttpMockRequest) -> bool + 'static + Sync + Send>>,
req_value: &Option<&HttpMockRequest>,
) -> bool {
let result = match (mock_value, req_value) {
(None, _) => true,
(Some(_), None) => self.negated,
(Some(mv), Some(rv)) => mv(rv),
};
if self.negated {
!result
} else {
result
}
}
fn name(&self) -> &str {
"matches"
}
fn distance(
&self,
mock_value: &Option<&Arc<dyn Fn(&HttpMockRequest) -> bool + 'static + Sync + Send>>,
req_value: &Option<&HttpMockRequest>,
) -> usize {
let result = match self.matches(mock_value, req_value) {
true => 0,
false => 1,
};
if self.negated {
return match result {
0 => 1,
_ => 0,
};
}
return result;
}
}
#[cfg(test)]
mod test {
use crate::{
common::data::HttpMockRegex,
server::matchers::comparators::{
AnyValueComparator, JSONContainsMatchComparator, JSONExactMatchComparator,
StringContainsComparator, StringEqualsComparator, StringRegexMatchComparator,
ValueComparator,
},
};
use regex::Regex;
use serde_json::json;
fn run_test<S, T>(
comparator: &dyn ValueComparator<S, T>,
v1: &S,
v2: &T,
expected_match: bool,
expected_distance: usize,
expected_name: &str,
) {
let match_result = comparator.matches(&Some(&v1), &Some(&v2));
let distance_result = comparator.distance(&Some(&v1), &Some(&v2));
let name_result = comparator.name();
assert_eq!(match_result, expected_match);
assert_eq!(distance_result, expected_distance);
assert_eq!(name_result, expected_name);
}
#[test]
fn json_exact_match_comparator_match() {
run_test(
&JSONExactMatchComparator::new(),
&json!({"name" : "Peter", "surname" : "Griffin"}),
&json!({"name" : "Peter", "surname" : "Griffin"}),
true,
0,
"equals",
);
}
#[test]
fn json_exact_match_comparator_no_match() {
run_test(
&JSONExactMatchComparator::new(),
&json!({"name" : "Peter", "surname" : "Griffin"}),
&json!({"name" : "Walter", "surname" : "White"}),
false,
9,
"equals",
);
}
#[test]
fn json_contains_comparator_match() {
run_test(
&JSONContainsMatchComparator::new(false),
&json!({ "other" : { "human" : { "surname" : "Griffin" }}}),
&json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}),
true,
15, "includes",
);
}
#[test]
fn json_contains_comparator_no_match() {
run_test(
&JSONContainsMatchComparator::new(false),
&json!({ "surname" : "Griffin" }),
&json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}),
false,
35, "includes",
);
}
#[test]
fn string_exact_comparator_match() {
run_test(
&StringEqualsComparator::new(true, false),
&"test string".to_string(),
&"test string".to_string(),
true,
0, "equals",
);
}
#[test]
fn string_exact_comparator_no_match() {
run_test(
&StringEqualsComparator::new(true, false),
&"test string".to_string(),
&"not a test string".to_string(),
false,
6, "equals",
);
}
#[test]
fn string_exact_comparator_case_sensitive_match() {
run_test(
&StringEqualsComparator::new(false, false),
&"TEST string".to_string(),
&"test STRING".to_string(),
true,
0,
"equals",
);
}
#[test]
fn string_contains_comparator_match() {
run_test(
&StringContainsComparator::new(true, false),
&"st st".to_string(),
&"test string".to_string(),
true,
6, "includes",
);
}
#[test]
fn string_contains_comparator_no_match() {
run_test(
&StringContainsComparator::new(true, false),
&"xxx".to_string(),
&"yyy".to_string(),
false,
3, "includes",
);
}
#[test]
fn string_contains_comparator_case_sensitive_match() {
run_test(
&StringContainsComparator::new(false, false),
&"ST st".to_string(),
&"test STRING".to_string(),
true,
6,
"includes",
);
}
#[test]
fn regex_comparator_match() {
run_test(
&StringRegexMatchComparator::new(),
&HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()),
&"2014-01-01".to_string(),
true,
0, "matches regex",
);
}
#[test]
fn regex_comparator_no_match() {
run_test(
&StringRegexMatchComparator::new(),
&HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()),
&"xxx".to_string(),
false,
3,
"matches regex",
);
}
#[test]
fn any_comparator_match() {
run_test(
&AnyValueComparator::new(),
&"00000000".to_string(),
&"xxx".to_string(),
true,
0, "any",
);
}
}