use std::{borrow::Cow, sync::Arc};
use assert_json_diff::{CompareMode, Config, assert_json_matches_no_panic};
use serde_json::Value;
use crate::{
common::{
data::{HttpMockRegex, HttpMockRequest},
util::HttpMockBytes,
},
server::matchers::{
comparison,
comparison::{
distance_for, distance_for_prefix, distance_for_substring, distance_for_suffix, hostname_equals,
regex_unmatched_length, string_distance, string_equals, string_has_prefix, string_has_suffix,
string_includes,
},
},
};
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;
}
#[derive(Default)]
pub struct JsonEqualsComparator {}
impl JsonEqualsComparator {
pub fn new() -> Self {
Self::default()
}
}
impl ValueComparator<Value, Value> for JsonEqualsComparator {
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 JsonIncludesComparator {
pub negated: bool,
}
impl JsonIncludesComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<Value, Value> for JsonIncludesComparator {
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 { "excludes" } else { "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 = 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 { "not equal to" } else { "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 { "not equal to" } else { "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 StringIncludesComparator {
case_sensitive: bool,
negated: bool,
}
impl StringIncludesComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringIncludesComparator {
fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool {
string_includes(self.case_sensitive, self.negated, mock_value, req_value)
}
fn name(&self) -> &str {
if self.negated { "excludes" } else { "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 = StringIncludesComparator {
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 = StringIncludesComparator {
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 = StringIncludesComparator {
case_sensitive: true,
negated: false,
};
assert_eq!(comparator.distance(&mock_value, &req_value), 0); }
}
pub struct StringPrefixComparator {
case_sensitive: bool,
negated: bool,
}
impl StringPrefixComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringPrefixComparator {
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 { "prefix not" } else { "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 StringSuffixComparator {
case_sensitive: bool,
negated: bool,
}
impl StringSuffixComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<String, String> for StringSuffixComparator {
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 { "suffix not" } else { "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 StringMatchesComparator {
case_sensitive: bool,
negated: bool,
}
impl StringMatchesComparator {
pub fn new(case_sensitive: bool, negated: bool) -> Self {
Self {
case_sensitive,
negated,
}
}
}
impl ValueComparator<HttpMockRegex, String> for StringMatchesComparator {
fn matches(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> bool {
comparison::string_matches_regex(self.case_sensitive, self.negated, mock_value, req_value)
}
fn name(&self) -> &str {
if self.negated {
"does not match regex"
} else {
"matches regex"
}
}
fn distance(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> usize {
comparison::regex_string_distance(self.case_sensitive, self.negated, mock_value, req_value)
}
}
#[derive(Default)]
pub struct BytesMatchesComparator {}
impl BytesMatchesComparator {
pub fn new() -> Self {
Self::default()
}
}
impl ValueComparator<HttpMockRegex, HttpMockBytes> for BytesMatchesComparator {
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 U16EqualsComparator {
negated: bool,
}
impl U16EqualsComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<u16, u16> for U16EqualsComparator {
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 { "not equal to" } else { "equals" }
}
fn distance(&self, mock_value: &Option<&u16>, req_value: &Option<&u16>) -> usize {
comparison::distance_for_usize(mock_value, req_value)
}
}
pub struct BytesEqualsComparator {
negated: bool,
}
impl BytesEqualsComparator {
pub fn new(negated: bool) -> Self {
Self { negated }
}
}
impl ValueComparator<HttpMockBytes, HttpMockBytes> for BytesEqualsComparator {
fn matches(&self, mock_value: &Option<&HttpMockBytes>, req_value: &Option<&HttpMockBytes>) -> bool {
comparison::bytes_equals(self.negated, mock_value, req_value)
}
fn name(&self) -> &str {
if self.negated { "not equal to" } else { "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()).unwrap_or_default();
let req_slice = req_value.as_ref().map(|rv| rv.to_bytes()).unwrap_or_default();
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 { "excludes" } else { "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()).unwrap_or_default();
let req_slice = req_value.as_ref().map(|rv| rv.to_bytes()).unwrap_or_default();
distance_for(mock_slice.as_ref(), req_slice.as_ref())
}
}
fn bytes_affix_distance(
negated: bool,
from_end: bool,
mock_value: &Option<&HttpMockBytes>,
req_value: &Option<&HttpMockBytes>,
) -> usize {
let mock_slice = mock_value.as_ref().map(|mv| mv.to_bytes()).unwrap_or_default();
let req_slice = req_value.as_ref().map(|rv| rv.to_bytes()).unwrap_or_default();
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 req_window = if from_end {
&req_slice[req_slice.len() - compared_window..]
} else {
&req_slice[..compared_window]
};
let distance = distance_for(&mock_slice[..compared_window], req_window);
if negated { compared_window - distance } else { distance }
}
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_has_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 {
bytes_affix_distance(self.negated, false, mock_value, req_value)
}
}
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_has_suffix(self.negated, mock_value, req_value)
}
fn name(&self) -> &str {
if self.negated { "suffix not" } else { "has suffix" }
}
fn distance(&self, mock_value: &Option<&HttpMockBytes>, req_value: &Option<&HttpMockBytes>) -> usize {
bytes_affix_distance(self.negated, true, mock_value, req_value)
}
}
#[derive(Default)]
pub struct AnyValueComparator {}
impl AnyValueComparator {
pub fn new() -> Self {
Self::default()
}
}
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,
};
}
result
}
}
#[cfg(test)]
mod test {
use regex::Regex;
use serde_json::json;
use crate::{
common::data::HttpMockRegex,
server::matchers::comparators::{
AnyValueComparator, JsonEqualsComparator, JsonIncludesComparator, StringEqualsComparator,
StringIncludesComparator, StringMatchesComparator, ValueComparator,
},
};
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_equals_match() {
run_test(
&JsonEqualsComparator::new(),
&json!({"name" : "Peter", "surname" : "Griffin"}),
&json!({"name" : "Peter", "surname" : "Griffin"}),
true,
0,
"equals",
);
}
#[test]
fn json_equals_no_match() {
run_test(
&JsonEqualsComparator::new(),
&json!({"name" : "Peter", "surname" : "Griffin"}),
&json!({"name" : "Walter", "surname" : "White"}),
false,
9,
"equals",
);
}
#[test]
fn json_includes_match() {
run_test(
&JsonIncludesComparator::new(false),
&json!({ "other" : { "human" : { "surname" : "Griffin" }}}),
&json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}),
true,
15, "includes",
);
}
#[test]
fn json_includes_no_match() {
run_test(
&JsonIncludesComparator::new(false),
&json!({ "surname" : "Griffin" }),
&json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}),
false,
35, "includes",
);
}
#[test]
fn string_equals_match() {
run_test(
&StringEqualsComparator::new(true, false),
&"test string".to_string(),
&"test string".to_string(),
true,
0, "equals",
);
}
#[test]
fn string_equals_no_match() {
run_test(
&StringEqualsComparator::new(true, false),
&"test string".to_string(),
&"not a test string".to_string(),
false,
6, "equals",
);
}
#[test]
fn string_equals_case_insensitive_match() {
run_test(
&StringEqualsComparator::new(false, false),
&"TEST string".to_string(),
&"test STRING".to_string(),
true,
0,
"equals",
);
}
#[test]
fn string_includes_match() {
run_test(
&StringIncludesComparator::new(true, false),
&"st st".to_string(),
&"test string".to_string(),
true,
6, "includes",
);
}
#[test]
fn string_includes_no_match() {
run_test(
&StringIncludesComparator::new(true, false),
&"xxx".to_string(),
&"yyy".to_string(),
false,
3, "includes",
);
}
#[test]
fn string_includes_case_insensitive_match() {
run_test(
&StringIncludesComparator::new(false, false),
&"ST st".to_string(),
&"test STRING".to_string(),
true,
6,
"includes",
);
}
#[test]
fn regex_match() {
run_test(
&StringMatchesComparator::new(true, false),
&HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()),
&"2014-01-01".to_string(),
true,
0, "matches regex",
);
}
#[test]
fn regex_no_match() {
run_test(
&StringMatchesComparator::new(true, false),
&HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()),
&"xxx".to_string(),
false,
3,
"matches regex",
);
}
#[test]
fn any_value_match() {
run_test(
&AnyValueComparator::new(),
&"00000000".to_string(),
&"xxx".to_string(),
true,
0, "any",
);
}
}