Skip to main content

basis_tasks/
error.rs

1//! One error type for the whole crate.
2//!
3//! Every fallible operation in the moved implementation — file I/O, decode
4//! failures, policy refusals — already carries a human-readable message; that
5//! was true when it answered only to `basis-cli`'s own `ClientError`, and
6//! nothing about becoming a library changes it. What changes is the type: a
7//! `Result<_, String>` is an implementation detail a caller should not have
8//! to `.to_string()` their way around, so the public surface returns this
9//! instead. `basis-cli`'s exit-code and hint *text* — which of these are a
10//! timeout, which are a usage error, what a `next:` line reads — stays
11//! exactly where ADR-0015 puts it, in the CLI's own `ClientError`; this type
12//! carries no opinion about either. What it does carry, for the handful of
13//! errors that name one unambiguously, is [`hint`](Error::hint) — a fact a
14//! host builds its own hint text from, not the text itself.
15
16use std::fmt;
17
18use crate::handle::TaskHandle;
19
20/// A next step this error names unambiguously enough for a host to build its
21/// own hint text from, without parsing this error's message. Not every
22/// error carries one: an ordinary operational failure has no next step this
23/// crate can name from here, and stays [`None`](Error::hint).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Hint {
26    /// The caller is itself a task in a different workspace; spawning here
27    /// needs the detached escape — `basis spawn --detached <PROMPT>`, in
28    /// `basis-cli`'s words.
29    SpawnDetached,
30    /// No task in the target workspace has a conversation to continue yet —
31    /// `basis spawn <PROMPT>`, in `basis-cli`'s words.
32    SpawnFresh,
33    /// The conversation named exists but cannot be continued right now —
34    /// still running (one executor per conversation), or never attached —
35    /// so the next step is the task itself: `basis wait <task>`.
36    Wait(TaskHandle),
37}
38
39/// An error from a `basis-tasks` operation: what went wrong, in one line.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Error {
42    message: String,
43    invalid_reference: bool,
44    hint: Option<Hint>,
45}
46
47impl Error {
48    pub(crate) fn new(message: impl Into<String>) -> Self {
49        Self {
50            message: message.into(),
51            invalid_reference: false,
52            hint: None,
53        }
54    }
55
56    /// An error over a handle or reference the caller gave that could never
57    /// have resolved — not a task in a state that might still change (a
58    /// running task, an expired wait), but one that was never going to name
59    /// anything valid, the way a malformed handle or a handle from a
60    /// different workspace does not.
61    pub(crate) fn invalid_reference(message: impl Into<String>) -> Self {
62        Self {
63            message: message.into(),
64            invalid_reference: true,
65            hint: None,
66        }
67    }
68
69    /// Attaches the next step this error names, for a host that wants to
70    /// build a hint from it.
71    #[must_use]
72    pub(crate) fn with_hint(self, hint: Hint) -> Self {
73        Self {
74            hint: Some(hint),
75            ..self
76        }
77    }
78
79    /// Whether this is exactly that: a bad argument no amount of waiting
80    /// fixes, as opposed to an ordinary operational failure. A host mapping
81    /// this crate's errors onto its own vocabulary — `basis-cli`'s "usage"
82    /// exit code (ADR-0015), for one — reads this rather than the message
83    /// text.
84    pub fn is_invalid_reference(&self) -> bool {
85        self.invalid_reference
86    }
87
88    /// The next step this error names, when it names one unambiguously —
89    /// see [`Hint`].
90    pub fn hint(&self) -> Option<&Hint> {
91        self.hint.as_ref()
92    }
93}
94
95impl fmt::Display for Error {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(&self.message)
98    }
99}
100
101impl std::error::Error for Error {}
102
103impl From<String> for Error {
104    fn from(message: String) -> Self {
105        Self::new(message)
106    }
107}
108
109impl From<&str> for Error {
110    fn from(message: &str) -> Self {
111        Self::new(message)
112    }
113}