ai_usagebar/error.rs
1//! Shared error type. Vendors and renderers convert their failures into
2//! `AppError` so the widget shell can decide whether to retry, fall back to
3//! cache, show ⚠, or show "Loading…".
4
5use std::io;
6use std::path::PathBuf;
7
8pub type Result<T> = std::result::Result<T, AppError>;
9
10#[derive(Debug, thiserror::Error)]
11pub enum AppError {
12 /// Local I/O failed (cache write, credentials read, theme file, etc.).
13 #[error("io error at {path}: {source}")]
14 Io {
15 path: PathBuf,
16 #[source]
17 source: io::Error,
18 },
19
20 /// Generic I/O without a meaningful path (e.g. stdout writes).
21 #[error(transparent)]
22 IoBare(#[from] io::Error),
23
24 /// A vendor's credentials file is missing, unreadable, or malformed.
25 /// Distinct from `Io` because the widget treats it as "user must re-auth"
26 /// rather than a transient failure.
27 #[error("credentials error: {0}")]
28 Credentials(String),
29
30 /// HTTP request failed at the transport layer (DNS, TLS, timeout, connect).
31 /// Maps to claudebar's "HTTP 000" — show `Loading…`, don't write
32 /// `.last_error`, retry next tick.
33 #[error("network transport error: {0}")]
34 Transport(String),
35
36 /// HTTP request reached the server but returned a non-2xx status.
37 /// Carries the code + best-effort body so the widget can populate
38 /// `.last_error` for the tooltip.
39 #[error("HTTP {status}: {body}")]
40 Http { status: u16, body: String },
41
42 /// API returned 2xx but the body did not match our expected schema.
43 /// Treated like an HTTP error for tooltip purposes, but logged separately
44 /// because it signals undocumented-endpoint drift.
45 #[error("schema mismatch: {0}")]
46 Schema(String),
47
48 /// JSON serialization/deserialization failure (config files, response bodies).
49 #[error("json error: {0}")]
50 Json(#[from] serde_json::Error),
51
52 /// TOML config parse failure.
53 #[error("toml error: {0}")]
54 Toml(#[from] toml::de::Error),
55
56 /// Catch-all for unexpected conditions (cache lock contention, etc.).
57 #[error("{0}")]
58 Other(String),
59}
60
61impl AppError {
62 /// Convenience for non-pathful I/O.
63 pub fn io_at(path: impl Into<PathBuf>, source: io::Error) -> Self {
64 AppError::Io {
65 path: path.into(),
66 source,
67 }
68 }
69
70 /// True for transient network errors that the widget should hide behind a
71 /// "Loading…" rather than a "⚠".
72 pub fn is_transient(&self) -> bool {
73 matches!(self, AppError::Transport(_))
74 }
75}
76
77/// Map a reqwest error into the right variant. Connection-class failures
78/// become `Transport` (transient); the rest become generic `Http`/`Other`.
79impl From<reqwest::Error> for AppError {
80 fn from(err: reqwest::Error) -> Self {
81 if err.is_timeout() || err.is_connect() || err.is_request() {
82 return AppError::Transport(err.to_string());
83 }
84 if let Some(status) = err.status() {
85 return AppError::Http {
86 status: status.as_u16(),
87 body: err.to_string(),
88 };
89 }
90 AppError::Other(err.to_string())
91 }
92}