Skip to content

Commit

Permalink
refactor: Replace Mismatch by CommonMismatch
Browse files Browse the repository at this point in the history
  • Loading branch information
tienvx committed Feb 27, 2024
1 parent 84dfc19 commit b2e4356
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 108 deletions.
71 changes: 35 additions & 36 deletions rust/pact_matching/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ use pact_models::path_exp::DocPath;
#[cfg(feature = "datetime")] use pact_models::time_utils::validate_datetime;
use tracing::debug;

use crate::{DiffConfig, MatchingContext, merge_result};
use crate::{DiffConfig, MatchingContext, Mismatch, CommonMismatch, merge_result};
use crate::binary_utils::{convert_data, match_content_type};
use crate::matchers::*;
use crate::matchingrules::{compare_lists_with_matchingrule, compare_maps_with_matchingrule};

use super::Mismatch;

lazy_static! {
static ref DEC_REGEX: Regex = Regex::new(r"\d+\.\d+").unwrap();
}
Expand Down Expand Up @@ -331,6 +329,7 @@ pub fn match_json(
Err(mismatches.clone())
} else {
compare_json(&DocPath::root(), &expected_json.unwrap(), &actual_json.unwrap(), context)
.map_err(|mismatches| mismatches.iter().map(|mismatch| mismatch.to_body_mismatch()).collect())
}
}

