use std::fmt;
use crate::ids::{ClusterId, IndexName, PartitionId, PrincipalId};
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ErrorCode {
PartitionUnresolved,
PlacementMissing,
PlacementBackendUnavailable,
UnsupportedEndpoint,
StaleEpoch,
AuthFailed,
Unauthorized,
UpstreamFailed,
Overloaded,
CursorUnresolvable,
PayloadTooLarge,
}
impl ErrorCode {
#[must_use]
pub fn as_slug(self) -> &'static str {
match self {
Self::PartitionUnresolved => "partition_unresolved",
Self::PlacementMissing => "placement_missing",
Self::PlacementBackendUnavailable => "placement_backend_unavailable",
Self::UnsupportedEndpoint => "unsupported_endpoint",
Self::StaleEpoch => "stale_epoch",
Self::AuthFailed => "auth_failed",
Self::Unauthorized => "unauthorized",
Self::UpstreamFailed => "upstream_failed",
Self::Overloaded => "overloaded",
Self::CursorUnresolvable => "cursor_unresolvable",
Self::PayloadTooLarge => "payload_too_large",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_slug())
}
}
#[derive(Clone, Default, PartialEq, Eq, Debug)]
pub struct DecisionChain {
pub principal: Option<PrincipalId>,
pub partition: Option<PartitionId>,
pub cluster: Option<ClusterId>,
pub index: Option<IndexName>,
}
impl DecisionChain {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ErrorContext {
pub code: ErrorCode,
pub decision_chain: DecisionChain,
pub retryable: bool,
pub remediation: &'static str,
}
impl ErrorContext {
#[must_use]
pub fn new(code: ErrorCode, retryable: bool, remediation: &'static str) -> Self {
Self {
code,
decision_chain: DecisionChain::new(),
retryable,
remediation,
}
}
#[must_use]
pub fn with_chain(mut self, chain: DecisionChain) -> Self {
self.decision_chain = chain;
self
}
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} (retryable={}): {}",
self.code, self.retryable, self.remediation
)
}
}
impl std::error::Error for ErrorContext {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_error_code_has_a_stable_distinct_slug() {
let all = [
ErrorCode::PartitionUnresolved,
ErrorCode::PlacementMissing,
ErrorCode::PlacementBackendUnavailable,
ErrorCode::UnsupportedEndpoint,
ErrorCode::StaleEpoch,
ErrorCode::AuthFailed,
ErrorCode::Unauthorized,
ErrorCode::UpstreamFailed,
ErrorCode::Overloaded,
ErrorCode::CursorUnresolvable,
ErrorCode::PayloadTooLarge,
];
let mut seen = std::collections::HashSet::new();
for code in all {
let slug = code.as_slug();
assert_eq!(slug, code.to_string(), "Display must equal as_slug");
assert!(
slug.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"{slug} must be lowercase snake_case"
);
assert!(seen.insert(slug), "duplicate slug {slug}");
}
assert_eq!(seen.len(), all.len());
}
#[test]
fn context_carries_chain_and_displays_actionably() {
let chain = DecisionChain {
partition: Some(PartitionId::from("t-1")),
..DecisionChain::new()
};
let ctx = ErrorContext::new(
ErrorCode::PlacementMissing,
false,
"register a placement for the partition",
)
.with_chain(chain.clone());
assert_eq!(ctx.decision_chain, chain);
assert!(!ctx.retryable);
assert!(ctx.to_string().contains("placement_missing"));
assert!(ctx.to_string().contains("register a placement"));
}
#[test]
fn context_is_a_std_error() {
fn assert_error<E: std::error::Error>(_: &E) {}
let ctx = ErrorContext::new(ErrorCode::Overloaded, true, "retry with backoff");
assert_error(&ctx);
}
}