Skip to main content

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}