Skip to main content

cufflink_native/
error.rs

1use cufflink_types::ext::ExtFailure;
2use std::fmt;
3
4/// Why an extension failed a call: a `code` its caller can tell apart and
5/// a message.
6///
7/// Any error converts into one with the code [`ExtError::FAILED`], so `?`
8/// passes on the errors an extension meets; [`ExtError::new`] names another
9/// code.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ExtError {
12    pub code: String,
13    pub message: String,
14}
15
16/// What an extension answers.
17pub type Result<T> = std::result::Result<T, ExtError>;
18
19impl ExtError {
20    /// The code of an error converted from any other.
21    pub const FAILED: &'static str = "failed";
22
23    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
24        Self {
25            code: code.into(),
26            message: message.into(),
27        }
28    }
29}
30
31impl<E: Into<Box<dyn std::error::Error + Send + Sync>>> From<E> for ExtError {
32    fn from(error: E) -> Self {
33        Self::new(Self::FAILED, error.into().to_string())
34    }
35}
36
37impl fmt::Display for ExtError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "{}: {}", self.code, self.message)
40    }
41}
42
43impl From<ExtError> for ExtFailure {
44    fn from(error: ExtError) -> Self {
45        Self {
46            code: error.code,
47            message: error.message,
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    fn converted<E: Into<Box<dyn std::error::Error + Send + Sync>>>(error: E) -> ExtError {
57        ExtError::from(error)
58    }
59
60    #[test]
61    fn any_error_converts_as_failed() {
62        let parsed = "x".parse::<u32>().map_err(ExtError::from).unwrap_err();
63        assert_eq!(
64            parsed,
65            ExtError::new("failed", "invalid digit found in string")
66        );
67        assert_eq!(
68            converted("scope is required"),
69            ExtError::new("failed", "scope is required")
70        );
71        assert_eq!(
72            converted(String::from("no quota")),
73            ExtError::new("failed", "no quota")
74        );
75        assert_eq!(
76            converted(std::io::Error::other("disk gone")),
77            ExtError::new("failed", "disk gone")
78        );
79    }
80
81    #[test]
82    fn question_mark_converts() {
83        fn scope(payload: &serde_json::Value) -> Result<String> {
84            let scope = payload["scope"].as_str().ok_or("scope is required")?;
85            let length: u8 = scope.len().try_into()?;
86            Ok(format!("{scope}:{length}"))
87        }
88        assert_eq!(
89            scope(&serde_json::json!({})),
90            Err(ExtError::new("failed", "scope is required"))
91        );
92        assert_eq!(
93            scope(&serde_json::json!({"scope": "a".repeat(300)})).map_err(|e| e.code),
94            Err("failed".into())
95        );
96        assert_eq!(
97            scope(&serde_json::json!({"scope": "drive"})),
98            Ok("drive:5".into())
99        );
100    }
101
102    #[test]
103    fn an_error_reads_and_travels_as_its_code_and_message() {
104        let error = ExtError::new("quota", "daily quota exhausted");
105        assert_eq!(error.to_string(), "quota: daily quota exhausted");
106        assert_eq!(
107            ExtFailure::from(error),
108            ExtFailure {
109                code: "quota".into(),
110                message: "daily quota exhausted".into()
111            }
112        );
113    }
114}