aperture_cli/cli/
errors.rs1use crate::constants;
17use crate::error::Error;
18
19pub fn print_error_with_json(error: &Error, json_format: bool) {
21 if !json_format {
22 print_error(error);
23 return;
24 }
25 let json_error = error.to_json();
26 let Ok(json_output) = serde_json::to_string_pretty(&json_error) else {
27 print_error(error);
28 return;
29 };
30 eprintln!("{json_output}");
31}
32
33pub fn print_error(error: &Error) {
35 write_error(error, &mut std::io::stderr());
36}
37
38#[allow(clippy::too_many_lines)]
43fn write_error<W: std::io::Write>(error: &Error, writer: &mut W) {
44 match error {
45 Error::Internal {
46 kind,
47 message,
48 context,
49 } => {
50 let _ = writeln!(writer, "{kind}: {message}");
51 let Some(ctx) = context else { return };
52 if let Some(suggestion) = &ctx.suggestion {
53 let _ = writeln!(writer, "\nHint: {suggestion}");
54 }
55 }
56 Error::Io(io_err) => match io_err.kind() {
57 std::io::ErrorKind::NotFound => {
58 let _ = writeln!(
59 writer,
60 "File Not Found\n{io_err}\n\nHint: {}",
61 constants::ERR_FILE_NOT_FOUND
62 );
63 }
64 std::io::ErrorKind::PermissionDenied => {
65 let _ = writeln!(
66 writer,
67 "Permission Denied\n{io_err}\n\nHint: {}",
68 constants::ERR_PERMISSION
69 );
70 }
71 _ => {
72 let _ = writeln!(writer, "File System Error\n{io_err}");
73 }
74 },
75 Error::Network(req_err) => {
76 if req_err.is_connect() {
77 let _ = writeln!(
78 writer,
79 "Connection Error\n{req_err}\n\nHint: {}",
80 constants::ERR_CONNECTION
81 );
82 return;
83 }
84 if req_err.is_timeout() {
85 let _ = writeln!(
86 writer,
87 "Timeout Error\n{req_err}\n\nHint: {}",
88 constants::ERR_TIMEOUT
89 );
90 return;
91 }
92 if !req_err.is_status() {
93 let _ = writeln!(writer, "Network Error\n{req_err}");
94 return;
95 }
96 let Some(status) = req_err.status() else {
97 let _ = writeln!(writer, "Network Error\n{req_err}");
98 return;
99 };
100 match status.as_u16() {
101 401 => {
102 let _ = writeln!(
103 writer,
104 "Authentication Error\n{req_err}\n\nHint: {}",
105 constants::ERR_API_CREDENTIALS
106 );
107 }
108 403 => {
109 let _ = writeln!(
110 writer,
111 "Permission Error\n{req_err}\n\nHint: {}",
112 constants::ERR_PERMISSION_DENIED
113 );
114 }
115 404 => {
116 let _ = writeln!(
117 writer,
118 "Not Found Error\n{req_err}\n\nHint: {}",
119 constants::ERR_ENDPOINT_NOT_FOUND
120 );
121 }
122 429 => {
123 let _ = writeln!(
124 writer,
125 "Rate Limited\n{req_err}\n\nHint: {}",
126 constants::ERR_RATE_LIMITED
127 );
128 }
129 500..=599 => {
130 let _ = writeln!(
131 writer,
132 "Server Error\n{req_err}\n\nHint: {}",
133 constants::ERR_SERVER_ERROR
134 );
135 }
136 _ => {
137 let _ = writeln!(writer, "HTTP Error\n{req_err}");
138 }
139 }
140 }
141 Error::Yaml(yaml_err) => {
142 let _ = writeln!(
143 writer,
144 "YAML Parsing Error\n{yaml_err}\n\nHint: {}",
145 constants::ERR_YAML_SYNTAX
146 );
147 }
148 Error::Json(json_err) => {
149 let _ = writeln!(
150 writer,
151 "JSON Parsing Error\n{json_err}\n\nHint: {}",
152 constants::ERR_JSON_SYNTAX
153 );
154 }
155 Error::Toml(toml_err) => {
156 let _ = writeln!(
157 writer,
158 "TOML Parsing Error\n{toml_err}\n\nHint: {}",
159 constants::ERR_TOML_SYNTAX
160 );
161 }
162 Error::Anyhow(anyhow_err) => {
163 let _ = writeln!(writer, "Error\n{anyhow_err}");
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use std::time::Duration;
172 use wiremock::matchers::{method, path};
173 use wiremock::{Mock, MockServer, ResponseTemplate};
174
175 fn capture(error: &Error) -> String {
176 let mut buf = Vec::new();
177 write_error(error, &mut buf);
178 String::from_utf8(buf).expect("output is valid UTF-8")
179 }
180
181 #[test]
184 fn test_internal_without_suggestion() {
185 let err = Error::validation_error("bad input");
186 let out = capture(&err);
187 assert!(out.contains("Validation"));
188 assert!(out.contains("bad input"));
189 }
190
191 #[test]
192 fn test_internal_with_suggestion() {
193 let err = Error::spec_not_found("my-api");
194 let out = capture(&err);
195 assert!(out.contains("Specification"));
196 assert!(out.contains("my-api"));
197 assert!(out.contains("Hint:"));
198 }
199
200 #[test]
201 fn test_io_not_found() {
202 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
203 let err = Error::Io(io_err);
204 let out = capture(&err);
205 assert!(out.contains("File Not Found"));
206 assert!(out.contains(constants::ERR_FILE_NOT_FOUND));
207 }
208
209 #[test]
210 fn test_io_permission_denied() {
211 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
212 let err = Error::Io(io_err);
213 let out = capture(&err);
214 assert!(out.contains("Permission Denied"));
215 assert!(out.contains(constants::ERR_PERMISSION));
216 }
217
218 #[test]
219 fn test_io_other() {
220 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
221 let err = Error::Io(io_err);
222 let out = capture(&err);
223 assert!(out.contains("File System Error"));
224 }
225
226 #[test]
227 fn test_yaml_error() {
228 let yaml_err = serde_yaml::from_str::<serde_yaml::Value>("key: - value").unwrap_err();
229 let err = Error::Yaml(yaml_err);
230 let out = capture(&err);
231 assert!(out.contains("YAML Parsing Error"));
232 assert!(out.contains(constants::ERR_YAML_SYNTAX));
233 }
234
235 #[test]
236 fn test_json_error() {
237 let json_err = serde_json::from_str::<serde_json::Value>("{bad").unwrap_err();
238 let err = Error::Json(json_err);
239 let out = capture(&err);
240 assert!(out.contains("JSON Parsing Error"));
241 assert!(out.contains(constants::ERR_JSON_SYNTAX));
242 }
243
244 #[test]
245 fn test_toml_error() {
246 let toml_err = toml::from_str::<toml::Value>("key = ").unwrap_err();
247 let err = Error::Toml(toml_err);
248 let out = capture(&err);
249 assert!(out.contains("TOML Parsing Error"));
250 assert!(out.contains(constants::ERR_TOML_SYNTAX));
251 }
252
253 #[test]
254 fn test_anyhow_error() {
255 let err = Error::Anyhow(anyhow::anyhow!("something went wrong"));
256 let out = capture(&err);
257 assert!(out.contains("Error"));
258 assert!(out.contains("something went wrong"));
259 }
260
261 async fn status_error(status: u16) -> reqwest::Error {
266 let server = MockServer::start().await;
267 Mock::given(method("GET"))
268 .and(path("/err"))
269 .respond_with(ResponseTemplate::new(status))
270 .mount(&server)
271 .await;
272
273 reqwest::Client::new()
274 .get(format!("{}/err", server.uri()))
275 .send()
276 .await
277 .expect("request reached mock server")
278 .error_for_status()
279 .expect_err("status >= 400 must produce an error")
280 }
281
282 #[tokio::test]
283 async fn test_network_connect_error() {
284 let req_err = reqwest::Client::new()
286 .get("http://127.0.0.1:1/")
287 .send()
288 .await
289 .expect_err("port 1 must refuse connections");
290 assert!(req_err.is_connect(), "expected a connect error");
291 let out = capture(&Error::Network(req_err));
292 assert!(out.contains("Connection Error"));
293 assert!(out.contains(constants::ERR_CONNECTION));
294 }
295
296 #[tokio::test]
297 async fn test_network_timeout_error() {
298 let server = MockServer::start().await;
299 Mock::given(method("GET"))
300 .and(path("/slow"))
301 .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(10)))
302 .mount(&server)
303 .await;
304
305 let req_err = reqwest::Client::builder()
306 .timeout(Duration::from_millis(1))
307 .build()
308 .unwrap()
309 .get(format!("{}/slow", server.uri()))
310 .send()
311 .await
312 .expect_err("request must time out");
313 assert!(req_err.is_timeout(), "expected a timeout error");
314 let out = capture(&Error::Network(req_err));
315 assert!(out.contains("Timeout Error"));
316 assert!(out.contains(constants::ERR_TIMEOUT));
317 }
318
319 #[tokio::test]
320 async fn test_network_401() {
321 let err = status_error(401).await;
322 let out = capture(&Error::Network(err));
323 assert!(out.contains("Authentication Error"));
324 assert!(out.contains(constants::ERR_API_CREDENTIALS));
325 }
326
327 #[tokio::test]
328 async fn test_network_403() {
329 let err = status_error(403).await;
330 let out = capture(&Error::Network(err));
331 assert!(out.contains("Permission Error"));
332 assert!(out.contains(constants::ERR_PERMISSION_DENIED));
333 }
334
335 #[tokio::test]
336 async fn test_network_404() {
337 let err = status_error(404).await;
338 let out = capture(&Error::Network(err));
339 assert!(out.contains("Not Found Error"));
340 assert!(out.contains(constants::ERR_ENDPOINT_NOT_FOUND));
341 }
342
343 #[tokio::test]
344 async fn test_network_429() {
345 let err = status_error(429).await;
346 let out = capture(&Error::Network(err));
347 assert!(out.contains("Rate Limited"));
348 assert!(out.contains(constants::ERR_RATE_LIMITED));
349 }
350
351 #[tokio::test]
352 async fn test_network_503() {
353 let err = status_error(503).await;
354 let out = capture(&Error::Network(err));
355 assert!(out.contains("Server Error"));
356 assert!(out.contains(constants::ERR_SERVER_ERROR));
357 }
358
359 #[tokio::test]
362 async fn test_network_400_fallback() {
363 let err = status_error(400).await;
364 let out = capture(&Error::Network(err));
365 assert!(out.contains("HTTP Error"));
366 }
367}