whisper_macos_cli/
error.rs1use std::process::ExitCode;
2
3#[derive(Debug, thiserror::Error)]
4pub enum Error {
5 #[error("no audio input provided")]
6 NoInput,
7
8 #[error("input not found: {path}")]
9 InputNotFound { path: String },
10
11 #[error("audio decode failed: {0}")]
12 AudioDecode(#[source] anyhow::Error),
13
14 #[error("unsupported audio format: {format}")]
15 UnsupportedFormat { format: String },
16
17 #[error("model not found: {name}")]
18 ModelNotFound { name: String },
19
20 #[error("model download failed: {0}")]
21 ModelDownload(#[source] anyhow::Error),
22
23 #[error("whisper inference failed: {0}")]
24 WhisperInference(String),
25
26 #[error("unsupported platform: macOS with Apple Silicon required")]
27 UnsupportedPlatform,
28
29 #[error("io error: {0}")]
30 Io(#[from] std::io::Error),
31
32 #[error("configuration error: {0}")]
33 Config(String),
34}
35
36impl Error {
37 pub fn exit_code(&self) -> u8 {
38 match self {
39 Self::NoInput => 64,
40 Self::InputNotFound { .. } => 66,
41 Self::AudioDecode(_) => 65,
42 Self::UnsupportedFormat { .. } => 65,
43 Self::ModelNotFound { .. } => 78,
44 Self::ModelDownload(_) => 69,
45 Self::WhisperInference(_) => 70,
46 Self::UnsupportedPlatform => 69,
47 Self::Io(_) => 74,
48 Self::Config(_) => 78,
49 }
50 }
51
52 pub fn to_exit_code(&self) -> ExitCode {
53 ExitCode::from(self.exit_code())
54 }
55
56 pub fn category(&self) -> &'static str {
57 match self {
58 Self::NoInput => "usage",
59 Self::InputNotFound { .. } => "input",
60 Self::AudioDecode(_) | Self::UnsupportedFormat { .. } => "data",
61 Self::ModelNotFound { .. } | Self::Config(_) => "config",
62 Self::ModelDownload(_) | Self::UnsupportedPlatform => "service",
63 Self::WhisperInference(_) => "internal",
64 Self::Io(_) => "io",
65 }
66 }
67
68 pub fn retryable(&self) -> bool {
69 matches!(self, Self::ModelDownload(_))
70 }
71
72 pub fn retry_after_ms(&self) -> Option<u64> {
73 match self {
74 Self::ModelDownload(_) => Some(2000),
75 _ => None,
76 }
77 }
78
79 pub fn hint(&self) -> Option<&'static str> {
80 match self {
81 Self::NoInput => Some("provide audio file(s) as arguments or pipe via stdin"),
82 Self::InputNotFound { .. } => Some("check the file path and try again"),
83 Self::AudioDecode(_) => {
84 Some("verify the file is a valid audio format (ogg, mp3, wav, flac)")
85 }
86 Self::UnsupportedFormat { .. } => Some("use --input-format to force a specific codec"),
87 Self::ModelNotFound { .. } => {
88 Some("run 'whisper-macos-cli models list' to see available models")
89 }
90 Self::ModelDownload(_) => Some("check network connectivity and retry"),
91 Self::WhisperInference(_) => Some("try a smaller model with --model base"),
92 Self::UnsupportedPlatform => Some("this CLI requires macOS with Apple Silicon (M1+)"),
93 Self::Io(_) => None,
94 Self::Config(_) => Some("run 'whisper-macos-cli doctor' to diagnose"),
95 }
96 }
97
98 pub fn docs_url(&self) -> &'static str {
99 match self {
100 Self::NoInput => {
101 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/AGENTS.md#contract"
102 }
103 Self::InputNotFound { .. } => {
104 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md"
105 }
106 Self::AudioDecode(_) => {
107 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md#audio-decode"
108 }
109 Self::UnsupportedFormat { .. } => {
110 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md#unsupported-format"
111 }
112 Self::ModelNotFound { .. } => {
113 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/AGENTS.md#model-management"
114 }
115 Self::ModelDownload(_) => {
116 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md#model-download"
117 }
118 Self::WhisperInference(_) => {
119 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md#inference"
120 }
121 Self::UnsupportedPlatform => {
122 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/README.md#platform-requirements"
123 }
124 Self::Io(_) => {
125 "https://github.com/daniloaguiarbr/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md"
126 }
127 Self::Config(_) => {
128 "https://github.com/daniloteixeira/Dropbox/ai/dev/rust/macos/whisper-macos-cli/blob/main/docs/TROUBLESHOOTING.md"
129 }
130 }
131 }
132
133 pub fn to_json(&self, correlation_id: &str) -> serde_json::Value {
134 serde_json::json!({
135 "schema_version": env!("CARGO_PKG_VERSION"),
136 "error": true,
137 "code": self.exit_code(),
138 "message": self.to_string(),
139 "category": self.category(),
140 "retryable": self.retryable(),
141 "retry_after_ms": self.retry_after_ms(),
142 "hint": self.hint(),
143 "docs_url": self.docs_url(),
144 "correlation_id": correlation_id,
145 })
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn no_input_exit_code_is_64() {
155 assert_eq!(Error::NoInput.exit_code(), 64);
156 }
157
158 #[test]
159 fn input_not_found_exit_code_is_66() {
160 let err = Error::InputNotFound {
161 path: "test.mp3".to_string(),
162 };
163 assert_eq!(err.exit_code(), 66);
164 }
165
166 #[test]
167 fn model_not_found_exit_code_is_78() {
168 let err = Error::ModelNotFound {
169 name: "unknown".to_string(),
170 };
171 assert_eq!(err.exit_code(), 78);
172 }
173
174 #[test]
175 fn error_json_contains_all_required_fields() {
176 let err = Error::ModelDownload(anyhow::anyhow!("HTTP 503"));
177 let json = err.to_json("test-corr-id");
178 assert_eq!(json["error"], true);
179 assert!(json["code"].is_number());
180 assert!(json["message"].is_string());
181 assert!(json["category"].is_string());
182 assert!(json["retryable"].is_boolean());
183 assert!(json["retry_after_ms"].is_number());
184 assert!(json["docs_url"].is_string());
185 assert_eq!(json["correlation_id"], "test-corr-id");
186 }
187}