1use std::fmt;
13
14use crate::ids::{ClusterId, IndexName, PartitionId, PrincipalId};
15
16#[non_exhaustive]
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
23pub enum ErrorCode {
24 PartitionUnresolved,
26 PlacementMissing,
28 PlacementBackendUnavailable,
30 UnsupportedEndpoint,
32 StaleEpoch,
35 AuthFailed,
37 Unauthorized,
39 UpstreamFailed,
41 Overloaded,
43 CursorUnresolvable,
47 PayloadTooLarge,
50}
51
52impl ErrorCode {
53 #[must_use]
55 pub fn as_slug(self) -> &'static str {
56 match self {
57 Self::PartitionUnresolved => "partition_unresolved",
58 Self::PlacementMissing => "placement_missing",
59 Self::PlacementBackendUnavailable => "placement_backend_unavailable",
60 Self::UnsupportedEndpoint => "unsupported_endpoint",
61 Self::StaleEpoch => "stale_epoch",
62 Self::AuthFailed => "auth_failed",
63 Self::Unauthorized => "unauthorized",
64 Self::UpstreamFailed => "upstream_failed",
65 Self::Overloaded => "overloaded",
66 Self::CursorUnresolvable => "cursor_unresolvable",
67 Self::PayloadTooLarge => "payload_too_large",
68 }
69 }
70}
71
72impl fmt::Display for ErrorCode {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(self.as_slug())
75 }
76}
77
78#[derive(Clone, Default, PartialEq, Eq, Debug)]
84pub struct DecisionChain {
85 pub principal: Option<PrincipalId>,
87 pub partition: Option<PartitionId>,
89 pub cluster: Option<ClusterId>,
91 pub index: Option<IndexName>,
93}
94
95impl DecisionChain {
96 #[must_use]
98 pub fn new() -> Self {
99 Self::default()
100 }
101}
102
103#[derive(Clone, PartialEq, Eq, Debug)]
105pub struct ErrorContext {
106 pub code: ErrorCode,
108 pub decision_chain: DecisionChain,
110 pub retryable: bool,
112 pub remediation: &'static str,
114}
115
116impl ErrorContext {
117 #[must_use]
120 pub fn new(code: ErrorCode, retryable: bool, remediation: &'static str) -> Self {
121 Self {
122 code,
123 decision_chain: DecisionChain::new(),
124 retryable,
125 remediation,
126 }
127 }
128
129 #[must_use]
131 pub fn with_chain(mut self, chain: DecisionChain) -> Self {
132 self.decision_chain = chain;
133 self
134 }
135}
136
137impl fmt::Display for ErrorContext {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(
140 f,
141 "{} (retryable={}): {}",
142 self.code, self.retryable, self.remediation
143 )
144 }
145}
146
147impl std::error::Error for ErrorContext {}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
157 fn every_error_code_has_a_stable_distinct_slug() {
158 let all = [
159 ErrorCode::PartitionUnresolved,
160 ErrorCode::PlacementMissing,
161 ErrorCode::PlacementBackendUnavailable,
162 ErrorCode::UnsupportedEndpoint,
163 ErrorCode::StaleEpoch,
164 ErrorCode::AuthFailed,
165 ErrorCode::Unauthorized,
166 ErrorCode::UpstreamFailed,
167 ErrorCode::Overloaded,
168 ErrorCode::CursorUnresolvable,
169 ErrorCode::PayloadTooLarge,
170 ];
171 let mut seen = std::collections::HashSet::new();
172 for code in all {
173 let slug = code.as_slug();
174 assert_eq!(slug, code.to_string(), "Display must equal as_slug");
175 assert!(
176 slug.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
177 "{slug} must be lowercase snake_case"
178 );
179 assert!(seen.insert(slug), "duplicate slug {slug}");
180 }
181 assert_eq!(seen.len(), all.len());
182 }
183
184 #[test]
185 fn context_carries_chain_and_displays_actionably() {
186 let chain = DecisionChain {
187 partition: Some(PartitionId::from("t-1")),
188 ..DecisionChain::new()
189 };
190 let ctx = ErrorContext::new(
191 ErrorCode::PlacementMissing,
192 false,
193 "register a placement for the partition",
194 )
195 .with_chain(chain.clone());
196
197 assert_eq!(ctx.decision_chain, chain);
198 assert!(!ctx.retryable);
199 assert!(ctx.to_string().contains("placement_missing"));
200 assert!(ctx.to_string().contains("register a placement"));
201 }
202
203 #[test]
204 fn context_is_a_std_error() {
205 fn assert_error<E: std::error::Error>(_: &E) {}
206 let ctx = ErrorContext::new(ErrorCode::Overloaded, true, "retry with backoff");
207 assert_error(&ctx);
208 }
209}