Skip to main content

spar/
error.rs

1//! One error type. Every failure that a user could plausibly cause carries a
2//! sentence explaining what to do about it, because a failure whose reason is
3//! missing from the message costs more than the failure itself.
4
5use std::fmt;
6
7/// Why a call failed, where the answer changes what to do about it.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ErrorKind {
10    #[default]
11    Other,
12    /// The call ran past its deadline and was killed. Worth its own kind
13    /// because asking again means waiting exactly as long again, which is the
14    /// one failure where a retry costs more than it can possibly win.
15    TimedOut,
16    /// The CLI itself could not answer: a non-zero exit, or an error event in
17    /// place of a message. Distinct from an answer that arrived and could not
18    /// be parsed, which is what the retry exists for and which a model
19    /// corrects readily when told what was wrong. Nothing about a refusal, a
20    /// quota, or a crash is corrected by being asked the same thing again.
21    CallFailed,
22    /// A write failed locally, but the destination could not be reread to tell
23    /// whether it landed. Repeating it blindly could duplicate the write.
24    UncertainWrite,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SparError {
29    message: String,
30    kind: ErrorKind,
31}
32
33impl SparError {
34    pub fn new(message: impl Into<String>) -> Self {
35        Self {
36            message: message.into(),
37            kind: ErrorKind::Other,
38        }
39    }
40
41    pub fn timed_out(message: impl Into<String>) -> Self {
42        Self {
43            message: message.into(),
44            kind: ErrorKind::TimedOut,
45        }
46    }
47
48    /// The CLI could not answer at all, as opposed to answering unusably.
49    pub fn call_failed(message: impl Into<String>) -> Self {
50        Self {
51            message: message.into(),
52            kind: ErrorKind::CallFailed,
53        }
54    }
55
56    pub fn uncertain_write(message: impl Into<String>) -> Self {
57        Self {
58            message: message.into(),
59            kind: ErrorKind::UncertainWrite,
60        }
61    }
62
63    pub fn kind(&self) -> ErrorKind {
64        self.kind
65    }
66
67    /// Whether asking the same thing again could plausibly go better.
68    pub fn worth_retrying(&self) -> bool {
69        !matches!(self.kind, ErrorKind::TimedOut | ErrorKind::UncertainWrite)
70    }
71
72    pub fn message(&self) -> &str {
73        &self.message
74    }
75
76    /// The last line of a multi-line failure. Useful when a nested command's
77    /// own error is the interesting part and the preamble is not.
78    pub fn last_line(&self) -> &str {
79        self.message.lines().next_back().unwrap_or(&self.message)
80    }
81
82    /// The first line, for one-line status output.
83    pub fn first_line(&self) -> &str {
84        self.message.lines().next().unwrap_or(&self.message)
85    }
86}
87
88impl fmt::Display for SparError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.write_str(&self.message)
91    }
92}
93
94impl std::error::Error for SparError {}
95
96impl From<std::io::Error> for SparError {
97    fn from(e: std::io::Error) -> Self {
98        SparError::new(e.to_string())
99    }
100}
101
102impl From<serde_json::Error> for SparError {
103    fn from(e: serde_json::Error) -> Self {
104        SparError::new(format!("invalid JSON: {e}"))
105    }
106}
107
108impl From<toml::de::Error> for SparError {
109    fn from(e: toml::de::Error) -> Self {
110        SparError::new(format!("invalid TOML: {e}"))
111    }
112}
113
114pub type Result<T> = std::result::Result<T, SparError>;
115
116/// Build a `SparError` with `format!` syntax.
117#[macro_export]
118macro_rules! spar_err {
119    ($($arg:tt)*) => { $crate::error::SparError::new(format!($($arg)*)) };
120}
121
122/// Return early with a `SparError`.
123#[macro_export]
124macro_rules! bail {
125    ($($arg:tt)*) => { return Err($crate::spar_err!($($arg)*)) };
126}