Skip to main content

dre_core/
error.rs

1//! DRE's error type at module boundaries: a registered [`Code`] (whose [`Kind`] decides the exit
2//! code and whether trying again can help), the message, and an optional hint at the fix.
3
4use crate::codes::{Code, ErrorCode, Kind};
5
6/// A failure with its code.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message}")]
9pub struct Error {
10    pub code: ErrorCode,
11    pub message: String,
12    /// What to do about it, when the message doesn't already say.
13    pub hint: Option<String>,
14}
15
16impl Error {
17    pub fn new(code: impl Into<ErrorCode>, message: impl Into<String>) -> Error {
18        Error {
19            code: code.into(),
20            message: message.into(),
21            hint: None,
22        }
23    }
24
25    pub fn with_hint(mut self, hint: impl Into<String>) -> Error {
26        self.hint = Some(hint.into());
27        self
28    }
29
30    pub fn kind(&self) -> Kind {
31        self.code.kind()
32    }
33
34    /// The step's own code, unless a more specific one is already set (anything but
35    /// `run-failed`, what an uncoded message gets).
36    pub fn or(mut self, code: Code) -> Error {
37        if self.code == Code::RunFailed {
38            self.code = code.into();
39        }
40        self
41    }
42
43    /// A plugin's failure: its own code when it gave one (`code` without `kind` counts as
44    /// `internal`), else `run-failed` until the step gives it one. `message` is what to show.
45    pub fn from_plugin(e: &dre_protocol::host::HostError, message: impl Into<String>) -> Error {
46        let code = match e {
47            dre_protocol::host::HostError::Plugin {
48                code: Some(code),
49                kind,
50                ..
51            } => ErrorCode::Plugin {
52                code: code.clone(),
53                kind: kind.as_deref().and_then(Kind::parse).unwrap_or(Kind::Internal),
54            },
55            _ => ErrorCode::Core(Code::RunFailed),
56        };
57        Error {
58            code,
59            message: message.into(),
60            hint: None,
61        }
62    }
63}
64
65/// An uncoded message: `run-failed`, until the step that knows better gives it a code.
66impl From<String> for Error {
67    fn from(message: String) -> Error {
68        Error::new(Code::RunFailed, message)
69    }
70}
71
72impl From<&str> for Error {
73    fn from(message: &str) -> Error {
74        Error::new(Code::RunFailed, message)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use dre_protocol::host::HostError;
82
83    fn plugin(kind: Option<&str>, code: Option<&str>) -> HostError {
84        HostError::Plugin {
85            plugin: "dre-plugin-sftp".into(),
86            message: "refused".into(),
87            kind: kind.map(str::to_string),
88            code: code.map(str::to_string),
89        }
90    }
91
92    #[test]
93    fn a_plugin_code_is_kept_with_its_kind() {
94        let e = Error::from_plugin(&plugin(Some("auth"), Some("sftp/bad-key")), "m").or(Code::DeliveryFailed);
95        assert_eq!(e.code.as_str(), "sftp/bad-key");
96        assert_eq!(e.kind(), Kind::Auth);
97        // A code without a kind counts as internal.
98        let e = Error::from_plugin(&plugin(None, Some("sftp/odd")), "m");
99        assert_eq!(e.kind(), Kind::Internal);
100    }
101
102    #[test]
103    fn without_a_code_the_step_gives_its_own() {
104        let e = Error::from_plugin(&plugin(Some("auth"), None), "m").or(Code::DeliveryFailed);
105        assert_eq!(e.code, Code::DeliveryFailed);
106    }
107}