use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
InvalidInput,
AuthMissing,
AuthExpired,
HydraExpired,
CdpUnavailable,
UrlNotAllowed,
OrderingDisabled,
ConfirmationRequired,
QuoteChanged,
CartInvalid,
AddressMissing,
NotFound,
RateLimited,
OtpInvalid,
CaptchaRequired,
LoginExpired,
Api,
Network,
Io,
Parse,
}
impl ErrorKind {
pub fn as_str(self) -> &'static str {
match self {
Self::InvalidInput => "invalid_input",
Self::AuthMissing => "auth_missing",
Self::AuthExpired => "auth_expired",
Self::HydraExpired => "hydra_expired",
Self::CdpUnavailable => "cdp_unavailable",
Self::UrlNotAllowed => "url_not_allowed",
Self::OrderingDisabled => "ordering_disabled",
Self::ConfirmationRequired => "confirmation_required",
Self::QuoteChanged => "quote_changed",
Self::CartInvalid => "cart_invalid",
Self::AddressMissing => "address_missing",
Self::NotFound => "not_found",
Self::RateLimited => "rate_limited",
Self::OtpInvalid => "otp_invalid",
Self::CaptchaRequired => "captcha_required",
Self::LoginExpired => "login_expired",
Self::Api => "api",
Self::Network => "network",
Self::Io => "io",
Self::Parse => "parse",
}
}
pub fn exit_code(self) -> i32 {
match self {
Self::InvalidInput | Self::CartInvalid | Self::NotFound | Self::AddressMissing => 2,
Self::QuoteChanged => 3,
Self::AuthMissing
| Self::AuthExpired
| Self::HydraExpired
| Self::CdpUnavailable
| Self::OtpInvalid
| Self::CaptchaRequired
| Self::LoginExpired => 4,
Self::OrderingDisabled | Self::UrlNotAllowed => 5,
Self::ConfirmationRequired => 6,
Self::RateLimited => 7,
Self::Api | Self::Network | Self::Io | Self::Parse => 1,
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct Error {
pub kind: ErrorKind,
pub message: String,
pub details: Option<serde_json::Value>,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
impl Error {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
details: None,
}
}
#[must_use]
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
pub fn invalid(message: impl Into<String>) -> Self {
Self::new(ErrorKind::InvalidInput, message)
}
pub fn parse(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Parse, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorKind::NotFound, message)
}
pub fn exit_code(&self) -> i32 {
self.kind.exit_code()
}
pub fn to_json(&self) -> serde_json::Value {
let mut e = serde_json::json!({
"kind": self.kind.as_str(),
"message": self.message,
});
if let Some(d) = &self.details {
e["details"] = d.clone();
}
serde_json::json!({ "error": e })
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::new(ErrorKind::Io, e.to_string())
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::new(ErrorKind::Parse, format!("JSON: {e}"))
}
}
impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Self {
Self::new(ErrorKind::Io, format!("cache database: {e}"))
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Self::new(ErrorKind::Network, e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_shape() {
let e = Error::new(ErrorKind::HydraExpired, "hydra gone");
let j = e.to_json();
assert_eq!(j["error"]["kind"], "hydra_expired");
assert_eq!(j["error"]["message"], "hydra gone");
assert_eq!(e.exit_code(), 4);
}
#[test]
fn address_missing_is_not_an_auth_error() {
assert_eq!(ErrorKind::AddressMissing.as_str(), "address_missing");
assert_eq!(ErrorKind::AddressMissing.exit_code(), 2);
}
#[test]
fn quote_changed_exits_3() {
assert_eq!(ErrorKind::QuoteChanged.exit_code(), 3);
}
}