Expand Down Expand Up @@ -396,26 +395,26 @@ pub fn compare_json(
expected: &Value,
actual: &Value,
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
debug!("compare: Comparing path {}", path);
match (expected, actual) {
(&Value::Object(ref emap), &Value::Object(ref amap)) => compare_maps(path, emap, amap, context),
(&Value::Object(_), _) => {
Err(vec![ Mismatch::BodyMismatch {
Err(vec![ CommonMismatch {
path: path.to_string(),
expected: Some(json_to_string(expected).into()),
actual: Some(json_to_string(actual).into()),
mismatch: format!("Type mismatch: Expected {} ({}) to be the same type as {} ({})",
expected: json_to_string(expected),
actual: json_to_string(actual),
description: format!("Type mismatch: Expected {} ({}) to be the same type as {} ({})",
value_of(actual), type_of(actual), value_of(expected), type_of(expected))
} ])
}
(&Value::Array(ref elist), &Value::Array(ref alist)) => compare_lists(path, elist, alist, context),
(&Value::Array(_), _) => {
Err(vec![ Mismatch::BodyMismatch {
Err(vec![ CommonMismatch {
path: path.to_string(),
expected: Some(json_to_string(expected).into()),
actual: Some(json_to_string(actual).into()),
mismatch: format!("Type mismatch: Expected {} ({}) to be the same type as {} ({})",
expected: json_to_string(expected),
actual: json_to_string(actual),
description: format!("Type mismatch: Expected {} ({}) to be the same type as {} ({})",
value_of(actual), type_of(actual), value_of(expected), type_of(expected)),
} ])
}
Expand All @@ -428,16 +427,16 @@ fn compare_maps(
expected: &serde_json::Map<String, Value>,
actual: &serde_json::Map<String, Value>,
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
let spath = path.to_string();
debug!("compare_maps: Comparing maps at {}: {:?} -> {:?}", spath, expected, actual);
if expected.is_empty() && context.config() == DiffConfig::NoUnexpectedKeys && !actual.is_empty() {
debug!("compare_maps: Expected map is empty, but actual is not");
Err(vec![ Mismatch::BodyMismatch {
Err(vec![ CommonMismatch {
path: spath,
expected: Some(json_to_string(&json!(expected)).into()),
actual: Some(json_to_string(&json!(actual)).into()),
mismatch: format!("Expected an empty Map but received {}", json_to_string(&json!(actual))),
expected: json_to_string(&json!(expected)),
actual: json_to_string(&json!(actual)),
description: format!("Expected an empty Map but received {}", json_to_string(&json!(actual))),
} ])
} else {
let mut result = Ok(());
Expand Down Expand Up @@ -473,7 +472,7 @@ fn compare_lists(
expected: &[Value],
actual: &[Value],
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
let spath = path.to_string();
if context.matcher_is_defined(path) {
debug!("compare_lists: matcher defined for path '{}'", path);
Expand All @@ -487,20 +486,20 @@ fn compare_lists(
}
result
} else if expected.is_empty() && !actual.is_empty() {
Err(vec![ Mismatch::BodyMismatch {
Err(vec![ CommonMismatch {
path: spath,
expected: Some(json_to_string(&json!(expected)).into()),
actual: Some(json_to_string(&json!(actual)).into()),
mismatch: format!("Expected an empty List but received {}", json_to_string(&json!(actual))),
expected: json_to_string(&json!(expected)),
actual: json_to_string(&json!(actual)),
description: format!("Expected an empty List but received {}", json_to_string(&json!(actual))),
} ])
} else {
let result = compare_list_content(path, expected, actual, context);
if expected.len() != actual.len() {
merge_result(result, Err(vec![ Mismatch::BodyMismatch {
merge_result(result, Err(vec![ CommonMismatch {
path: spath,
expected: Some(json_to_string(&json!(expected)).into()),
actual: Some(json_to_string(&json!(actual)).into()),
mismatch: format!("Expected a List with {} elements but received {} elements",
expected: json_to_string(&json!(expected)),
actual: json_to_string(&json!(actual)),
description: format!("Expected a List with {} elements but received {} elements",
expected.len(), actual.len()),
} ]))
} else {
Expand All @@ -514,7 +513,7 @@ fn compare_list_content(
expected: &[Value],
actual: &[Value],
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
let mut result = Ok(());
for (index, value) in expected.iter().enumerate() {
let ps = index.to_string();
Expand All @@ -523,11 +522,11 @@ fn compare_list_content(
if index < actual.len() {
result = merge_result(result, compare_json(&p, value, &actual[index], context));
} else if !context.matcher_is_defined(&p) {
result = merge_result(result,Err(vec![ Mismatch::BodyMismatch {
result = merge_result(result,Err(vec![ CommonMismatch {
path: path.to_string(),
expected: Some(json_to_string(&json!(expected)).into()),
actual: Some(json_to_string(&json!(actual)).into()),
mismatch: format!("Expected {} but was missing", json_to_string(value)) } ]))
expected: json_to_string(&json!(expected)),
actual: json_to_string(&json!(actual)),
description: format!("Expected {} but was missing", json_to_string(value)) } ]))
}
}
result
Expand All @@ -538,7 +537,7 @@ fn compare_values(
expected: &Value,
actual: &Value,
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
let matcher_result = if context.matcher_is_defined(path) {
debug!("compare_values: Calling match_values for path {}", path);
match_values(path, &context.select_best_matcher(&path), expected, actual)
Expand All @@ -548,11 +547,11 @@ fn compare_values(
debug!("compare_values: Comparing '{:?}' to '{:?}' at path '{}' -> {:?}", expected, actual, path.to_string(), matcher_result);
matcher_result.map_err(|messages| {
messages.iter().map(|message| {
Mismatch::BodyMismatch {
CommonMismatch {
path: path.to_string(),
expected: Some(format!("{}", expected).into()),
actual: Some(format!("{}", actual).into()),
mismatch: message.clone()
expected: format!("{}", expected),
actual: format!("{}", actual),
description: message.clone()
}
}).collect()
})
Expand Down
93 changes: 73 additions & 20 deletions rust/pact_matching/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ pub trait MatchingContext: Debug {
fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool;

/// Matches the keys of the expected and actual maps
fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<Mismatch>>;
fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>>;

/// Returns the plugin configuration associated with the context
fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig>;
Expand Down Expand Up @@ -558,7 +558,7 @@ impl MatchingContext for CoreMatchingContext {
}
}

fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<Mismatch>> {
fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
let mut expected_keys = expected.iter().cloned().collect::<Vec<String>>();
expected_keys.sort();
let mut actual_keys = actual.iter().cloned().collect::<Vec<String>>();
Expand All @@ -569,19 +569,19 @@ impl MatchingContext for CoreMatchingContext {
if !self.direct_matcher_defined(path, &hashset! { "values", "each-value", "each-key" }) {
match self.config {
DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
result.push(Mismatch::BodyMismatch {
result.push(CommonMismatch {
path: path.to_string(),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
expected: expected.for_mismatch(),
actual: actual.for_mismatch(),
description: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
});
}
DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
result.push(Mismatch::BodyMismatch {
result.push(CommonMismatch {
path: path.to_string(),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Expected a Map with keys [{}] but received one with keys [{}]",
expected: expected.for_mismatch(),
actual: actual.for_mismatch(),
description: format!("Expected a Map with keys [{}] but received one with keys [{}]",
expected_keys.join(", "), actual_keys.join(", ")),
});
}
Expand All @@ -600,21 +600,21 @@ impl MatchingContext for CoreMatchingContext {
for key in &actual_keys {
let key_path = path.join(key);
if let Err(err) = String::default().matches_with(key, &rule, false) {
result.push(Mismatch::BodyMismatch {
result.push(CommonMismatch {
path: key_path.to_string(),
expected: Some("".to_string().into()),
actual: Some(key.clone().into()),
mismatch: err.to_string(),
expected: "".to_string(),
actual: key.clone(),
description: err.to_string(),
});
}
}
}
Either::Right(name) => {
result.push(Mismatch::BodyMismatch {
result.push(CommonMismatch {
path: path.to_string(),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Expected a matching rule, found an unresolved reference '{}'",
expected: expected.for_mismatch(),
actual: actual.for_mismatch(),
description: format!("Expected a matching rule, found an unresolved reference '{}'",
name.name),
});
}
Expand Down Expand Up @@ -703,7 +703,7 @@ impl MatchingContext for HeaderMatchingContext {
self.inner_context.direct_matcher_defined(path, matchers)
}

fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<Mismatch>> {
fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
self.inner_context.match_keys(path, expected, actual)
}

Expand Down Expand Up @@ -760,6 +760,59 @@ fn match_xml(
}
}

/// Store common mismatch information so it can be converted to different type of mismatches
#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
pub struct CommonMismatch {
path: String,
expected: String,
actual: String,
description: String
}

impl CommonMismatch {
/// Convert common mismatch to body mismatch
pub fn to_body_mismatch(&self) -> Mismatch {
Mismatch::BodyMismatch {
path: self.path.clone(),
expected: Some(self.expected.clone().into()),
actual: Some(self.actual.clone().into()),
mismatch: self.description.clone()
}
}

/// Convert common mismatch to query mismatch
pub fn to_query_mismatch(&self) -> Mismatch {
Mismatch::QueryMismatch {
parameter: self.path.clone(),
expected: self.expected.clone().into(),
actual: self.actual.clone().into(),
mismatch: self.description.clone()
}
}

/// Convert common mismatch to header mismatch
pub fn to_header_mismatch(&self) -> Mismatch {
Mismatch::HeaderMismatch {
key: self.path.clone(),
expected: self.expected.clone().into(),
actual: self.actual.clone().into(),
mismatch: self.description.clone()
}
}
}

impl Display for CommonMismatch {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}

impl PartialEq for CommonMismatch {
fn eq(&self, other: &CommonMismatch) -> bool {
self.path == other.path && self.expected == other.expected && self.actual == other.actual
}
}

/// Enum that defines the different types of mismatches that can occur.
#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
pub enum Mismatch {
Expand Down Expand Up @@ -1051,7 +1104,7 @@ impl Display for Mismatch {
}
}

fn merge_result(res1: Result<(), Vec<Mismatch>>, res2: Result<(), Vec<Mismatch>>) -> Result<(), Vec<Mismatch>> {
fn merge_result<T: Clone>(res1: Result<(), Vec<T>>, res2: Result<(), Vec<T>>) -> Result<(), Vec<T>> {
match (&res1, &res2) {
(Ok(_), Ok(_)) => res1.clone(),
(Err(_), Ok(_)) => res1.clone(),
Expand Down
12 changes: 6 additions & 6 deletions rust/pact_matching/src/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use semver::Version;
use tracing::{debug, instrument, trace};

use crate::binary_utils::match_content_type;
use crate::{MatchingContext, Mismatch};
use crate::{MatchingContext, CommonMismatch};

#[cfg(feature = "plugins")]
lazy_static! {
Expand Down Expand Up @@ -829,7 +829,7 @@ pub fn match_strings(
expected: &str,
actual: &str,
context: &dyn MatchingContext
) -> Result<(), Vec<Mismatch>> {
) -> Result<(), Vec<CommonMismatch>> {
let matcher_result = if context.matcher_is_defined(&path) {
debug!("Calling match_values for path {}", path);
match_values(&path, &context.select_best_matcher(&path), expected, actual)
Expand All @@ -841,11 +841,11 @@ pub fn match_strings(
debug!("Comparing '{:?}' to '{:?}' at path '{}' -> {:?}", expected, actual, path, matcher_result);
matcher_result.map_err(|messages| {
messages.iter().map(|message| {
Mismatch::BodyMismatch {
CommonMismatch {
path: path.to_string(),
expected: Some(Bytes::from(expected.as_bytes().to_vec())),
actual: Some(Bytes::from(actual.as_bytes().to_vec())),
mismatch: message.clone()
expected: expected.to_string(),
actual: actual.to_string(),
description: message.clone()
}
}).collect()
})
Expand Down
Loading

0 comments on commit b2e4356

Please sign in to comment.