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}