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}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct SparError {
26    message: String,
27    kind: ErrorKind,
28}
29
30impl SparError {
31    pub fn new(message: impl Into<String>) -> Self {
32        Self {
33            message: message.into(),
34            kind: ErrorKind::Other,
35        }
36    }
37
38    pub fn timed_out(message: impl Into<String>) -> Self {
39        Self {
40            message: message.into(),
41            kind: ErrorKind::TimedOut,
42        }
43    }
44
45    /// The CLI could not answer at all, as opposed to answering unusably.
46    pub fn call_failed(message: impl Into<String>) -> Self {
47        Self {
48            message: message.into(),
49            kind: ErrorKind::CallFailed,
50        }
51    }
52
53    pub fn kind(&self) -> ErrorKind {
54        self.kind
55    }
56
57    /// Whether asking the same thing again could plausibly go better.
58    pub fn worth_retrying(&self) -> bool {
59        self.kind != ErrorKind::TimedOut
60    }
61
62    pub fn message(&self) -> &str {
63        &self.message
64    }
65
66    /// The last line of a multi-line failure. Useful when a nested command's
67    /// own error is the interesting part and the preamble is not.
68    pub fn last_line(&self) -> &str {
69        self.message.lines().next_back().unwrap_or(&self.message)
70    }
71
72    /// The first line, for one-line status output.
73    pub fn first_line(&self) -> &str {
74        self.message.lines().next().unwrap_or(&self.message)
75    }
76}
77
78impl fmt::Display for SparError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.write_str(&self.message)
81    }
82}
83
84impl std::error::Error for SparError {}
85
86impl From<std::io::Error> for SparError {
87    fn from(e: std::io::Error) -> Self {
88        SparError::new(e.to_string())
89    }
90}
91
92impl From<serde_json::Error> for SparError {
93    fn from(e: serde_json::Error) -> Self {
94        SparError::new(format!("invalid JSON: {e}"))
95    }
96}
97
98impl From<toml::de::Error> for SparError {
99    fn from(e: toml::de::Error) -> Self {
100        SparError::new(format!("invalid TOML: {e}"))
101    }
102}
103
104pub type Result<T> = std::result::Result<T, SparError>;
105
106/// Build a `SparError` with `format!` syntax.
107#[macro_export]
108macro_rules! spar_err {
109    ($($arg:tt)*) => { $crate::error::SparError::new(format!($($arg)*)) };
110}
111
112/// Return early with a `SparError`.
113#[macro_export]
114macro_rules! bail {
115    ($($arg:tt)*) => { return Err($crate::spar_err!($($arg)*)) };
116}