Skip to main content

santh_error/
contract.rs

1//! The fleet-wide error contract trait.
2//!
3//! [`SanthErrorContract`] lets any domain error enum render through the same
4//! actionable-message formatter as the canonical [`SanthError`], without
5//! folding its variants into a central type.
6
7use std::borrow::Cow;
8
9use crate::{compose_message, ErrorLocation, SanthError};
10
11/// The Santh error contract: every Santh error answers the same questions -
12/// *which* error it is ([`error_code`](Self::error_code)), *how to fix it*
13/// ([`fix_hint`](Self::fix_hint), always starting with `"Fix: "`), and *what*
14/// failed (`title` via [`Display`](std::fmt::Display)) - plus optional
15/// `context` and `location`.
16///
17/// Domain crates keep their own error enums - their variants *are* their
18/// behavior - and implement this trait to join the contract. They do **not**
19/// fold their variants into [`SanthError`]: the trait gives one consistent
20/// surface across the fleet without duplicating each crate's API upward. A
21/// `thiserror`-style enum needs only `error_code` and `fix_hint`; the title
22/// comes from `Display` and the rest defaults sensibly.
23///
24/// [`SanthError`] implements this trait, so the canonical type and every
25/// domain error render identically via
26/// [`actionable_message`](Self::actionable_message).
27pub trait SanthErrorContract: std::error::Error {
28    /// Stable, machine-readable error code, e.g. `"KEYHOG-E001"`.
29    fn error_code(&self) -> &'static str;
30
31    /// The actionable fix hint. Must start with `"Fix: "`.
32    fn fix_hint(&self) -> Cow<'_, str>;
33
34    /// One-line human-readable title. Defaults to the
35    /// [`Display`](std::fmt::Display) output, which is correct for
36    /// `thiserror`-style enums whose `Display` is the short message.
37    fn title(&self) -> Cow<'_, str> {
38        Cow::Owned(self.to_string())
39    }
40
41    /// Key-value diagnostic context. Empty by default.
42    fn context(&self) -> Vec<(Cow<'static, str>, String)> {
43        Vec::new()
44    }
45
46    /// Optional source or configuration location. `None` by default.
47    fn location(&self) -> Option<&ErrorLocation> {
48        None
49    }
50
51    /// Actionable, human-readable message: title, fix, context, location, and
52    /// the source chain, with secrets redacted. The default matches
53    /// [`SanthError`] exactly; override only for a custom layout.
54    fn actionable_message(&self) -> String {
55        let title = self.title();
56        let fix = self.fix_hint();
57        let context = self.context();
58        compose_message(
59            &title,
60            &fix,
61            &context,
62            self.location(),
63            std::error::Error::source(self),
64        )
65    }
66}
67
68impl SanthErrorContract for SanthError {
69    fn error_code(&self) -> &'static str {
70        self.code
71    }
72
73    fn fix_hint(&self) -> Cow<'_, str> {
74        Cow::Borrowed(&self.fix)
75    }
76
77    fn title(&self) -> Cow<'_, str> {
78        Cow::Borrowed(&self.title)
79    }
80
81    fn context(&self) -> Vec<(Cow<'static, str>, String)> {
82        self.context.clone()
83    }
84
85    fn location(&self) -> Option<&ErrorLocation> {
86        self.location.as_ref()
87    }
88}