Skip to main content

netsky_core/
error.rs

1//! The netsky error taxonomy.
2//!
3//! We speak in typed variants, not untyped messages. Each variant
4//! names a concrete boundary where recovery is meaningful: I/O,
5//! subprocess timeout vs nonzero exit, missing dep, tmux, prompt
6//! render, invalid input. A residual `Message` catch-all exists for
7//! one-shot error sites that don't yet warrant a dedicated variant —
8//! prefer promoting to a typed variant over using `Message` long-term.
9//!
10//! Display + std::error::Error + From impls are hand-rolled — no
11//! thiserror. Keeps the error surface transparent at the expense of
12//! a few lines per variant; worth it for zero macro deps.
13
14use std::io;
15
16/// Convenience alias used across the workspace.
17pub type Result<T> = std::result::Result<T, Error>;
18
19#[derive(Debug)]
20pub enum Error {
21    Io(io::Error),
22    Json(serde_json::Error),
23    Prompt(netsky_prompts::prompt::PromptError),
24    Tmux(String),
25    MissingDep(&'static str),
26    SubprocessTimeout {
27        what: String,
28        ceiling_s: u64,
29    },
30    SubprocessFailed {
31        what: String,
32        code: i32,
33    },
34    AiUnknownModel(String),
35    AiJsonParse {
36        runtime: String,
37        detail: String,
38    },
39    AiMissingOutput {
40        runtime: String,
41    },
42    Invalid(String),
43    /// Residual catch-all for sites not yet promoted to a typed
44    /// variant. Prefer to add a new variant when introducing a new
45    /// error class — `Message` is for stragglers, not the happy path.
46    Message(String),
47}
48
49impl Error {
50    pub fn msg(s: impl Into<String>) -> Self {
51        Self::Message(s.into())
52    }
53}
54
55impl std::fmt::Display for Error {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::Io(e) => write!(f, "I/O: {e}"),
59            Self::Json(e) => write!(f, "JSON: {e}"),
60            Self::Prompt(e) => write!(f, "prompt: {e}"),
61            Self::Tmux(s) => write!(f, "tmux: {s}"),
62            Self::MissingDep(s) => write!(f, "missing dep on PATH: {s}"),
63            Self::SubprocessTimeout { what, ceiling_s } => {
64                write!(f, "subprocess `{what}` timed out after {ceiling_s}s")
65            }
66            Self::SubprocessFailed { what, code } => {
67                write!(f, "subprocess `{what}` failed (exit {code})")
68            }
69            Self::AiUnknownModel(model) => write!(f, "unknown ai model `{model}`"),
70            Self::AiJsonParse { runtime, detail } => {
71                write!(f, "{runtime} JSON parse failed: {detail}")
72            }
73            Self::AiMissingOutput { runtime } => write!(f, "{runtime} JSON missing final text"),
74            Self::Invalid(s) => write!(f, "invalid input: {s}"),
75            Self::Message(s) => f.write_str(s),
76        }
77    }
78}
79
80impl std::error::Error for Error {
81    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
82        match self {
83            Self::Io(e) => Some(e),
84            Self::Json(e) => Some(e),
85            Self::Prompt(e) => Some(e),
86            _ => None,
87        }
88    }
89}
90
91impl From<io::Error> for Error {
92    fn from(e: io::Error) -> Self {
93        Self::Io(e)
94    }
95}
96
97impl From<serde_json::Error> for Error {
98    fn from(e: serde_json::Error) -> Self {
99        Self::Json(e)
100    }
101}
102
103impl From<netsky_prompts::prompt::PromptError> for Error {
104    fn from(e: netsky_prompts::prompt::PromptError) -> Self {
105        Self::Prompt(e)
106    }
107}
108
109impl From<netsky_sh::Error> for Error {
110    fn from(e: netsky_sh::Error) -> Self {
111        // netsky_sh covers tmux + shell + which; render as Tmux since
112        // that's where every current use site lives. Promote when we
113        // grow distinct branches worth distinguishing.
114        Self::Tmux(format!("{e}"))
115    }
116}
117
118impl From<std::time::SystemTimeError> for Error {
119    fn from(e: std::time::SystemTimeError) -> Self {
120        Self::Message(format!("system time: {e}"))
121    }
122}
123
124impl From<std::num::ParseIntError> for Error {
125    fn from(e: std::num::ParseIntError) -> Self {
126        Self::Invalid(format!("parse int: {e}"))
127    }
128}
129
130/// Early-return with an `Error::Message`. Drop-in for `anyhow::bail!`.
131/// Single-literal form supports captured identifiers via `format!`'s
132/// Rust 2021 implicit named args.
133#[macro_export]
134macro_rules! bail {
135    ($fmt:literal $(,)?) => {
136        return Err($crate::error::Error::Message(format!($fmt)))
137    };
138    ($fmt:expr, $($arg:tt)*) => {
139        return Err($crate::error::Error::Message(format!($fmt, $($arg)*)))
140    };
141}
142
143/// Construct an `Error::Message` from a format string. Drop-in for
144/// `anyhow::anyhow!`. Useful inside `.map_err(|e| netsky_core::anyhow!("...: {e}"))`.
145#[macro_export]
146macro_rules! anyhow {
147    ($fmt:literal $(,)?) => {
148        $crate::error::Error::Message(format!($fmt))
149    };
150    ($fmt:expr, $($arg:tt)*) => {
151        $crate::error::Error::Message(format!($fmt, $($arg)*))
152    };
153}