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}
90
91#[derive(Debug, thiserror::Error)]
93pub enum RuntimeError {
94 #[error(transparent)]
96 Cli(#[from] CliError),
97 #[error(transparent)]
99 Io(#[from] std::io::Error),
100 #[error(transparent)]
102 Http(#[from] reqwest::Error),
103 #[error("not logged in: run logbrew login")]
105 MissingToken,
106 #[error("api returned status {status}: {body}")]
108 Api {
109 status: u16,
111 body: String,
113 auth_source: &'static str,
115 auth_label: &'static str,
117 },
118 #[error("LogBrew API unreachable: {message}")]
120 StatusUnavailable {
121 api_url: String,
123 status_code: Option<u16>,
125 body: Option<String>,
127 authenticated: bool,
129 auth_source: &'static str,
131 auth_label: &'static str,
133 message: String,
135 },
136 #[error("{message}")]
138 Unavailable {
139 message: &'static str,
141 next: &'static str,
143 },
144}
145
146pub fn write_cli_error<W: std::io::Write>(
152 error: &CliError,
153 json: bool,
154 output: &mut W,
155) -> Result<(), std::io::Error> {
156 if json {
157 let body = serde_json::json!({
158 "ok": false,
159 "error": cli_error_code(error),
160 "message": error.to_string(),
161 "next": cli_error_next_step(error),
162 });
163 writeln!(output, "{body}")
164 } else {
165 writeln!(output, "{error}")?;
166 writeln!(output, "Next: {}", cli_error_next_step(error))
167 }
168}
169
170pub fn write_runtime_error<W: std::io::Write>(
176 error: &RuntimeError,
177 json: bool,
178 output: &mut W,
179) -> Result<(), std::io::Error> {
180 if json {
181 let body = runtime_error_json(error);
182 writeln!(output, "{body}")
183 } else {
184 write_human_runtime_error(error, output)
185 }
186}
187
188fn write_human_runtime_error<W: std::io::Write>(
190 error: &RuntimeError,
191 output: &mut W,
192) -> Result<(), std::io::Error> {
193 match error {
194 RuntimeError::StatusUnavailable {
195 api_url,
196 status_code,
197 body,
198 auth_label,
199 message,
200 ..
201 } => {
202 writeln!(output, "LogBrew API unreachable.")?;
203 writeln!(output, "API: {api_url}")?;
204 writeln!(output, "Auth: {auth_label}")?;
205 if let Some(status_code) = status_code {
206 writeln!(output, "Status: {status_code}")?;
207 }
208 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
209 writeln!(output, "Body: {body}")?;
210 } else {
211 writeln!(output, "Reason: {message}")?;
212 }
213 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
214 Ok(())
215 }
216 RuntimeError::Api {
217 status,
218 body,
219 auth_label,
220 ..
221 } => {
222 let api_details = ApiErrorDetails::parse(body);
223 writeln!(output, "{error}")?;
224 if let Some(code) = api_details.code.as_deref() {
225 writeln!(output, "Code: {code}")?;
226 }
227 writeln!(output, "Auth: {auth_label}")?;
228 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
229 }
230 RuntimeError::Cli(_)
231 | RuntimeError::Io(_)
232 | RuntimeError::Http(_)
233 | RuntimeError::MissingToken
234 | RuntimeError::Unavailable { .. } => {
235 writeln!(output, "{error}")?;
236 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
237 Ok(())
238 }
239 }
240}
241
242fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
244 match error {
245 RuntimeError::MissingToken
246 | RuntimeError::Unavailable { .. }
247 | RuntimeError::Io(_)
248 | RuntimeError::Http(_) => serde_json::json!({
249 "ok": false,
250 "error": runtime_error_code(error),
251 "message": error.to_string(),
252 "next": runtime_error_next_step(error),
253 }),
254 RuntimeError::Api {
255 status,
256 body,
257 auth_source,
258 ..
259 } => {
260 let api_details = ApiErrorDetails::parse(body);
261 let next = api_next_step(*status, &api_details);
262 serde_json::json!({
263 "ok": false,
264 "error": runtime_error_code(error),
265 "message": error.to_string(),
266 "status": status,
267 "body": body,
268 "api_error": api_details.error.as_deref(),
269 "api_code": api_details.code.as_deref(),
270 "api_next": api_details.next.as_deref(),
271 "auth_source": auth_source,
272 "next": next,
273 })
274 }
275 RuntimeError::StatusUnavailable {
276 api_url,
277 status_code,
278 body,
279 authenticated,
280 auth_source,
281 message,
282 ..
283 } => serde_json::json!({
284 "ok": false,
285 "error": runtime_error_code(error),
286 "status": "unreachable",
287 "status_code": status_code,
288 "body": body,
289 "api_url": api_url,
290 "authenticated": authenticated,
291 "auth_source": auth_source,
292 "message": message,
293 "next": runtime_error_next_step(error),
294 }),
295 RuntimeError::Cli(error) => serde_json::json!({
296 "ok": false,
297 "error": cli_error_code(error),
298 "message": error.to_string(),
299 "next": cli_error_next_step(error),
300 }),
301 }
302}
303
304const fn cli_error_code(error: &CliError) -> &'static str {
306 match error {
307 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
308 CliError::MissingArgument { .. } => "missing_argument",
309 CliError::MissingFlagValue { .. } => "missing_flag_value",
310 CliError::DuplicateFlag { .. } => "duplicate_flag",
311 CliError::UnexpectedArgument { .. } => "unexpected_argument",
312 CliError::UnknownFlag { .. } => "unknown_flag",
313 CliError::UnsupportedFlag { .. } => "unsupported_flag",
314 CliError::UnknownResource { .. } => "unknown_resource",
315 CliError::UnknownStatus(_) => "unknown_status",
316 CliError::UnknownLogLevel(_) => "unknown_log_level",
317 CliError::InvalidLimit(_) => "invalid_limit",
318 }
319}
320
321const fn cli_error_next_step(error: &CliError) -> &'static str {
323 match error {
324 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
325 CliError::MissingArgument { next, .. }
326 | CliError::MissingFlagValue { next, .. }
327 | CliError::DuplicateFlag { next, .. }
328 | CliError::UnexpectedArgument { next, .. }
329 | CliError::UnsupportedFlag { next, .. }
330 | CliError::UnknownFlag { next, .. }
331 | CliError::UnknownResource { next, .. }
332 | CliError::UnknownCommandName { next, .. } => next,
333 CliError::UnknownCommand => "run logbrew --help",
334 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
335 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
336 }
337}
338
339const fn runtime_error_code(error: &RuntimeError) -> &'static str {
341 match error {
342 RuntimeError::Cli(error) => cli_error_code(error),
343 RuntimeError::Io(_) => "io_error",
344 RuntimeError::Http(_) => "http_error",
345 RuntimeError::MissingToken => "not_logged_in",
346 RuntimeError::Api { .. } => "api_error",
347 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
348 RuntimeError::Unavailable { .. } => "unavailable",
349 }
350}
351
352const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
354
355#[derive(Debug, Clone, Default, PartialEq, Eq)]
357struct ApiErrorDetails {
358 error: Option<String>,
360 code: Option<String>,
362 next: Option<String>,
364}
365
366impl ApiErrorDetails {
367 fn parse(body: &str) -> Self {
369 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
370 return Self::default();
371 };
372
373 Self {
374 error: json_string_field(&value, "error"),
375 code: json_string_field(&value, "code"),
376 next: json_string_field(&value, "next"),
377 }
378 }
379}
380
381fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
383 value
384 .get(key)
385 .and_then(serde_json::Value::as_str)
386 .map(str::trim)
387 .filter(|value| !value.is_empty())
388 .map(ToOwned::to_owned)
389}
390
391fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
393 match error {
394 RuntimeError::Api { status, body, .. } => {
395 let api_details = ApiErrorDetails::parse(body);
396 api_next_step(*status, &api_details)
397 }
398 RuntimeError::Cli(_)
399 | RuntimeError::Io(_)
400 | RuntimeError::Http(_)
401 | RuntimeError::MissingToken
402 | RuntimeError::StatusUnavailable { .. }
403 | RuntimeError::Unavailable { .. } => {
404 Cow::Borrowed(fallback_runtime_error_next_step(error))
405 }
406 }
407}
408
409fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
411 api_details.next.as_ref().map_or_else(
412 || Cow::Borrowed(fallback_api_next_step(status)),
413 |next| Cow::Owned(next.clone()),
414 )
415}
416
417const fn fallback_api_next_step(status: u16) -> &'static str {
419 match status {
420 401 | 403 => "run logbrew login",
421 400 | 422 => "check command arguments or filters",
422 404 => "check the resource id or filters",
423 429 => "retry later",
424 500..=599 => "check LOGBREW_API_URL or retry later",
425 _ => "check command arguments or retry later",
426 }
427}
428
429const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
431 match error {
432 RuntimeError::Cli(error) => cli_error_next_step(error),
433 RuntimeError::MissingToken => "run logbrew login",
434 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
435 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
436 STATUS_UNAVAILABLE_NEXT_STEP
437 }
438 RuntimeError::Unavailable { next, .. } => next,
439 RuntimeError::Io(_) => "check local files and permissions",
440 }
441}