use std::fmt;
use std::marker::PhantomData;
use crate::core::{
strings, style, Color, Format, Formatter, MatchFailure, Matcher, OutputStyle, TextColor,
TextStyle,
};
use crate::matchers::diff::{Diff, DiffKind, DiffTag, Diffable, EqDiffMatcher};
const FORMAT_PLACEHOLDER: &str = "%s";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffSegmentStyle<T> {
pub insert: T,
pub delete: T,
pub equal: T,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct StringDiffStyle {
pub style: DiffSegmentStyle<OutputStyle>,
pub format: DiffSegmentStyle<String>,
}
impl StringDiffStyle {
pub fn provided() -> Self {
Self {
style: DiffSegmentStyle {
insert: OutputStyle {
style: TextStyle::BOLD | TextStyle::REVERSED,
color: TextColor {
fg: Some(Color::BrightGreen),
bg: None,
},
},
delete: OutputStyle {
style: TextStyle::BOLD | TextStyle::UNDERLINE,
color: TextColor {
fg: Some(Color::BrightRed),
bg: None,
},
},
equal: OutputStyle::default(),
},
format: DiffSegmentStyle {
insert: String::from("%s"),
delete: String::from("%s"),
equal: String::from("%s"),
},
}
}
}
impl Default for StringDiffStyle {
fn default() -> Self {
Self {
style: DiffSegmentStyle {
insert: OutputStyle::default(),
delete: OutputStyle::default(),
equal: OutputStyle::default(),
},
format: DiffSegmentStyle {
insert: String::from("%s"),
delete: String::from("%s"),
equal: String::from("%s"),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CollectionDiffStyle {
pub element_style: DiffSegmentStyle<OutputStyle>,
pub gutter_char: DiffSegmentStyle<char>,
pub gutter_style: DiffSegmentStyle<OutputStyle>,
}
impl CollectionDiffStyle {
pub fn provided() -> Self {
Self {
element_style: DiffSegmentStyle {
insert: OutputStyle {
style: TextStyle::BOLD,
color: TextColor {
fg: Some(Color::BrightGreen),
bg: None,
},
},
delete: OutputStyle {
style: TextStyle::BOLD,
color: TextColor {
fg: Some(Color::BrightRed),
bg: None,
},
},
equal: OutputStyle::default(),
},
gutter_char: DiffSegmentStyle {
insert: '+',
delete: '-',
equal: ' ',
},
gutter_style: DiffSegmentStyle {
insert: OutputStyle {
style: TextStyle::empty(),
color: TextColor {
fg: Some(Color::BrightGreen),
bg: None,
},
},
delete: OutputStyle {
style: TextStyle::empty(),
color: TextColor {
fg: Some(Color::BrightRed),
bg: None,
},
},
equal: OutputStyle::default(),
},
}
}
}
impl Default for CollectionDiffStyle {
fn default() -> Self {
Self {
element_style: DiffSegmentStyle {
insert: OutputStyle::default(),
delete: OutputStyle::default(),
equal: OutputStyle::default(),
},
gutter_char: DiffSegmentStyle {
insert: ' ',
delete: ' ',
equal: ' ',
},
gutter_style: DiffSegmentStyle {
insert: OutputStyle::default(),
delete: OutputStyle::default(),
equal: OutputStyle::default(),
},
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DiffStyle {
pub string: StringDiffStyle,
pub collection: CollectionDiffStyle,
}
impl DiffStyle {
pub fn provided() -> Self {
Self {
string: StringDiffStyle::provided(),
collection: CollectionDiffStyle::provided(),
}
}
}
#[derive(Debug)]
pub struct DiffFormat<Actual, Expected> {
style: DiffStyle,
marker: PhantomData<(Actual, Expected)>,
}
impl<Actual, Expected> DiffFormat<Actual, Expected> {
pub fn new(style: DiffStyle) -> Self {
Self {
style,
marker: PhantomData,
}
}
}
impl<Actual, Expected> Format for DiffFormat<Actual, Expected>
where
Actual: fmt::Debug,
Expected: Diffable<Actual> + fmt::Debug,
{
type Value = MatchFailure<Diff>;
fn fmt(&self, f: &mut Formatter, value: Self::Value) -> crate::Result<()> {
let diff = value.unwrap();
f.set_style(style::important());
if value.is_pos() {
f.write_str("Expected these to be equal:\n");
} else {
f.write_str("Expected these to not be equal:\n");
}
f.reset_style();
match Expected::KIND {
DiffKind::String => {
f.indented(style::indent(1), |f| {
for segment in diff {
let (format, style) = match segment.tag {
DiffTag::Insert => (
self.style.string.format.insert.clone(),
self.style.string.style.insert.clone(),
),
DiffTag::Delete => (
self.style.string.format.delete.clone(),
self.style.string.style.delete.clone(),
),
DiffTag::Equal => (
self.style.string.format.equal.clone(),
self.style.string.style.equal.clone(),
),
};
let formatted_segment =
format.replacen(FORMAT_PLACEHOLDER, &segment.value, 1);
f.set_style(style);
f.write_str(&formatted_segment);
}
Ok(())
})?;
Ok(())
}
DiffKind::Slice | DiffKind::Set | DiffKind::Map => {
f.indented(style::indent(1), |f| {
match Expected::KIND {
DiffKind::Slice => f.write_char('['),
DiffKind::Set | DiffKind::Map => f.write_char('{'),
_ => unreachable!(),
};
f.write_char('\n');
for segment in diff {
let (gutter, gutter_style, element_style) = match segment.tag {
DiffTag::Insert => (
self.style.collection.gutter_char.insert,
self.style.collection.gutter_style.insert.clone(),
self.style.collection.element_style.insert.clone(),
),
DiffTag::Delete => (
self.style.collection.gutter_char.delete,
self.style.collection.gutter_style.delete.clone(),
self.style.collection.element_style.delete.clone(),
),
DiffTag::Equal => (
self.style.collection.gutter_char.equal,
self.style.collection.gutter_style.equal.clone(),
self.style.collection.element_style.equal.clone(),
),
};
f.set_style(gutter_style);
f.write_char(' ');
f.write_char(gutter);
f.reset_style();
f.write_str(strings::whitespace(style::indent_len(1) as usize - 2));
f.indented_hanging(style::indent(1), |f| {
f.set_style(element_style);
f.write_str(&segment.value);
f.reset_style();
Ok(())
})?;
f.write_char(',');
f.write_char('\n');
}
match Expected::KIND {
DiffKind::Slice => f.write_char(']'),
DiffKind::Set | DiffKind::Map => f.write_char('}'),
_ => unreachable!(),
};
Ok(())
})?;
Ok(())
}
DiffKind::Custom(name) => Err(crate::Error::msg(format!(
"this is not a supported diffable kind: {name}",
))),
}
}
}
pub fn eq_diff<'a, Actual, Expected>(expected: Expected) -> Matcher<'a, Actual, Actual>
where
Actual: fmt::Debug + PartialEq<Expected> + Eq + 'a,
Expected: fmt::Debug + Diffable<Actual> + 'a,
{
Matcher::new(
EqDiffMatcher::new(expected),
DiffFormat::<Actual, Expected>::new(DiffStyle::provided()),
)
}
#[cfg(test)]
mod tests {
use super::eq_diff;
use crate::expect;
#[test]
fn succeeds_when_equal() {
expect!("some string").to(eq_diff("some string"));
expect!(["some", "slice"]).to(eq_diff(["some", "slice"]));
}
#[test]
fn succeeds_when_not_equal() {
expect!("some string").to_not(eq_diff("a different string"));
expect!(["some", "slice"]).to_not(eq_diff(["different", "slice"]));
}
#[test]
#[should_panic]
fn fails_when_equal() {
expect!("some string").to_not(eq_diff("some string"));
expect!(["some", "slice"]).to_not(eq_diff(["some", "slice"]));
}
#[test]
#[should_panic]
fn fails_when_not_equal() {
expect!("some string").to(eq_diff("a different string"));
expect!(["some", "slice"]).to(eq_diff(["different", "slice"]));
}
}