browser_automation_cli/error.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Typed CLI errors with sysexits-style exit codes.
3//!
4//! # Error kinds
5//!
6//! [`ErrorKind`](crate::error::ErrorKind) maps to process exit codes used by the
7//! binary and JSON envelopes. Stable machine strings come from
8//! [`ErrorKind::as_str`](crate::error::ErrorKind::as_str).
9//!
10//! # Conversions
11//!
12//! - [`CliError::exit_code`](crate::error::CliError::exit_code) /
13//! [`ErrorKind::exit_code`](crate::error::ErrorKind::exit_code) — infallible
14//! `u8` mapping (zero-cost)
15//! - [`ErrorKind::as_str`](crate::error::ErrorKind::as_str) — static
16//! `&'static str` for JSON `error.kind` (zero-cost)
17//! - Prefer these helpers over ad-hoc `as` casts when mapping to process status
18//! - There is no fallible `TryFrom` path: every kind has a fixed exit code
19//!
20//! # Cost
21//!
22//! | Operation | Cost |
23//! |-----------|------|
24//! | `exit_code()` | O(1), no allocation |
25//! | `as_str()` | O(1), static slice |
26//! | `CliError::new` / `with_suggestion` | allocates `String` message (and optional suggestion) |
27//!
28//! # Examples
29//!
30//! ```
31//! use browser_automation_cli::error::{CliError, ErrorKind};
32//!
33//! let err = CliError::with_suggestion(
34//! ErrorKind::Unavailable,
35//! "chrome not found",
36//! "install Chromium or Google Chrome",
37//! );
38//! assert_eq!(err.exit_code(), 69);
39//! assert_eq!(err.kind().as_str(), "unavailable");
40//! ```
41//!
42//! # See also
43//!
44//! - [`crate::envelope`] for JSON success/error envelopes
45//! - [`crate::exit_code_for`] for library callers that already hold a
46//! [`CliError`](crate::error::CliError)
47
48use thiserror::Error;
49
50/// High-level failure category mapped to a process exit code.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ErrorKind {
53 /// Invalid usage or clap parse failure (`2`).
54 Usage,
55 /// Invalid data or payload (`65`).
56 Data,
57 /// Required input missing (`66`).
58 NoInput,
59 /// Browser or dependency unavailable (`69`).
60 Unavailable,
61 /// Internal software failure (`70`).
62 Software,
63 /// Browser runtime failure (`70`).
64 Browser,
65 /// CDP/protocol failure (`70`).
66 Protocol,
67 /// Wall-clock timeout (`124`).
68 Timeout,
69 /// Cancelled by signal (`130`).
70 Cancelled,
71 /// Broken pipe on stdout (`141`).
72 BrokenPipe,
73 /// Configuration error (`78`).
74 Config,
75 /// I/O failure (`74`).
76 Io,
77}
78
79impl ErrorKind {
80 /// Process exit code for this kind.
81 pub fn exit_code(self) -> u8 {
82 match self {
83 ErrorKind::Usage => 2,
84 ErrorKind::Data => 65,
85 ErrorKind::NoInput => 66,
86 ErrorKind::Unavailable => 69,
87 ErrorKind::Software | ErrorKind::Browser | ErrorKind::Protocol => 70,
88 ErrorKind::Config => 78,
89 ErrorKind::Io => 74,
90 ErrorKind::Timeout => 124,
91 ErrorKind::Cancelled => 130,
92 ErrorKind::BrokenPipe => 141,
93 }
94 }
95
96 /// Stable machine-readable kind string for JSON envelopes.
97 pub fn as_str(self) -> &'static str {
98 match self {
99 ErrorKind::Usage => "usage",
100 ErrorKind::Data => "data",
101 ErrorKind::NoInput => "no-input",
102 ErrorKind::Unavailable => "unavailable",
103 ErrorKind::Software => "software",
104 ErrorKind::Browser => "browser",
105 ErrorKind::Protocol => "protocol",
106 ErrorKind::Timeout => "timeout",
107 ErrorKind::Cancelled => "cancelled",
108 ErrorKind::BrokenPipe => "broken-pipe",
109 ErrorKind::Config => "config",
110 ErrorKind::Io => "io",
111 }
112 }
113}
114
115/// Typed CLI error with optional remediation hint.
116#[derive(Debug, Clone, Error)]
117#[error("{message}")]
118pub struct CliError {
119 kind: ErrorKind,
120 message: String,
121 suggestion: Option<String>,
122 /// Optional partial JSON payload (e.g. fail-fast `run` steps).
123 data: Option<serde_json::Value>,
124}
125
126impl CliError {
127 /// Create an error without a suggestion.
128 ///
129 /// Marked `#[cold]`: success paths dominate; keeps error construction out of
130 /// the predicted hot branch layout (rules_rust_eficiencia_e_performance).
131 #[cold]
132 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
133 Self {
134 kind,
135 message: message.into(),
136 suggestion: None,
137 data: None,
138 }
139 }
140
141 /// Create an error with a short remediation suggestion for agents.
142 #[cold]
143 pub fn with_suggestion(
144 kind: ErrorKind,
145 message: impl Into<String>,
146 suggestion: impl Into<String>,
147 ) -> Self {
148 Self {
149 kind,
150 message: message.into(),
151 suggestion: Some(suggestion.into()),
152 data: None,
153 }
154 }
155
156 /// Attach partial JSON `data` (kept on error envelopes).
157 pub fn with_data(mut self, data: serde_json::Value) -> Self {
158 self.data = Some(data);
159 self
160 }
161
162 /// Error category.
163 pub fn kind(&self) -> ErrorKind {
164 self.kind
165 }
166
167 /// Human-readable message (also used in JSON `error.message`).
168 pub fn message(&self) -> &str {
169 &self.message
170 }
171
172 /// Optional remediation hint.
173 pub fn suggestion(&self) -> Option<&str> {
174 self.suggestion.as_deref()
175 }
176
177 /// Optional partial data payload.
178 pub fn data(&self) -> Option<&serde_json::Value> {
179 self.data.as_ref()
180 }
181
182 /// Process exit code for this error.
183 pub fn exit_code(&self) -> u8 {
184 self.kind.exit_code()
185 }
186}
187