sift_error/lib.rs
1use std::{error::Error as StdError, fmt, result::Result as StdResult};
2
3#[cfg(test)]
4mod test;
5
6/// Other Sift crates should just import this prelude to get everything necessary to construct
7/// [Error] types.
8pub mod prelude {
9 pub use super::{Error, ErrorKind, Result, SiftError};
10}
11
12/// A `Result` that returns [Error] as the error-type.
13///
14/// This is a convenience type alias for `std::result::Result<T, Error>`.
15/// It's used throughout Sift crates as the standard error handling type.
16///
17/// # Example
18///
19/// ```rust
20/// use sift_error::{Error, ErrorKind, Result};
21///
22/// fn might_fail() -> Result<String> {
23/// Ok("success".to_string())
24/// }
25///
26/// fn handle_error() -> Result<()> {
27/// might_fail()?;
28/// Ok(())
29/// }
30/// ```
31pub type Result<T> = StdResult<T, Error>;
32pub type BoxedError = Box<dyn std::error::Error + Send + Sync>;
33
34/// Trait that defines the behavior of errors that Sift manages.
35///
36/// This trait provides methods for adding context and help text to errors,
37/// allowing for rich error messages that guide users toward resolution.
38///
39/// # Example
40///
41/// ```rust
42/// use sift_error::prelude::*;
43/// use std::io;
44///
45/// fn read_config() -> Result<String> {
46/// std::fs::read_to_string("config.toml")
47/// .map_err(|e| Error::new(ErrorKind::IoError, e))
48/// .context("failed to read configuration file")
49/// .help("ensure the config.toml file exists and is readable")
50/// }
51/// ```
52pub trait SiftError<T, C>
53where
54 C: fmt::Display + Send + Sync + 'static,
55{
56 /// Adds context that is printed with the error.
57 ///
58 /// Context is displayed as the most recent error message, with previous
59 /// context forming a chain of causes.
60 ///
61 /// # Example
62 ///
63 /// ```rust
64 /// use sift_error::prelude::*;
65 ///
66 /// fn example() -> Result<()> {
67 /// let err = Error::new_msg(ErrorKind::IoError, "file not found");
68 /// Err(err).context("failed to load user data")
69 /// }
70 /// ```
71 fn context(self, ctx: C) -> Result<T>;
72
73 /// Like `context` but takes in a closure.
74 ///
75 /// This is useful when constructing the context string is expensive,
76 /// as the closure is only called if there's an error.
77 ///
78 /// # Example
79 ///
80 /// ```rust
81 /// use sift_error::prelude::*;
82 ///
83 /// fn example(user_id: &str) -> Result<()> {
84 /// let err = Error::new_msg(ErrorKind::NotFoundError, "resource missing");
85 /// Err(err).with_context(|| format!("user {} not found", user_id))
86 /// }
87 /// ```
88 fn with_context<F>(self, op: F) -> Result<T>
89 where
90 F: Fn() -> C;
91
92 /// User-help text.
93 ///
94 /// Help text provides actionable guidance to users on how to resolve
95 /// the error. It's displayed separately from the error context.
96 ///
97 /// # Example
98 ///
99 /// ```rust
100 /// use sift_error::prelude::*;
101 ///
102 /// fn example() -> Result<()> {
103 /// let err = Error::new_msg(ErrorKind::ConfigError, "invalid config");
104 /// Err(err).help("check your sift.toml file for syntax errors")
105 /// }
106 /// ```
107 fn help(self, txt: C) -> Result<T>;
108}
109
110/// Error type returned across all Sift crates.
111#[derive(Debug)]
112pub struct Error {
113 context: Option<Vec<String>>,
114 help: Option<String>,
115 kind: ErrorKind,
116 inner: Option<BoxedError>,
117}
118
119impl StdError for Error {}
120
121impl Error {
122 /// Initializes an [Error] from a standard error type.
123 ///
124 /// This constructor wraps a standard library error (or any type implementing
125 /// `std::error::Error`) into a Sift error with the specified [ErrorKind].
126 ///
127 /// # Arguments
128 ///
129 /// * `kind` - The category of error that occurred
130 /// * `err` - The underlying error to wrap
131 ///
132 /// # Example
133 ///
134 /// ```rust
135 /// use sift_error::{Error, ErrorKind};
136 /// use std::io;
137 ///
138 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
139 /// let sift_error = Error::new(ErrorKind::IoError, io_error);
140 /// ```
141 pub fn new<E>(kind: ErrorKind, err: E) -> Self
142 where
143 E: StdError + Send + Sync + 'static,
144 {
145 let inner = Box::new(err);
146 Self {
147 inner: Some(inner),
148 kind,
149 context: None,
150 help: None,
151 }
152 }
153
154 /// Initializes an [Error] with a generic message string.
155 ///
156 /// This constructor creates an error from a string message without
157 /// wrapping an underlying error type.
158 ///
159 /// # Arguments
160 ///
161 /// * `kind` - The category of error that occurred
162 /// * `msg` - A string message describing the error
163 ///
164 /// # Example
165 ///
166 /// ```rust
167 /// use sift_error::{Error, ErrorKind};
168 ///
169 /// let error = Error::new_msg(ErrorKind::NotFoundError, "resource not found");
170 /// ```
171 pub fn new_msg<S: AsRef<str>>(kind: ErrorKind, msg: S) -> Self {
172 Self {
173 inner: None,
174 kind,
175 context: Some(vec![msg.as_ref().to_string()]),
176 help: None,
177 }
178 }
179
180 /// Initializes a general catch-all type of [Error].
181 ///
182 /// Contributors should be careful not to use this unless strictly necessary.
183 /// Prefer more specific [ErrorKind] variants when possible.
184 ///
185 /// # Arguments
186 ///
187 /// * `msg` - A string message describing the error
188 ///
189 /// # Example
190 ///
191 /// ```rust
192 /// use sift_error::Error;
193 ///
194 /// let error = Error::new_general("unexpected condition occurred");
195 /// ```
196 pub fn new_general<S: AsRef<str>>(msg: S) -> Self {
197 Self::new_msg(ErrorKind::GeneralError, msg)
198 }
199
200 /// Used for user-errors that have to do with bad arguments.
201 ///
202 /// This is a convenience constructor for argument validation errors.
203 ///
204 /// # Arguments
205 ///
206 /// * `msg` - A string message describing the argument validation failure
207 ///
208 /// # Example
209 ///
210 /// ```rust
211 /// use sift_error::Error;
212 ///
213 /// fn validate_age(age: i32) -> Result<(), Error> {
214 /// if age < 0 {
215 /// return Err(Error::new_arg_error("age must be non-negative"));
216 /// }
217 /// Ok(())
218 /// }
219 /// ```
220 pub fn new_arg_error<S: AsRef<str>>(msg: S) -> Self {
221 Self::new_msg(ErrorKind::ArgumentValidationError, msg)
222 }
223
224 /// Creates an error for empty gRPC responses.
225 ///
226 /// Tonic response types usually return optional types that we need to handle;
227 /// if responses are empty then this is the appropriate way to initialize an
228 /// [Error] for that situation, though this has never been observed in practice.
229 ///
230 /// # Arguments
231 ///
232 /// * `msg` - A string message describing the empty response situation
233 ///
234 /// # Example
235 ///
236 /// ```rust
237 /// use sift_error::Error;
238 ///
239 /// // This would typically be used when a gRPC response is unexpectedly empty
240 /// let error = Error::new_empty_response("asset response was empty");
241 /// ```
242 pub fn new_empty_response<S: AsRef<str>>(msg: S) -> Self {
243 Self {
244 inner: None,
245 kind: ErrorKind::EmptyResponseError,
246 context: Some(vec![msg.as_ref().to_string()]),
247 help: Some("please contact Sift".to_string()),
248 }
249 }
250
251 /// Get the underlying error kind.
252 ///
253 /// # Returns
254 ///
255 /// The [ErrorKind] that categorizes this error.
256 ///
257 /// # Example
258 ///
259 /// ```rust
260 /// use sift_error::{Error, ErrorKind};
261 ///
262 /// let error = Error::new_msg(ErrorKind::NotFoundError, "resource missing");
263 /// assert_eq!(error.kind(), ErrorKind::NotFoundError);
264 /// ```
265 pub fn kind(&self) -> ErrorKind {
266 self.kind
267 }
268}
269
270/// Various categories of errors that can occur throughout Sift crates.
271///
272/// Each variant represents a different category of error that can occur when
273/// interacting with Sift services or processing data. Error kinds help categorize
274/// errors for better error handling and user feedback.
275///
276/// # Example
277///
278/// ```rust
279/// use sift_error::{Error, ErrorKind};
280///
281/// let error = Error::new_msg(ErrorKind::NotFoundError, "asset not found");
282/// match error.kind() {
283/// ErrorKind::NotFoundError => println!("Resource was not found"),
284/// ErrorKind::IoError => println!("I/O error occurred"),
285/// _ => println!("Other error"),
286/// }
287/// ```
288#[derive(Debug, PartialEq, Copy, Clone)]
289pub enum ErrorKind {
290 /// Indicates that the error is due to a resource already existing.
291 AlreadyExistsError,
292 /// Indicates user-error having to do with bad arguments.
293 ArgumentValidationError,
294 /// Indicates that the program is unable to grab credentials from a user's `sift.toml` file.
295 ConfigError,
296 /// Indicates that the program was unable to connect to Sift.
297 ///
298 /// This occurs when there are network issues, invalid URIs, TLS problems,
299 /// or other connection-related failures when attempting to reach Sift services.
300 GrpcConnectError,
301 /// Indicates that the program was unable to retrieve the run being requested.
302 RetrieveRunError,
303 /// Indicates that the program was unable to retrieve the asset being requested.
304 RetrieveAssetError,
305 /// Indicates that the program was unable to update the asset being requested.
306 UpdateAssetError,
307 /// Indicates a failure to update a run.
308 UpdateRunError,
309 /// Indicates that the program was unable to retrieve the ingestion config being requested.
310 RetrieveIngestionConfigError,
311 /// Indicates that the program was unable to encode the message being requested.
312 EncodeMessageError,
313 /// Indicates a failure to create a run.
314 CreateRunError,
315 /// Indicates a failure to create an ingestion config.
316 CreateIngestionConfigError,
317 /// Indicates a failure to create a flow.
318 CreateFlowError,
319 /// Indicates a failure to find the requested resource, likely because it doesn't exist.
320 NotFoundError,
321 /// General I/O errors.
322 IoError,
323 /// Indicates that there was a conversion between numeric times.
324 NumberConversionError,
325 /// Indicates a failure to generate a particular time-type from arguments.
326 TimeConversionError,
327 /// General errors that can occur while streaming telemetry i.e. data ingestion.
328 ///
329 /// This is a catch-all for streaming-related errors that don't fit into more
330 /// specific categories, such as stream initialization failures or unexpected
331 /// stream state errors.
332 StreamError,
333 /// Indicates that all retries were exhausted in the configured retry policy.
334 RetriesExhausted,
335 /// General errors that can occur while processing backups during streaming.
336 BackupsError,
337 /// Indicates that the user is making a change that is not backwards compatible with an
338 /// existing ingestion config.
339 IncompatibleIngestionConfigChange,
340 /// Indicates that a user provided a flow-name that doesn't match any configured flow in the
341 /// parent ingestion config.
342 UnknownFlow,
343 /// Indicates an empty response from a gRPC service.
344 ///
345 /// This really shouldn't happen in normal operation. It occurs when a gRPC
346 /// response is unexpectedly empty.
347 EmptyResponseError,
348 /// When failing to decode protobuf from its wire format.
349 ProtobufDecodeError,
350 /// When backup checksums don't match.
351 BackupIntegrityError,
352 /// When backup file/buffer limit has been reached.
353 BackupLimitReached,
354 /// Errors with the SiftStream Metrics Server.
355 SiftStreamMetricsServerError,
356 /// General errors that are rarely returned.
357 ///
358 /// This is a catch-all error kind for unexpected or unclassified errors.
359 /// Contributors should prefer more specific error kinds when possible.
360 GeneralError,
361}
362
363impl<T, C> SiftError<T, C> for Result<T>
364where
365 C: fmt::Display + Send + Sync + 'static,
366{
367 fn with_context<F>(self, op: F) -> Result<T>
368 where
369 F: Fn() -> C,
370 {
371 self.map_err(|mut err| {
372 if let Some(context) = err.context.as_mut() {
373 context.push(format!("{}", op()));
374 } else {
375 err.context = Some(vec![format!("{}", op())]);
376 }
377 err
378 })
379 }
380
381 fn context(self, ctx: C) -> Self {
382 self.map_err(|mut err| {
383 if let Some(context) = err.context.as_mut() {
384 context.push(format!("{ctx}"));
385 } else {
386 err.context = Some(vec![format!("{ctx}")]);
387 }
388 err
389 })
390 }
391
392 fn help(self, txt: C) -> Self {
393 self.map_err(|mut err| {
394 err.help = Some(format!("{txt}"));
395 err
396 })
397 }
398}
399
400impl fmt::Display for ErrorKind {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 match self {
403 Self::AlreadyExistsError => write!(f, "AlreadyExistsError"),
404 Self::GrpcConnectError => write!(f, "GrpcConnectError"),
405 Self::RetriesExhausted => write!(f, "RetriesExhausted"),
406 Self::RetrieveAssetError => write!(f, "RetrieveAssetError"),
407 Self::UpdateAssetError => write!(f, "UpdateAssetError"),
408 Self::RetrieveRunError => write!(f, "RetrieveRunError"),
409 Self::RetrieveIngestionConfigError => write!(f, "RetrieveIngestionConfigError"),
410 Self::EncodeMessageError => write!(f, "EncodeMessageError"),
411 Self::EmptyResponseError => write!(f, "EmptyResponseError"),
412 Self::NotFoundError => write!(f, "NotFoundError"),
413 Self::CreateRunError => write!(f, "CreateRunError"),
414 Self::ArgumentValidationError => write!(f, "ArgumentValidationError"),
415 Self::GeneralError => write!(f, "GeneralError"),
416 Self::IoError => write!(f, "IoError"),
417 Self::ConfigError => write!(f, "ConfigError"),
418 Self::UpdateRunError => write!(f, "UpdateRunError"),
419 Self::CreateIngestionConfigError => write!(f, "CreateIngestionConfigError"),
420 Self::NumberConversionError => write!(f, "NumberConversionError"),
421 Self::CreateFlowError => write!(f, "CreateFlowError"),
422 Self::TimeConversionError => write!(f, "TimeConversionError"),
423 Self::StreamError => write!(f, "StreamError"),
424 Self::UnknownFlow => write!(f, "UnknownFlow"),
425 Self::BackupsError => write!(f, "BackupsError"),
426 Self::BackupIntegrityError => write!(f, "BackupIntegrityError"),
427 Self::BackupLimitReached => write!(f, "BackupLimitReached"),
428 Self::ProtobufDecodeError => write!(f, "ProtobufDecodeError"),
429 Self::IncompatibleIngestionConfigChange => {
430 write!(f, "IncompatibleIngestionConfigChange")
431 }
432 Self::SiftStreamMetricsServerError => write!(f, "SiftStreamMetricsServerError"),
433 }
434 }
435}
436
437const NEW_LINE_DELIMITER: &str = "\n ";
438
439impl fmt::Display for Error {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 let Error {
442 context,
443 kind,
444 help,
445 inner,
446 } = self;
447
448 let root_cause = inner.as_ref().map(|e| format!("{e}"));
449
450 let (most_recent_cause, chain) = context.as_ref().map_or_else(
451 || {
452 let root = root_cause.clone().unwrap_or_default();
453 (String::new(), format!("- {root}"))
454 },
455 |c| {
456 let mut cause_iter = c.iter().rev();
457
458 if let Some(first) = cause_iter.next() {
459 let mut cause_chain = cause_iter
460 .map(|s| format!("- {s}"))
461 .collect::<Vec<String>>()
462 .join(NEW_LINE_DELIMITER);
463
464 if let Some(root) = root_cause.clone() {
465 if cause_chain.is_empty() {
466 cause_chain = format!("- {root}");
467 } else {
468 cause_chain = format!("{cause_chain}{NEW_LINE_DELIMITER}- {root}");
469 }
470 }
471
472 (first.clone(), cause_chain)
473 } else {
474 (
475 String::new(),
476 root_cause
477 .as_ref()
478 .map_or_else(String::new, |s| format!("- {s}")),
479 )
480 }
481 },
482 );
483
484 match help {
485 Some(help_txt) if most_recent_cause.is_empty() => {
486 writeln!(
487 f,
488 "[{kind}]\n\n[cause]:{NEW_LINE_DELIMITER}{chain}\n\n[help]:{NEW_LINE_DELIMITER}- {help_txt}"
489 )
490 }
491 None if most_recent_cause.is_empty() => {
492 writeln!(f, "[{kind}]\n\n[cause]:{NEW_LINE_DELIMITER}{chain}")
493 }
494 Some(help_txt) => {
495 writeln!(
496 f,
497 "[{kind}]: {most_recent_cause}\n\n[cause]:{NEW_LINE_DELIMITER}{chain}\n\n[help]:{NEW_LINE_DELIMITER}- {help_txt}"
498 )
499 }
500 None => {
501 writeln!(
502 f,
503 "[{kind}]: {most_recent_cause}\n\n[cause]:{NEW_LINE_DELIMITER}{chain}"
504 )
505 }
506 }
507 }
508}
509
510impl From<std::io::Error> for Error {
511 fn from(value: std::io::Error) -> Self {
512 Self {
513 context: None,
514 help: None,
515 inner: Some(Box::new(value)),
516 kind: ErrorKind::IoError,
517 }
518 }
519}