use thiserror::Error;
#[allow(clippy::result_large_err)]
#[derive(Debug, Error)]
pub enum XurlError {
#[error("HTTP Error: {0}")]
Http(String),
#[error("IO Error: {0}")]
Io(String),
#[error("Invalid Method: Invalid HTTP method: {0}")]
InvalidMethod(String),
#[error("{body}")]
Api {
status: u16,
body: String,
},
#[error("{0}")]
Validation(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Invalid path parameter {name:?}: value {value:?} contains a reserved character")]
InvalidPathParam {
name: String,
value: String,
},
#[error("Internal error: {0}")]
Internal(String),
#[error("JSON Error: {0}")]
Json(String),
#[error("Auth Error: {0}")]
Auth(String),
#[error("Token Store Error: {0}")]
TokenStore(String),
#[error("envelope-already-emitted")]
EnvelopeAlreadyEmitted {
exit_code: i32,
},
#[error("{}", auth_method_mismatch_message(.endpoint, .rendered_url.as_deref(), .method, .requested.as_deref(), .supported, .available_in_app.as_deref(), .app.as_deref(), .other_apps_with_creds.as_deref()))]
AuthMethodMismatch {
endpoint: String,
rendered_url: Option<String>,
method: String,
requested: Option<String>,
supported: Vec<String>,
available_in_app: Option<Vec<String>>,
app: Option<String>,
other_apps_with_creds: Option<Vec<String>>,
},
}
#[allow(clippy::too_many_arguments)]
fn auth_method_mismatch_message(
endpoint: &str,
rendered_url: Option<&str>,
method: &str,
requested: Option<&str>,
supported: &[String],
available_in_app: Option<&[String]>,
app: Option<&str>,
other_apps_with_creds: Option<&[String]>,
) -> String {
let display_path = rendered_url.unwrap_or(endpoint);
let app_name = app.unwrap_or("the active app");
let suggest_first = |fallback: &str| {
supported
.first()
.map(|s| format!(" Add credentials with: xr auth {s} --app {fallback}."))
.unwrap_or_default()
};
match (requested, available_in_app, other_apps_with_creds) {
(Some(req), _, _) => {
let pretty_req = pretty_scheme(req);
let alt = supported
.iter()
.map(|s| format!("--auth {s}"))
.collect::<Vec<_>>()
.join(" or ");
if alt.is_empty() {
format!("{pretty_req} auth is not accepted at {method} {display_path}.")
} else {
format!("{pretty_req} auth is not accepted at {method} {display_path}. Use {alt}.")
}
}
(None, Some(avail), Some(others)) if avail.is_empty() && !others.is_empty() => {
let alts = others.join(", ");
let accepts = if supported.is_empty() {
"none".to_string()
} else {
supported.join(", ")
};
format!(
"App '{app_name}' has no stored credentials, but other apps do ({alts}). Endpoint {method} {display_path} accepts: {accepts}. Try --app NAME with one of the apps above."
)
}
(None, Some(avail), _) => {
let has = if avail.is_empty() {
"none".to_string()
} else {
avail.join(", ")
};
let accepts = if supported.is_empty() {
"none".to_string()
} else {
supported.join(", ")
};
let suggest = suggest_first(app_name);
format!(
"No stored auth method on app '{app_name}' is accepted at {method} {display_path}. App has: {has}. Endpoint accepts: {accepts}.{suggest}"
)
}
(None, None, _) => {
format!("Auth method is not accepted at {method} {display_path}.")
}
}
}
fn pretty_scheme(name: &str) -> String {
crate::api::auth_matrix::WireScheme::from_wire(name)
.map(|ws| ws.pretty().to_string())
.unwrap_or_else(|| name.to_string())
}
#[allow(dead_code)] impl XurlError {
pub fn api(status: u16, body: impl Into<String>) -> Self {
Self::Api {
status,
body: body.into(),
}
}
pub fn validation(body: impl Into<String>) -> Self {
Self::Validation(body.into())
}
pub fn auth(message: impl Into<String>) -> Self {
Self::Auth(message.into())
}
pub fn auth_with_cause(message: &str, cause: &dyn std::fmt::Display) -> Self {
Self::Auth(format!("{message} (cause: {cause})"))
}
pub fn token_store(message: impl Into<String>) -> Self {
Self::TokenStore(message.into())
}
#[must_use]
pub fn is_api(&self) -> bool {
matches!(self, Self::Api { .. })
}
#[must_use]
pub fn is_validation(&self) -> bool {
matches!(self, Self::Validation(_))
}
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
Self::Auth(_) => "auth-required",
Self::TokenStore(_) => "token-store",
Self::Api { status: 401, .. } => "auth-required",
Self::Api { status: 429, .. } => "rate-limited",
Self::Api { status: 404, .. } => "not-found",
Self::Api { .. } => "network-error",
Self::Http(_) => "network-error",
Self::Io(_) => "io",
Self::Json(_) => "serialization",
Self::InvalidMethod(_) => "invalid-method",
Self::AuthMethodMismatch { .. } => "auth-method-mismatch",
Self::Validation(_) => "validation",
Self::InvalidUrl(_) => "invalid-url",
Self::InvalidPathParam { .. } => "invalid-path-param",
Self::Internal(_) => "internal",
Self::EnvelopeAlreadyEmitted { .. } => "confirmation-required",
}
}
#[must_use]
pub fn exit_code(&self) -> i32 {
match self {
Self::Auth(_) | Self::TokenStore(_) => EXIT_AUTH_REQUIRED,
Self::Api { status: 401, .. } => EXIT_AUTH_REQUIRED,
Self::Api { status: 429, .. } => EXIT_RATE_LIMITED,
Self::Api { status: 404, .. } => EXIT_NOT_FOUND,
Self::Http(msg) if msg.contains("401") || msg.contains("Unauthorized") => {
EXIT_AUTH_REQUIRED
}
Self::Http(msg) if msg.contains("429") => EXIT_RATE_LIMITED,
Self::Http(msg) if msg.contains("404") => EXIT_NOT_FOUND,
Self::Io(_) => EXIT_NETWORK_ERROR,
Self::Validation(_)
| Self::InvalidUrl(_)
| Self::InvalidPathParam { .. }
| Self::Internal(_) => EXIT_GENERAL_ERROR,
Self::AuthMethodMismatch { .. } => EXIT_AUTH_MISMATCH,
Self::EnvelopeAlreadyEmitted { exit_code } => *exit_code,
_ => EXIT_GENERAL_ERROR,
}
}
}
impl From<reqwest::Error> for XurlError {
fn from(err: reqwest::Error) -> Self {
Self::Http(err.to_string())
}
}
impl From<std::io::Error> for XurlError {
fn from(err: std::io::Error) -> Self {
Self::Io(err.to_string())
}
}
impl From<serde_json::Error> for XurlError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err.to_string())
}
}
impl From<serde_yaml::Error> for XurlError {
fn from(err: serde_yaml::Error) -> Self {
Self::Json(err.to_string())
}
}
impl From<url::ParseError> for XurlError {
fn from(err: url::ParseError) -> Self {
Self::Http(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, XurlError>;
#[allow(dead_code)] pub const EXIT_SUCCESS: i32 = 0;
#[allow(dead_code)] pub const EXIT_GENERAL_ERROR: i32 = 1;
#[allow(dead_code)] pub const EXIT_AUTH_MISMATCH: i32 = 2;
#[allow(dead_code)] pub const EXIT_AUTH_REQUIRED: i32 = 77;
#[allow(dead_code)] pub const EXIT_RATE_LIMITED: i32 = 3;
#[allow(dead_code)] pub const EXIT_NOT_FOUND: i32 = 4;
#[allow(dead_code)] pub const EXIT_NETWORK_ERROR: i32 = 5;
#[allow(dead_code)] #[must_use]
pub fn exit_code_for_error(e: &XurlError) -> i32 {
e.exit_code()
}