Skip to main content

concinnity_world/check/
fault.rs

1//! A validation failure and where in an asset's args it was found. A checker
2//! walks authored JSON by descending into it, so the walk already knows where it
3//! is: each frame attaches the one hop it descended through as the error unwinds,
4//! and the location assembles itself outermost-first without any frame having to
5//! know the whole path. An editor can then point at the value at fault instead of
6//! leaving the author to find it from the message alone.
7
8/// One hop into an asset's authored args: an object key or an array index.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Step {
11    /// An object key.
12    Field(String),
13    /// An array index.
14    Index(usize),
15}
16
17/// A failed check: what is wrong, and where in the args it was found.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Fault {
20    /// The checker's own message, which is what a build reports verbatim.
21    pub message: String,
22    /// Hops from the args root to the value at fault, outermost first. Empty
23    /// when nothing narrower than the whole asset is to blame.
24    pub at: Vec<Step>,
25}
26
27impl Fault {
28    pub(crate) fn new(message: impl Into<String>) -> Fault {
29        Fault {
30            message: message.into(),
31            at: Vec::new(),
32        }
33    }
34
35    // Record that this fault was found inside `step`. Called as the error
36    // unwinds, so the outermost hop is attached last and lands first.
37    pub(crate) fn within(mut self, step: Step) -> Fault {
38        self.at.insert(0, step);
39        self
40    }
41
42    // Say what the message is about, keeping the location: the caller's label
43    // then the message, which is how a nested complaint reads as one sentence
44    // ("`let` is missing", "Behavior 'chase': unknown node `teleport`").
45    pub(crate) fn about(mut self, what: &str) -> Fault {
46        self.message = format!("{what} {}", self.message);
47        self
48    }
49}
50
51impl std::fmt::Display for Fault {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.write_str(&self.message)
54    }
55}
56
57pub(crate) fn field(name: &str) -> Step {
58    Step::Field(name.to_string())
59}
60
61// Attach one hop to whatever a nested check reported. Implemented on `Result` so
62// a descent reads as the call plus where it went: `check(..).at_field("cond")?`.
63pub(crate) trait Locate<T> {
64    fn at_field(self, name: &str) -> Result<T, Fault>;
65    fn at_index(self, index: usize) -> Result<T, Fault>;
66}
67
68impl<T> Locate<T> for Result<T, Fault> {
69    fn at_field(self, name: &str) -> Result<T, Fault> {
70        self.map_err(|f| f.within(field(name)))
71    }
72
73    fn at_index(self, index: usize) -> Result<T, Fault> {
74        self.map_err(|f| f.within(Step::Index(index)))
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    // The walk attaches hops as the error unwinds, so a fault raised deep in a
83    // body comes out addressing that spot from the root.
84    #[test]
85    fn hops_attached_while_unwinding_read_outermost_first() {
86        let deep: Result<(), Fault> = Err(Fault::new("is missing"));
87        let out = deep
88            .at_field("cond")
89            .at_field("if")
90            .at_index(1)
91            .at_field("do")
92            .unwrap_err();
93        assert_eq!(
94            out.at,
95            vec![field("do"), Step::Index(1), field("if"), field("cond")],
96        );
97    }
98
99    #[test]
100    fn a_fault_with_nothing_to_blame_carries_no_location() {
101        let f = Fault::new("duplicate name");
102        assert!(f.at.is_empty());
103        assert_eq!(f.to_string(), "duplicate name");
104    }
105
106    // Labels compose into one sentence, and they never disturb the location:
107    // the build reads the message and the editor reads the place.
108    #[test]
109    fn labels_compose_and_leave_the_location_alone() {
110        let f = Fault::new("is missing")
111            .within(field("value"))
112            .about("`let`")
113            .about("Behavior 'chase':");
114        assert_eq!(f.message, "Behavior 'chase': `let` is missing");
115        assert_eq!(f.at, vec![field("value")]);
116    }
117
118    #[test]
119    fn success_passes_through_untouched() {
120        let ok: Result<u8, Fault> = Ok(7);
121        assert_eq!(ok.at_field("cond").at_index(0).unwrap(), 7);
122    }
123}