Skip to main content

browser_automation_cli/
error.rs

1//! Typed CLI errors with sysexits-style exit codes.
2//!
3//! # Error kinds
4//!
5//! `ErrorKind` maps to process exit codes used by the binary and JSON envelopes.
6//!
7//! # Examples
8//!
9//! ```
10//! use browser_automation_cli::error::{CliError, ErrorKind};
11//!
12//! let err = CliError::with_suggestion(
13//!     ErrorKind::Unavailable,
14//!     "chrome not found",
15//!     "install Chromium or Google Chrome",
16//! );
17//! assert_eq!(err.exit_code(), 69);
18//! assert_eq!(err.kind().as_str(), "unavailable");
19//! ```
20
21use std::fmt;
22
23/// High-level failure category mapped to a process exit code.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ErrorKind {
26    /// Invalid usage or clap parse failure (`2`).
27    Usage,
28    /// Invalid data or payload (`65`).
29    Data,
30    /// Required input missing (`66`).
31    NoInput,
32    /// Browser or dependency unavailable (`69`).
33    Unavailable,
34    /// Internal software failure (`70`).
35    Software,
36    /// Browser runtime failure (`70`).
37    Browser,
38    /// CDP/protocol failure (`70`).
39    Protocol,
40    /// Wall-clock timeout (`124`).
41    Timeout,
42    /// Cancelled by signal (`130`).
43    Cancelled,
44    /// Broken pipe on stdout (`141`).
45    BrokenPipe,
46    /// Configuration error (`78`).
47    Config,
48    /// I/O failure (`74`).
49    Io,
50}
51
52impl ErrorKind {
53    /// Process exit code for this kind.
54    pub fn exit_code(self) -> u8 {
55        match self {
56            ErrorKind::Usage => 2,
57            ErrorKind::Data => 65,
58            ErrorKind::NoInput => 66,
59            ErrorKind::Unavailable => 69,
60            ErrorKind::Software | ErrorKind::Browser | ErrorKind::Protocol => 70,
61            ErrorKind::Config => 78,
62            ErrorKind::Io => 74,
63            ErrorKind::Timeout => 124,
64            ErrorKind::Cancelled => 130,
65            ErrorKind::BrokenPipe => 141,
66        }
67    }
68
69    /// Stable machine-readable kind string for JSON envelopes.
70    pub fn as_str(self) -> &'static str {
71        match self {
72            ErrorKind::Usage => "usage",
73            ErrorKind::Data => "data",
74            ErrorKind::NoInput => "no-input",
75            ErrorKind::Unavailable => "unavailable",
76            ErrorKind::Software => "software",
77            ErrorKind::Browser => "browser",
78            ErrorKind::Protocol => "protocol",
79            ErrorKind::Timeout => "timeout",
80            ErrorKind::Cancelled => "cancelled",
81            ErrorKind::BrokenPipe => "broken-pipe",
82            ErrorKind::Config => "config",
83            ErrorKind::Io => "io",
84        }
85    }
86}
87
88/// Typed CLI error with optional remediation hint.
89#[derive(Debug, Clone)]
90pub struct CliError {
91    kind: ErrorKind,
92    message: String,
93    suggestion: Option<String>,
94    /// Optional partial JSON payload (e.g. fail-fast `run` steps).
95    data: Option<serde_json::Value>,
96}
97
98impl CliError {
99    /// Create an error without a suggestion.
100    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
101        Self {
102            kind,
103            message: message.into(),
104            suggestion: None,
105            data: None,
106        }
107    }
108
109    /// Create an error with a short remediation suggestion for agents.
110    pub fn with_suggestion(
111        kind: ErrorKind,
112        message: impl Into<String>,
113        suggestion: impl Into<String>,
114    ) -> Self {
115        Self {
116            kind,
117            message: message.into(),
118            suggestion: Some(suggestion.into()),
119            data: None,
120        }
121    }
122
123    /// Attach partial JSON `data` (kept on error envelopes).
124    pub fn with_data(mut self, data: serde_json::Value) -> Self {
125        self.data = Some(data);
126        self
127    }
128
129    /// Error category.
130    pub fn kind(&self) -> ErrorKind {
131        self.kind
132    }
133
134    /// Human-readable message (also used in JSON `error.message`).
135    pub fn message(&self) -> &str {
136        &self.message
137    }
138
139    /// Optional remediation hint.
140    pub fn suggestion(&self) -> Option<&str> {
141        self.suggestion.as_deref()
142    }
143
144    /// Optional partial data payload.
145    pub fn data(&self) -> Option<&serde_json::Value> {
146        self.data.as_ref()
147    }
148
149    /// Process exit code for this error.
150    pub fn exit_code(&self) -> u8 {
151        self.kind.exit_code()
152    }
153}
154
155impl fmt::Display for CliError {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(f, "{}", self.message)
158    }
159}
160
161impl std::error::Error for CliError {}