use crate::domain::model::malformed_entry::MalformedEntry;
pub trait EntryDefectScanner {
fn scan(&self) -> anyhow::Result<Vec<MalformedEntry>>;
}
#[cfg(test)]
pub mod test_support {
use super::*;
use crate::domain::model::entry_locator::EntryLocator;
use crate::domain::model::entry_origin::EntryOrigin;
use crate::domain::model::load_defect::LoadDefect;
pub struct FakeEntryDefectScanner {
defects: Vec<MalformedEntry>,
}
impl FakeEntryDefectScanner {
pub fn empty() -> Self {
Self {
defects: Vec::new(),
}
}
pub fn with_defects(defects: Vec<MalformedEntry>) -> Self {
Self { defects }
}
}
impl EntryDefectScanner for FakeEntryDefectScanner {
fn scan(&self) -> anyhow::Result<Vec<MalformedEntry>> {
Ok(self.defects.clone())
}
}
pub fn check_entry_defect_scanner_contract<S, F>(make: F)
where
S: EntryDefectScanner,
F: Fn(Vec<MalformedEntry>) -> S,
{
let scanner = make(Vec::new());
let got = scanner.scan().expect("scan must not fail on empty corpus");
assert!(got.is_empty(), "empty corpus must yield no defects");
let entry = MalformedEntry {
location: EntryLocator::new("docs/adr/0001-broken/index.md"),
origin: EntryOrigin::Local,
defect: LoadDefect::MissingId,
};
let scanner = make(vec![entry.clone()]);
let got = scanner.scan().expect("scan must succeed");
assert_eq!(got, vec![entry]);
let entry = MalformedEntry {
location: EntryLocator::new("../shared/adr/0042-broken/index.md"),
origin: EntryOrigin::Union {
name: "../shared/adr".into(),
},
defect: LoadDefect::InvalidFrontmatter {
reason: "unexpected token".into(),
},
};
let scanner = make(vec![entry.clone()]);
let got = scanner.scan().expect("scan must succeed");
assert_eq!(got, vec![entry]);
let variants = vec![
LoadDefect::SourceUnreadable {
reason: "permission denied".into(),
},
LoadDefect::InvalidFrontmatter {
reason: "missing colon".into(),
},
LoadDefect::MissingId,
LoadDefect::IdPrefixMismatch {
expected: "ADR-".into(),
found: "DDR-".into(),
},
LoadDefect::InvalidStatus {
value: "Klingon".into(),
},
];
let entries: Vec<_> = variants
.into_iter()
.enumerate()
.map(|(i, defect)| MalformedEntry {
location: EntryLocator::new(format!("docs/adr/{i:04}/index.md")),
origin: EntryOrigin::Local,
defect,
})
.collect();
let scanner = make(entries.clone());
let mut got = scanner.scan().expect("scan must succeed");
got.sort_by(|a, b| a.location.as_str().cmp(b.location.as_str()));
let mut expected = entries.clone();
expected.sort_by(|a, b| a.location.as_str().cmp(b.location.as_str()));
assert_eq!(got, expected, "every LoadDefect variant must round-trip");
let local = MalformedEntry {
location: EntryLocator::new("docs/adr/local/index.md"),
origin: EntryOrigin::Local,
defect: LoadDefect::MissingId,
};
let unioned = MalformedEntry {
location: EntryLocator::new("../shared/adr/u/index.md"),
origin: EntryOrigin::Union {
name: "../shared/adr".into(),
},
defect: LoadDefect::MissingId,
};
let scanner = make(vec![local.clone(), unioned.clone()]);
let got = scanner.scan().expect("scan must succeed");
assert_eq!(got.len(), 2);
assert!(got.contains(&local));
assert!(got.contains(&unioned));
}
#[cfg(test)]
mod contract_self_check {
use super::*;
#[test]
fn fake_satisfies_contract() {
check_entry_defect_scanner_contract(FakeEntryDefectScanner::with_defects);
}
}
}