pub mod path;
pub mod serialize;
pub mod value;
mod macros;
use crate::assertions::{AssertEquality, AssertEquivalence};
use crate::colored::mark_diff_str;
use crate::recursive_comparison::path::Path;
use crate::recursive_comparison::serialize::to_recursive_value;
use crate::recursive_comparison::value::Value;
use crate::spec::{
AssertFailure, CollectFailures, DiffFormat, DoFail, FailingStrategy, GetFailures, SoftPanic,
Spec,
};
use crate::std::fmt::{self, Display};
use crate::std::string::{String, ToString};
use crate::std::vec::Vec;
use crate::std::{format, vec};
use serde_core::Serialize;
pub struct RecursiveComparison<'a, S, R> {
spec: Spec<'a, S, R>,
compared_fields: Vec<Path<'a>>,
ignored_fields: Vec<Path<'a>>,
ignore_not_expected_fields: bool,
}
impl<S, R> GetFailures for RecursiveComparison<'_, S, R> {
fn has_failures(&self) -> bool {
self.spec.has_failures()
}
fn failures(&self) -> Vec<AssertFailure> {
self.spec.failures()
}
fn display_failures(&self) -> Vec<String> {
self.spec.display_failures()
}
}
impl<S, R> DoFail for RecursiveComparison<'_, S, R>
where
R: FailingStrategy,
{
fn do_fail_with(&mut self, failures: impl IntoIterator<Item = AssertFailure>) {
self.spec.do_fail_with(failures);
}
fn do_fail_with_message(&mut self, message: impl Into<String>) {
self.spec.do_fail_with_message(message);
}
}
impl<S> SoftPanic for RecursiveComparison<'_, S, CollectFailures> {
fn soft_panic(&self) {
self.spec.soft_panic();
}
}
impl<'a, S, R> RecursiveComparison<'a, S, R> {
pub(crate) fn new(spec: Spec<'a, S, R>) -> Self {
Self {
spec,
compared_fields: vec![],
ignored_fields: vec![],
ignore_not_expected_fields: false,
}
}
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn comparing_only_field(mut self, field_path: impl Into<Path<'a>>) -> Self {
self.compared_fields.push(field_path.into());
self
}
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn comparing_only_fields<P>(
mut self,
list_of_field_path: impl IntoIterator<Item = P>,
) -> Self
where
P: Into<Path<'a>>,
{
self.compared_fields
.extend(list_of_field_path.into_iter().map(Into::into));
self
}
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn ignoring_field(mut self, field_path: impl Into<Path<'a>>) -> Self {
self.ignored_fields.push(field_path.into());
self
}
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn ignoring_fields<P>(mut self, list_of_field_path: impl IntoIterator<Item = P>) -> Self
where
P: Into<Path<'a>>,
{
self.ignored_fields
.extend(list_of_field_path.into_iter().map(Into::into));
self
}
#[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"]
pub fn ignoring_not_expected_fields(mut self) -> Self {
self.ignore_not_expected_fields = true;
self
}
fn compare<'b>(&self, actual: &'b Value, expected: &'b Value) -> ComparisonResult<'b> {
let mut ignored = Vec::new();
let mut not_expected = Vec::new();
let mut non_equal = Vec::new();
for (actual_path, actual_value) in actual.depth_first_iter() {
if self
.ignored_fields
.iter()
.any(|ignored| actual_path.starts_with(ignored))
|| (!self.compared_fields.is_empty()
&& !self
.compared_fields
.iter()
.any(|compared| actual_path.starts_with(compared)))
{
ignored.push(actual_path);
continue;
}
if let Some(expected_value) = expected.get_path(&actual_path) {
if actual_value != expected_value {
non_equal.push(NonEqual {
path: actual_path,
actual_value,
expected_value,
});
}
} else if self.ignore_not_expected_fields {
ignored.push(actual_path);
} else {
not_expected.push(NotExpected {
path: actual_path,
value: actual_value,
});
}
}
ComparisonResult {
ignored,
non_equal,
not_expected,
}
}
}
struct NotExpected<'a> {
path: Path<'a>,
value: &'a Value,
}
impl Display for NotExpected<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let NotExpected { path, value } = self;
write!(f, "{path}: {value:?}")
}
}
struct NonEqual<'a> {
path: Path<'a>,
actual_value: &'a Value,
expected_value: &'a Value,
}
struct ComparisonResult<'a> {
ignored: Vec<Path<'a>>,
non_equal: Vec<NonEqual<'a>>,
not_expected: Vec<NotExpected<'a>>,
}
impl ComparisonResult<'_> {
fn has_failure(&self) -> bool {
!self.non_equal.is_empty() || !self.not_expected.is_empty()
}
}
fn display_non_equal(non_equal: &NonEqual<'_>, diff_format: &DiffFormat) -> String {
let NonEqual {
path,
actual_value,
expected_value,
} = non_equal;
let mut display_details = String::new();
let debug_actual = format!("{actual_value:?}");
let debug_expected = format!("{expected_value:?}");
display_details.push_str(&path.to_string());
if debug_actual == debug_expected {
let (marked_actual_type, marked_expected_type) = mark_diff_str(
&actual_value.type_name(),
&expected_value.type_name(),
diff_format,
);
display_details.push_str(": value <");
display_details.push_str(&debug_actual);
display_details.push_str("> was equal, but type was <");
display_details.push_str(&marked_actual_type);
display_details.push_str("> and expected type is <");
display_details.push_str(&marked_expected_type);
} else {
let (marked_actual, marked_expected) =
mark_diff_str(&debug_actual, &debug_expected, diff_format);
display_details.push_str(": expected <");
display_details.push_str(&marked_expected);
display_details.push_str("> but was <");
display_details.push_str(&marked_actual);
}
display_details.push('>');
display_details
}
fn display_compare_details(compared: &ComparisonResult<'_>, diff_format: &DiffFormat) -> String {
let mut display_details = String::new();
if !compared.non_equal.is_empty() {
display_details.push_str("\n non equal fields:\n");
for a_non_equal in &compared.non_equal {
display_details.push_str(" ");
display_details.push_str(&display_non_equal(a_non_equal, diff_format));
display_details.push('\n');
}
}
if !compared.not_expected.is_empty() {
display_details.push_str("\n the following fields were not expected:\n");
for a_not_expected in &compared.not_expected {
display_details.push_str(" ");
display_details.push_str(&a_not_expected.to_string());
display_details.push('\n');
}
}
if !compared.ignored.is_empty() {
display_details.push_str("\n the following fields were ignored:\n");
for an_ignored in &compared.ignored {
display_details.push_str(" ");
display_details.push_str(&an_ignored.to_string());
display_details.push('\n');
}
}
display_details
}
impl<S, E, R> AssertEquality<E> for RecursiveComparison<'_, S, R>
where
S: Serialize,
E: Serialize,
R: FailingStrategy,
{
fn is_equal_to(mut self, expected: E) -> Self {
let expression = self.spec.expression();
let actual = to_recursive_value(self.spec.subject())
.unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}"));
let expected = to_recursive_value(&expected)
.unwrap_or_else(|err| panic!("failed to serialize the expected value, reason: {err}"));
let compared = self.compare(&actual, &expected);
if compared.has_failure() {
let compare_details = display_compare_details(&compared, self.spec.diff_format());
self.do_fail_with_message(format!(
r"expected {expression} to be equal to {expected:?} (using recursive comparison)
but was: {actual:?}
expected: {expected:?}
{compare_details}"
));
}
self
}
fn is_not_equal_to(mut self, expected: E) -> Self {
let expression = self.spec.expression();
let actual = to_recursive_value(self.spec.subject())
.unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}"));
let expected = to_recursive_value(&expected)
.unwrap_or_else(|err| panic!("failed to serialize the expected value, reason: {err}"));
let compared = self.compare(&actual, &expected);
if !compared.has_failure() {
let compare_details = display_compare_details(&compared, self.spec.diff_format());
self.do_fail_with_message(format!(
r"expected {expression} to be not equal to {expected:?} (using recursive comparison)
but was: {actual:?}
expected: {expected:?}
{compare_details}"
));
}
self
}
}
impl<S, R> AssertEquivalence<Value> for RecursiveComparison<'_, S, R>
where
S: Serialize,
R: FailingStrategy,
{
fn is_equivalent_to(mut self, expected: Value) -> Self {
let expression = self.spec.expression();
let actual = to_recursive_value(self.spec.subject())
.unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}"));
let compared = self.compare(&actual, &expected);
if compared.has_failure() {
let compare_details = display_compare_details(&compared, self.spec.diff_format());
self.do_fail_with_message(format!(
r"expected {expression} to be equivalent to {expected:?} (using recursive comparison)
but was: {actual:?}
expected: {expected:?}
{compare_details}"
));
}
self
}
fn is_not_equivalent_to(mut self, expected: Value) -> Self {
let expression = self.spec.expression();
let actual = to_recursive_value(self.spec.subject())
.unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}"));
self.ignore_not_expected_fields = true;
let compared = self.compare(&actual, &expected);
if !compared.has_failure() {
let compare_details = display_compare_details(&compared, self.spec.diff_format());
self.do_fail_with_message(format!(
r"expected {expression} to be not equivalent to {expected:?} (using recursive comparison)
but was: {actual:?}
expected: {expected:?}
{compare_details}"
));
}
self
}
}
#[cfg(test)]
mod tests;