#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Step {
Field(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fault {
pub message: String,
pub at: Vec<Step>,
}
impl Fault {
pub(crate) fn new(message: impl Into<String>) -> Fault {
Fault {
message: message.into(),
at: Vec::new(),
}
}
pub(crate) fn within(mut self, step: Step) -> Fault {
self.at.insert(0, step);
self
}
pub(crate) fn about(mut self, what: &str) -> Fault {
self.message = format!("{what} {}", self.message);
self
}
}
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
pub(crate) fn field(name: &str) -> Step {
Step::Field(name.to_string())
}
pub(crate) trait Locate<T> {
fn at_field(self, name: &str) -> Result<T, Fault>;
fn at_index(self, index: usize) -> Result<T, Fault>;
}
impl<T> Locate<T> for Result<T, Fault> {
fn at_field(self, name: &str) -> Result<T, Fault> {
self.map_err(|f| f.within(field(name)))
}
fn at_index(self, index: usize) -> Result<T, Fault> {
self.map_err(|f| f.within(Step::Index(index)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hops_attached_while_unwinding_read_outermost_first() {
let deep: Result<(), Fault> = Err(Fault::new("is missing"));
let out = deep
.at_field("cond")
.at_field("if")
.at_index(1)
.at_field("do")
.unwrap_err();
assert_eq!(
out.at,
vec![field("do"), Step::Index(1), field("if"), field("cond")],
);
}
#[test]
fn a_fault_with_nothing_to_blame_carries_no_location() {
let f = Fault::new("duplicate name");
assert!(f.at.is_empty());
assert_eq!(f.to_string(), "duplicate name");
}
#[test]
fn labels_compose_and_leave_the_location_alone() {
let f = Fault::new("is missing")
.within(field("value"))
.about("`let`")
.about("Behavior 'chase':");
assert_eq!(f.message, "Behavior 'chase': `let` is missing");
assert_eq!(f.at, vec![field("value")]);
}
#[test]
fn success_passes_through_untouched() {
let ok: Result<u8, Fault> = Ok(7);
assert_eq!(ok.at_field("cond").at_index(0).unwrap(), 7);
}
}