use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
use std::borrow::Cow;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CliError {
#[error("unknown or missing command")]
UnknownCommand,
#[error("unknown command: {command}")]
UnknownCommandName {
command: String,
next: &'static str,
},
#[error("missing argument: {argument}")]
MissingArgument {
argument: &'static str,
next: &'static str,
},
#[error("missing value for {flag}")]
MissingFlagValue {
flag: &'static str,
next: &'static str,
},
#[error("duplicate flag: {flag}")]
DuplicateFlag {
flag: &'static str,
next: &'static str,
},
#[error("unexpected argument for {command}: {argument}")]
UnexpectedArgument {
argument: String,
command: &'static str,
next: &'static str,
},
#[error("unknown flag: {flag}")]
UnknownFlag {
flag: String,
next: &'static str,
},
#[error("unsupported flag for {command}: {flag}")]
UnsupportedFlag {
flag: String,
command: &'static str,
next: &'static str,
},
#[error("unknown resource: {resource}")]
UnknownResource {
resource: String,
next: &'static str,
},
#[error("unknown issue status: {0}")]
UnknownStatus(String),
#[error("unknown log level: {0}")]
UnknownLogLevel(String),
#[error("invalid limit: {0}")]
InvalidLimit(String),
}
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error(transparent)]
Cli(#[from] CliError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Http(#[from] reqwest::Error),
#[error("not logged in: run logbrew login")]
MissingToken,
#[error("api returned status {status}: {body}")]
Api {
status: u16,
body: String,
auth_source: &'static str,
auth_label: &'static str,
},
#[error("LogBrew API unreachable: {message}")]
StatusUnavailable {
api_url: String,
status_code: Option<u16>,
body: Option<String>,
authenticated: bool,
auth_source: &'static str,
auth_label: &'static str,
message: String,
},
#[error("{message}")]
Unavailable {
message: &'static str,
next: &'static str,
},
}
pub fn write_cli_error<W: std::io::Write>(
error: &CliError,
json: bool,
output: &mut W,
) -> Result<(), std::io::Error> {
if json {
let body = serde_json::json!({
"ok": false,
"error": cli_error_code(error),
"message": error.to_string(),
"next": cli_error_next_step(error),
});
writeln!(output, "{body}")
} else {
writeln!(output, "{error}")?;
writeln!(output, "Next: {}", cli_error_next_step(error))
}
}
pub fn write_runtime_error<W: std::io::Write>(
error: &RuntimeError,
json: bool,
output: &mut W,
) -> Result<(), std::io::Error> {
if json {
let body = runtime_error_json(error);
writeln!(output, "{body}")
} else {
write_human_runtime_error(error, output)
}
}
fn write_human_runtime_error<W: std::io::Write>(
error: &RuntimeError,
output: &mut W,
) -> Result<(), std::io::Error> {
match error {
RuntimeError::StatusUnavailable {
api_url,
status_code,
body,
auth_label,
message,
..
} => {
writeln!(output, "LogBrew API unreachable.")?;
writeln!(output, "API: {api_url}")?;
writeln!(output, "Auth: {auth_label}")?;
if let Some(status_code) = status_code {
writeln!(output, "Status: {status_code}")?;
}
if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
writeln!(output, "Body: {body}")?;
} else {
writeln!(output, "Reason: {message}")?;
}
writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
Ok(())
}
RuntimeError::Api {
status,
body,
auth_label,
..
} => {
let api_details = ApiErrorDetails::parse(body);
writeln!(output, "{error}")?;
if let Some(code) = api_details.code.as_deref() {
writeln!(output, "Code: {code}")?;
}
writeln!(output, "Auth: {auth_label}")?;
writeln!(output, "Next: {}", api_next_step(*status, &api_details))
}
RuntimeError::Cli(_)
| RuntimeError::Io(_)
| RuntimeError::Http(_)
| RuntimeError::MissingToken
| RuntimeError::Unavailable { .. } => {
writeln!(output, "{error}")?;
writeln!(output, "Next: {}", runtime_error_next_step(error))?;
Ok(())
}
}
}
fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
match error {
RuntimeError::MissingToken
| RuntimeError::Unavailable { .. }
| RuntimeError::Io(_)
| RuntimeError::Http(_) => serde_json::json!({
"ok": false,
"error": runtime_error_code(error),
"message": error.to_string(),
"next": runtime_error_next_step(error),
}),
RuntimeError::Api {
status,
body,
auth_source,
..
} => {
let api_details = ApiErrorDetails::parse(body);
let next = api_next_step(*status, &api_details);
serde_json::json!({
"ok": false,
"error": runtime_error_code(error),
"message": error.to_string(),
"status": status,
"body": body,
"api_error": api_details.error.as_deref(),
"api_code": api_details.code.as_deref(),
"api_next": api_details.next.as_deref(),
"auth_source": auth_source,
"next": next,
})
}
RuntimeError::StatusUnavailable {
api_url,
status_code,
body,
authenticated,
auth_source,
message,
..
} => serde_json::json!({
"ok": false,
"error": runtime_error_code(error),
"status": "unreachable",
"status_code": status_code,
"body": body,
"api_url": api_url,
"authenticated": authenticated,
"auth_source": auth_source,
"message": message,
"next": runtime_error_next_step(error),
}),
RuntimeError::Cli(error) => serde_json::json!({
"ok": false,
"error": cli_error_code(error),
"message": error.to_string(),
"next": cli_error_next_step(error),
}),
}
}
const fn cli_error_code(error: &CliError) -> &'static str {
match error {
CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
CliError::MissingArgument { .. } => "missing_argument",
CliError::MissingFlagValue { .. } => "missing_flag_value",
CliError::DuplicateFlag { .. } => "duplicate_flag",
CliError::UnexpectedArgument { .. } => "unexpected_argument",
CliError::UnknownFlag { .. } => "unknown_flag",
CliError::UnsupportedFlag { .. } => "unsupported_flag",
CliError::UnknownResource { .. } => "unknown_resource",
CliError::UnknownStatus(_) => "unknown_status",
CliError::UnknownLogLevel(_) => "unknown_log_level",
CliError::InvalidLimit(_) => "invalid_limit",
}
}
const fn cli_error_next_step(error: &CliError) -> &'static str {
match error {
CliError::InvalidLimit(_) => "use --limit with a positive whole number",
CliError::MissingArgument { next, .. }
| CliError::MissingFlagValue { next, .. }
| CliError::DuplicateFlag { next, .. }
| CliError::UnexpectedArgument { next, .. }
| CliError::UnsupportedFlag { next, .. }
| CliError::UnknownFlag { next, .. }
| CliError::UnknownResource { next, .. }
| CliError::UnknownCommandName { next, .. } => next,
CliError::UnknownCommand => "run logbrew --help",
CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
}
}
const fn runtime_error_code(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Cli(error) => cli_error_code(error),
RuntimeError::Io(_) => "io_error",
RuntimeError::Http(_) => "http_error",
RuntimeError::MissingToken => "not_logged_in",
RuntimeError::Api { .. } => "api_error",
RuntimeError::StatusUnavailable { .. } => "status_unreachable",
RuntimeError::Unavailable { .. } => "unavailable",
}
}
const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ApiErrorDetails {
error: Option<String>,
code: Option<String>,
next: Option<String>,
}
impl ApiErrorDetails {
fn parse(body: &str) -> Self {
let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
return Self::default();
};
Self {
error: json_string_field(&value, "error"),
code: json_string_field(&value, "code"),
next: json_string_field(&value, "next"),
}
}
}
fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
match error {
RuntimeError::Api { status, body, .. } => {
let api_details = ApiErrorDetails::parse(body);
api_next_step(*status, &api_details)
}
RuntimeError::Cli(_)
| RuntimeError::Io(_)
| RuntimeError::Http(_)
| RuntimeError::MissingToken
| RuntimeError::StatusUnavailable { .. }
| RuntimeError::Unavailable { .. } => {
Cow::Borrowed(fallback_runtime_error_next_step(error))
}
}
}
fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
api_details.next.as_ref().map_or_else(
|| Cow::Borrowed(fallback_api_next_step(status)),
|next| Cow::Owned(next.clone()),
)
}
const fn fallback_api_next_step(status: u16) -> &'static str {
match status {
401 | 403 => "run logbrew login",
400 | 422 => "check command arguments or filters",
404 => "check the resource id or filters",
429 => "retry later",
500..=599 => "check LOGBREW_API_URL or retry later",
_ => "check command arguments or retry later",
}
}
const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
match error {
RuntimeError::Cli(error) => cli_error_next_step(error),
RuntimeError::MissingToken => "run logbrew login",
RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
STATUS_UNAVAILABLE_NEXT_STEP
}
RuntimeError::Unavailable { next, .. } => next,
RuntimeError::Io(_) => "check local files and permissions",
}
}