1use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum ErrorKind {
12 Transient,
14 Config,
16 Preflight,
18 Unsupported,
21 ResyncRequired,
24 Resource,
27 Consistency,
29 Internal,
31 Cancelled,
33}
34
35impl ErrorKind {
36 pub fn is_retryable(self) -> bool {
37 matches!(self, ErrorKind::Transient)
38 }
39}
40
41impl fmt::Display for ErrorKind {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 let s = match self {
44 ErrorKind::Transient => "transient",
45 ErrorKind::Config => "config",
46 ErrorKind::Preflight => "preflight",
47 ErrorKind::Unsupported => "unsupported",
48 ErrorKind::ResyncRequired => "resync_required",
49 ErrorKind::Resource => "resource",
50 ErrorKind::Consistency => "consistency",
51 ErrorKind::Internal => "internal",
52 ErrorKind::Cancelled => "cancelled",
53 };
54 f.write_str(s)
55 }
56}
57
58#[derive(Debug)]
60pub struct Error {
61 kind: ErrorKind,
62 message: String,
63 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
64}
65
66impl Error {
67 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
68 Error {
69 kind,
70 message: message.into(),
71 source: None,
72 }
73 }
74
75 pub fn with_source(
76 kind: ErrorKind,
77 message: impl Into<String>,
78 source: impl std::error::Error + Send + Sync + 'static,
79 ) -> Self {
80 Error {
81 kind,
82 message: message.into(),
83 source: Some(Box::new(source)),
84 }
85 }
86
87 pub fn transient(message: impl Into<String>) -> Self {
88 Self::new(ErrorKind::Transient, message)
89 }
90 pub fn config(message: impl Into<String>) -> Self {
91 Self::new(ErrorKind::Config, message)
92 }
93 pub fn preflight(message: impl Into<String>) -> Self {
94 Self::new(ErrorKind::Preflight, message)
95 }
96 pub fn unsupported(message: impl Into<String>) -> Self {
97 Self::new(ErrorKind::Unsupported, message)
98 }
99 pub fn resync_required(message: impl Into<String>) -> Self {
100 Self::new(ErrorKind::ResyncRequired, message)
101 }
102 pub fn resource(message: impl Into<String>) -> Self {
103 Self::new(ErrorKind::Resource, message)
104 }
105 pub fn internal(message: impl Into<String>) -> Self {
106 Self::new(ErrorKind::Internal, message)
107 }
108 pub fn consistency(message: impl Into<String>) -> Self {
109 Self::new(ErrorKind::Consistency, message)
110 }
111 pub fn cancelled() -> Self {
112 Self::new(ErrorKind::Cancelled, "operation cancelled")
113 }
114
115 pub fn kind(&self) -> ErrorKind {
116 self.kind
117 }
118
119 pub fn message(&self) -> &str {
120 &self.message
121 }
122
123 pub fn is_retryable(&self) -> bool {
124 self.kind.is_retryable()
125 }
126
127 pub fn context(mut self, ctx: impl fmt::Display) -> Self {
129 self.message = format!("{ctx}: {}", self.message);
130 self
131 }
132}
133
134impl fmt::Display for Error {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "[{}] {}", self.kind, self.message)?;
137 if let Some(src) = &self.source {
138 write!(f, ": {src}")?;
139 }
140 Ok(())
141 }
142}
143
144impl std::error::Error for Error {
145 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146 self.source
147 .as_deref()
148 .map(|e| e as &(dyn std::error::Error + 'static))
149 }
150}
151
152pub type Result<T, E = Error> = std::result::Result<T, E>;
153
154pub trait ResultExt<T> {
156 fn context(self, ctx: impl fmt::Display) -> Result<T>;
157}
158
159impl<T> ResultExt<T> for Result<T> {
160 fn context(self, ctx: impl fmt::Display) -> Result<T> {
161 self.map_err(|e| e.context(ctx))
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn display_includes_kind_and_source() {
171 let io = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset by peer");
172 let e = Error::with_source(ErrorKind::Transient, "source connection lost", io);
173 assert_eq!(
174 e.to_string(),
175 "[transient] source connection lost: reset by peer"
176 );
177 assert!(e.is_retryable());
178 assert!(std::error::Error::source(&e).is_some());
179 }
180
181 #[test]
182 fn only_transient_is_retryable() {
183 assert!(Error::transient("x").is_retryable());
184 for e in [
185 Error::config("x"),
186 Error::preflight("x"),
187 Error::unsupported("x"),
188 Error::resync_required("x"),
189 Error::resource("x"),
190 Error::internal("x"),
191 Error::consistency("x"),
192 Error::cancelled(),
193 ] {
194 assert!(!e.is_retryable(), "{e}");
195 }
196 }
197
198 #[test]
199 fn context_prefixes_message_and_keeps_kind() {
200 let e = Error::unsupported("no replica identity").context("table public.t");
201 assert_eq!(e.kind(), ErrorKind::Unsupported);
202 assert_eq!(e.message(), "table public.t: no replica identity");
203 }
204}