1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4use std::borrow::Cow;
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
8pub enum CliError {
9 #[error("unknown or missing command")]
11 UnknownCommand,
12 #[error("unknown command: {command}")]
14 UnknownCommandName {
15 command: String,
17 next: &'static str,
19 },
20 #[error("missing argument: {argument}")]
22 MissingArgument {
23 argument: &'static str,
25 next: &'static str,
27 },
28 #[error("missing value for {flag}")]
30 MissingFlagValue {
31 flag: &'static str,
33 next: &'static str,
35 },
36 #[error("duplicate flag: {flag}")]
38 DuplicateFlag {
39 flag: &'static str,
41 next: &'static str,
43 },
44 #[error("unexpected argument for {command}: {argument}")]
46 UnexpectedArgument {
47 argument: String,
49 command: &'static str,
51 next: &'static str,
53 },
54 #[error("unknown flag: {flag}")]
56 UnknownFlag {
57 flag: String,
59 next: &'static str,
61 },
62 #[error("unsupported flag for {command}: {flag}")]
64 UnsupportedFlag {
65 flag: String,
67 command: &'static str,
69 next: &'static str,
71 },
72 #[error("unknown resource: {resource}")]
74 UnknownResource {
75 resource: String,
77 next: &'static str,
79 },
80 #[error("unknown issue status: {0}")]
82 UnknownStatus(String),
83 #[error("unknown log level: {0}")]
85 UnknownLogLevel(String),
86 #[error("invalid limit: {0}")]
88 InvalidLimit(String),
89 #[error("invalid setup source: {0}")]
91 InvalidSetupSource(String),
92}
93
94#[derive(Debug, thiserror::Error)]
96pub enum RuntimeError {
97 #[error(transparent)]
99 Cli(#[from] CliError),
100 #[error(transparent)]
102 Io(#[from] std::io::Error),
103 #[error(transparent)]
105 Http(#[from] reqwest::Error),
106 #[error("not logged in: run logbrew login")]
108 MissingToken,
109 #[error("api returned status {status}: {body}")]
111 Api {
112 status: u16,
114 body: String,
116 auth_source: &'static str,
118 auth_label: &'static str,
120 },
121 #[error("LogBrew API unreachable: {message}")]
123 StatusUnavailable {
124 api_url: String,
126 status_code: Option<u16>,
128 body: Option<String>,
130 authenticated: bool,
132 auth_source: &'static str,
134 auth_label: &'static str,
136 message: String,
138 },
139 #[error("{message}")]
141 Unavailable {
142 message: &'static str,
144 next: &'static str,
146 },
147}
148
149pub fn write_cli_error<W: std::io::Write>(
155 error: &CliError,
156 json: bool,
157 output: &mut W,
158) -> Result<(), std::io::Error> {
159 if json {
160 let body = serde_json::json!({
161 "ok": false,
162 "error": cli_error_code(error),
163 "message": error.to_string(),
164 "next": cli_error_next_step(error),
165 });
166 writeln!(output, "{body}")
167 } else {
168 writeln!(output, "{error}")?;
169 writeln!(output, "Next: {}", cli_error_next_step(error))
170 }
171}
172
173pub fn write_runtime_error<W: std::io::Write>(
179 error: &RuntimeError,
180 json: bool,
181 output: &mut W,
182) -> Result<(), std::io::Error> {
183 if json {
184 let body = runtime_error_json(error);
185 writeln!(output, "{body}")
186 } else {
187 write_human_runtime_error(error, output)
188 }
189}
190
191fn write_human_runtime_error<W: std::io::Write>(
193 error: &RuntimeError,
194 output: &mut W,
195) -> Result<(), std::io::Error> {
196 match error {
197 RuntimeError::StatusUnavailable {
198 api_url,
199 status_code,
200 body,
201 auth_label,
202 message,
203 ..
204 } => {
205 writeln!(output, "LogBrew API unreachable.")?;
206 writeln!(output, "API: {api_url}")?;
207 writeln!(output, "Auth: {auth_label}")?;
208 if let Some(status_code) = status_code {
209 writeln!(output, "Status: {status_code}")?;
210 }
211 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
212 writeln!(output, "Body: {body}")?;
213 } else {
214 writeln!(output, "Reason: {message}")?;
215 }
216 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
217 Ok(())
218 }
219 RuntimeError::Api {
220 status,
221 body,
222 auth_label,
223 ..
224 } => {
225 let api_details = ApiErrorDetails::parse(body);
226 writeln!(output, "{error}")?;
227 if let Some(code) = api_details.code.as_deref() {
228 writeln!(output, "Code: {code}")?;
229 }
230 writeln!(output, "Auth: {auth_label}")?;
231 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
232 }
233 RuntimeError::Cli(_)
234 | RuntimeError::Io(_)
235 | RuntimeError::Http(_)
236 | RuntimeError::MissingToken
237 | RuntimeError::Unavailable { .. } => {
238 writeln!(output, "{error}")?;
239 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
240 Ok(())
241 }
242 }
243}
244
245fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
247 match error {
248 RuntimeError::MissingToken
249 | RuntimeError::Unavailable { .. }
250 | RuntimeError::Io(_)
251 | RuntimeError::Http(_) => serde_json::json!({
252 "ok": false,
253 "error": runtime_error_code(error),
254 "message": error.to_string(),
255 "next": runtime_error_next_step(error),
256 }),
257 RuntimeError::Api {
258 status,
259 body,
260 auth_source,
261 ..
262 } => {
263 let api_details = ApiErrorDetails::parse(body);
264 let next = api_next_step(*status, &api_details);
265 serde_json::json!({
266 "ok": false,
267 "error": runtime_error_code(error),
268 "message": error.to_string(),
269 "status": status,
270 "body": body,
271 "api_error": api_details.error.as_deref(),
272 "api_code": api_details.code.as_deref(),
273 "api_next": api_details.next.as_deref(),
274 "auth_source": auth_source,
275 "next": next,
276 })
277 }
278 RuntimeError::StatusUnavailable {
279 api_url,
280 status_code,
281 body,
282 authenticated,
283 auth_source,
284 message,
285 ..
286 } => serde_json::json!({
287 "ok": false,
288 "error": runtime_error_code(error),
289 "status": "unreachable",
290 "status_code": status_code,
291 "body": body,
292 "api_url": api_url,
293 "authenticated": authenticated,
294 "auth_source": auth_source,
295 "message": message,
296 "next": runtime_error_next_step(error),
297 }),
298 RuntimeError::Cli(error) => serde_json::json!({
299 "ok": false,
300 "error": cli_error_code(error),
301 "message": error.to_string(),
302 "next": cli_error_next_step(error),
303 }),
304 }
305}
306
307const fn cli_error_code(error: &CliError) -> &'static str {
309 match error {
310 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
311 CliError::MissingArgument { .. } => "missing_argument",
312 CliError::MissingFlagValue { .. } => "missing_flag_value",
313 CliError::DuplicateFlag { .. } => "duplicate_flag",
314 CliError::UnexpectedArgument { .. } => "unexpected_argument",
315 CliError::UnknownFlag { .. } => "unknown_flag",
316 CliError::UnsupportedFlag { .. } => "unsupported_flag",
317 CliError::UnknownResource { .. } => "unknown_resource",
318 CliError::UnknownStatus(_) => "unknown_status",
319 CliError::UnknownLogLevel(_) => "unknown_log_level",
320 CliError::InvalidLimit(_) => "invalid_limit",
321 CliError::InvalidSetupSource(_) => "invalid_setup_source",
322 }
323}
324
325const fn cli_error_next_step(error: &CliError) -> &'static str {
327 match error {
328 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
329 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
330 CliError::MissingArgument { next, .. }
331 | CliError::MissingFlagValue { next, .. }
332 | CliError::DuplicateFlag { next, .. }
333 | CliError::UnexpectedArgument { next, .. }
334 | CliError::UnsupportedFlag { next, .. }
335 | CliError::UnknownFlag { next, .. }
336 | CliError::UnknownResource { next, .. }
337 | CliError::UnknownCommandName { next, .. } => next,
338 CliError::UnknownCommand => "run logbrew --help",
339 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
340 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
341 }
342}
343
344const fn runtime_error_code(error: &RuntimeError) -> &'static str {
346 match error {
347 RuntimeError::Cli(error) => cli_error_code(error),
348 RuntimeError::Io(_) => "io_error",
349 RuntimeError::Http(_) => "http_error",
350 RuntimeError::MissingToken => "not_logged_in",
351 RuntimeError::Api { .. } => "api_error",
352 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
353 RuntimeError::Unavailable { .. } => "unavailable",
354 }
355}
356
357const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
359
360#[derive(Debug, Clone, Default, PartialEq, Eq)]
362struct ApiErrorDetails {
363 error: Option<String>,
365 code: Option<String>,
367 next: Option<String>,
369}
370
371impl ApiErrorDetails {
372 fn parse(body: &str) -> Self {
374 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
375 return Self::default();
376 };
377
378 Self {
379 error: json_string_field(&value, "error"),
380 code: json_string_field(&value, "code"),
381 next: json_string_field(&value, "next"),
382 }
383 }
384}
385
386fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
388 value
389 .get(key)
390 .and_then(serde_json::Value::as_str)
391 .map(str::trim)
392 .filter(|value| !value.is_empty())
393 .map(ToOwned::to_owned)
394}
395
396fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
398 match error {
399 RuntimeError::Api { status, body, .. } => {
400 let api_details = ApiErrorDetails::parse(body);
401 api_next_step(*status, &api_details)
402 }
403 RuntimeError::Cli(_)
404 | RuntimeError::Io(_)
405 | RuntimeError::Http(_)
406 | RuntimeError::MissingToken
407 | RuntimeError::StatusUnavailable { .. }
408 | RuntimeError::Unavailable { .. } => {
409 Cow::Borrowed(fallback_runtime_error_next_step(error))
410 }
411 }
412}
413
414fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
416 api_details.next.as_ref().map_or_else(
417 || Cow::Borrowed(fallback_api_next_step(status)),
418 |next| Cow::Owned(next.clone()),
419 )
420}
421
422const fn fallback_api_next_step(status: u16) -> &'static str {
424 match status {
425 401 | 403 => "run logbrew login",
426 400 | 422 => "check command arguments or filters",
427 404 => "check the resource id or filters",
428 429 => "retry later",
429 500..=599 => "check LOGBREW_API_URL or retry later",
430 _ => "check command arguments or retry later",
431 }
432}
433
434const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
436 match error {
437 RuntimeError::Cli(error) => cli_error_next_step(error),
438 RuntimeError::MissingToken => "run logbrew login",
439 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
440 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
441 STATUS_UNAVAILABLE_NEXT_STEP
442 }
443 RuntimeError::Unavailable { next, .. } => next,
444 RuntimeError::Io(_) => "check local files and permissions",
445 }
446}