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(crate::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    Invalid(String),
35    /// Residual catch-all for sites not yet promoted to a typed
36    /// variant. Prefer to add a new variant when introducing a new
37    /// error class — `Message` is for stragglers, not the happy path.
38    Message(String),
39}
40
41impl Error {
42    pub fn msg(s: impl Into<String>) -> Self {
43        Self::Message(s.into())
44    }
45}
46
47impl std::fmt::Display for Error {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Self::Io(e) => write!(f, "I/O: {e}"),
51            Self::Json(e) => write!(f, "JSON: {e}"),
52            Self::Prompt(e) => write!(f, "prompt: {e}"),
53            Self::Tmux(s) => write!(f, "tmux: {s}"),
54            Self::MissingDep(s) => write!(f, "missing dep on PATH: {s}"),
55            Self::SubprocessTimeout { what, ceiling_s } => {
56                write!(f, "subprocess `{what}` timed out after {ceiling_s}s")
57            }
58            Self::SubprocessFailed { what, code } => {
59                write!(f, "subprocess `{what}` failed (exit {code})")
60            }
61            Self::Invalid(s) => write!(f, "invalid input: {s}"),
62            Self::Message(s) => f.write_str(s),
63        }
64    }
65}
66
67impl std::error::Error for Error {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::Io(e) => Some(e),
71            Self::Json(e) => Some(e),
72            Self::Prompt(e) => Some(e),
73            _ => None,
74        }
75    }
76}
77
78impl From<io::Error> for Error {
79    fn from(e: io::Error) -> Self {
80        Self::Io(e)
81    }
82}
83
84impl From<serde_json::Error> for Error {
85    fn from(e: serde_json::Error) -> Self {
86        Self::Json(e)
87    }
88}
89
90impl From<crate::prompt::PromptError> for Error {
91    fn from(e: crate::prompt::PromptError) -> Self {
92        Self::Prompt(e)
93    }
94}
95
96impl From<netsky_sh::Error> for Error {
97    fn from(e: netsky_sh::Error) -> Self {
98        // netsky_sh covers tmux + shell + which; render as Tmux since
99        // that's where every current use site lives. Promote when we
100        // grow distinct branches worth distinguishing.
101        Self::Tmux(format!("{e}"))
102    }
103}
104
105impl From<std::time::SystemTimeError> for Error {
106    fn from(e: std::time::SystemTimeError) -> Self {
107        Self::Message(format!("system time: {e}"))
108    }
109}
110
111impl From<std::num::ParseIntError> for Error {
112    fn from(e: std::num::ParseIntError) -> Self {
113        Self::Invalid(format!("parse int: {e}"))
114    }
115}
116
117/// Early-return with an `Error::Message`. Drop-in for `anyhow::bail!`.
118/// Single-literal form supports captured identifiers via `format!`'s
119/// Rust 2021 implicit named args.
120#[macro_export]
121macro_rules! bail {
122    ($fmt:literal $(,)?) => {
123        return Err($crate::error::Error::Message(format!($fmt)))
124    };
125    ($fmt:expr, $($arg:tt)*) => {
126        return Err($crate::error::Error::Message(format!($fmt, $($arg)*)))
127    };
128}
129
130/// Construct an `Error::Message` from a format string. Drop-in for
131/// `anyhow::anyhow!`. Useful inside `.map_err(|e| netsky_core::anyhow!("...: {e}"))`.
132#[macro_export]
133macro_rules! anyhow {
134    ($fmt:literal $(,)?) => {
135        $crate::error::Error::Message(format!($fmt))
136    };
137    ($fmt:expr, $($arg:tt)*) => {
138        $crate::error::Error::Message(format!($fmt, $($arg)*))
139    };
140}