use serde::Deserialize;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("not found: {message}")]
NotFound {
message: String,
request_id: Option<String>,
},
#[error("authentication failed: {message}")]
Authentication {
message: String,
request_id: Option<String>,
},
#[error("permission denied: {message}")]
PermissionDenied {
message: String,
request_id: Option<String>,
},
#[error("rate limited (retry after {retry_after:?}s): {message}")]
RateLimited {
message: String,
retry_after: Option<u64>,
request_id: Option<String>,
},
#[error("validation failed: {message}")]
Validation {
message: String,
details: Option<serde_json::Value>,
request_id: Option<String>,
},
#[error("already exists: {message}")]
AlreadyExists {
message: String,
request_id: Option<String>,
},
#[error("payment required: {message}")]
PaymentRequired {
message: String,
details: Option<serde_json::Value>,
request_id: Option<String>,
},
#[error("API error [{code}] (HTTP {status}): {message}")]
Api {
code: String,
message: String,
status: u16,
request_id: Option<String>,
},
#[error("network error: {0}")]
Network(#[source] reqwest::Error),
#[error("decode error: {0}")]
Decode(#[source] serde_json::Error),
#[error("invalid configuration: {0}")]
Config(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Deserialize)]
pub(crate) struct ErrorEnvelope {
pub error: ErrorBody,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ErrorBody {
pub code: String,
pub message: String,
pub details: Option<serde_json::Value>,
}
pub(crate) fn map_error(
status: u16,
body: ErrorBody,
request_id: Option<String>,
retry_after: Option<u64>,
) -> Error {
match body.code.as_str() {
"not_found" => Error::NotFound {
message: body.message,
request_id,
},
"unauthenticated" => Error::Authentication {
message: body.message,
request_id,
},
"permission_denied" => Error::PermissionDenied {
message: body.message,
request_id,
},
"rate_limited" => Error::RateLimited {
message: body.message,
retry_after,
request_id,
},
"invalid_argument" => Error::Validation {
message: body.message,
details: body.details,
request_id,
},
"already_exists" => Error::AlreadyExists {
message: body.message,
request_id,
},
"payment_required" => Error::PaymentRequired {
message: body.message,
details: body.details,
request_id,
},
_ => Error::Api {
code: body.code,
message: body.message,
status,
request_id,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_not_found() {
let body = ErrorBody {
code: "not_found".into(),
message: "sub_x does not exist".into(),
details: None,
};
let err = map_error(404, body, Some("req_abc".into()), None);
match err {
Error::NotFound {
message,
request_id,
} => {
assert_eq!(message, "sub_x does not exist");
assert_eq!(request_id.as_deref(), Some("req_abc"));
}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_rate_limited_with_retry_after() {
let body = ErrorBody {
code: "rate_limited".into(),
message: "slow down".into(),
details: None,
};
let err = map_error(429, body, None, Some(30));
match err {
Error::RateLimited { retry_after, .. } => assert_eq!(retry_after, Some(30)),
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn unknown_code_falls_through_to_api_variant() {
let body = ErrorBody {
code: "brand_new_code".into(),
message: "???".into(),
details: None,
};
let err = map_error(418, body, None, None);
match err {
Error::Api { code, status, .. } => {
assert_eq!(code, "brand_new_code");
assert_eq!(status, 418);
}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_unauthenticated() {
let body = ErrorBody {
code: "unauthenticated".into(),
message: "bad key".into(),
details: None,
};
match map_error(401, body, None, None) {
Error::Authentication { .. } => {}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_permission_denied() {
let body = ErrorBody {
code: "permission_denied".into(),
message: "nope".into(),
details: None,
};
match map_error(403, body, None, None) {
Error::PermissionDenied { .. } => {}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_invalid_argument() {
let body = ErrorBody {
code: "invalid_argument".into(),
message: "bad".into(),
details: Some(serde_json::json!({"field": "x"})),
};
match map_error(400, body, None, None) {
Error::Validation { details, .. } => assert!(details.is_some()),
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_already_exists() {
let body = ErrorBody {
code: "already_exists".into(),
message: "dupe".into(),
details: None,
};
match map_error(409, body, None, None) {
Error::AlreadyExists { .. } => {}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn maps_payment_required() {
let body = ErrorBody {
code: "payment_required".into(),
message: "pay up".into(),
details: None,
};
match map_error(402, body, None, None) {
Error::PaymentRequired { .. } => {}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn internal_and_unavailable_fall_through_to_api() {
for code in ["internal", "unavailable"] {
let body = ErrorBody {
code: code.into(),
message: "oops".into(),
details: None,
};
match map_error(500, body, None, None) {
Error::Api { code: c, .. } => assert_eq!(c, code),
other => panic!("wrong variant for {code}: {other:?}"),
}
}
}
}