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}