1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//! Hard load-time failures reported by an [`EntryDefectScanner`].
//!
//! Distinct from semantic warnings on a successfully parsed entry,
//! which stay on the repository surface. A `LoadDefect` means the
//! adapter could not produce a valid domain value from the source.
/// Closed set of load-time failures the domain knows how to talk
/// about. Adapters translate their own error vocabulary
/// (`io::Error`, `serde_yaml::Error`, …) into one of these variants.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LoadDefect {
/// Source could not be read at all (missing file, permission denied,
/// transport error, …). `reason` is an adapter-formatted human message.
SourceUnreadable { reason: String },
/// Frontmatter was present but unparseable.
InvalidFrontmatter { reason: String },
/// Frontmatter was readable but did not carry an `id:` field.
MissingId,
/// The entry's `id:` did not start with the corpus's configured
/// `id_prefix`. Carries both sides for the error message.
IdPrefixMismatch { expected: String, found: String },
/// The entry's `status:` is not part of the corpus's allowed set.
InvalidStatus { value: String },
/// The entry parsed cleanly but its `events.jsonl` sibling is absent,
/// breaking the v7 structural contract.
MissingEventsLog,
/// The frontmatter `status:` field does not match the journal's
/// terminal projection; either the frontmatter or the journal is stale.
StatusJournalMismatch {
frontmatter: String,
terminal: String,
},
}
#[cfg(test)]
pub mod strategy {
use super::*;
use proptest::prelude::*;
pub fn arb_load_defect() -> impl Strategy<Value = LoadDefect> {
prop_oneof![
"[a-z0-9 ]{1,40}".prop_map(|reason| LoadDefect::SourceUnreadable { reason }),
"[a-z0-9 ]{1,40}".prop_map(|reason| LoadDefect::InvalidFrontmatter { reason }),
Just(LoadDefect::MissingId),
("[A-Z]{2,5}-", "[A-Za-z]{2,5}-")
.prop_map(|(expected, found)| { LoadDefect::IdPrefixMismatch { expected, found } }),
"[a-z]{1,20}".prop_map(|value| LoadDefect::InvalidStatus { value }),
Just(LoadDefect::MissingEventsLog),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variants_are_distinguishable_by_equality() {
assert_ne!(
LoadDefect::MissingId,
LoadDefect::InvalidStatus { value: "x".into() }
);
assert_ne!(
LoadDefect::SourceUnreadable { reason: "a".into() },
LoadDefect::SourceUnreadable { reason: "b".into() },
);
}
#[test]
fn equality_is_value_based() {
assert_eq!(
LoadDefect::IdPrefixMismatch {
expected: "ADR-".into(),
found: "DDR-".into(),
},
LoadDefect::IdPrefixMismatch {
expected: "ADR-".into(),
found: "DDR-".into(),
},
);
}
#[test]
fn hash_groups_equal_values() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(LoadDefect::MissingId);
set.insert(LoadDefect::MissingId);
assert_eq!(set.len(), 1);
}
}