Skip to main content

luft_storage/
error.rs

1//! Unified error type for the storage layer.
2
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum StorageError {
7    #[error("sqlx error: {0}")]
8    Sqlx(#[from] sqlx::Error),
9
10    #[error("migration error: {0}")]
11    Migration(#[from] sqlx::migrate::MigrateError),
12
13    #[error("serialization error: {0}")]
14    Serde(#[from] serde_json::Error),
15
16    #[error("io error: {0}")]
17    Io(#[from] std::io::Error),
18
19    #[error("not found: {0}")]
20    NotFound(String),
21
22    #[error("invalid input: {0}")]
23    Invalid(String),
24}
25
26pub type StorageResult<T> = std::result::Result<T, StorageError>;
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn debug_format_for_io_error() {
34        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
35        let err: StorageError = io_err.into();
36        let dbg = format!("{:?}", err);
37        assert!(
38            dbg.contains("Io"),
39            "Debug should contain variant name: {dbg}"
40        );
41        assert!(
42            dbg.contains("missing"),
43            "Debug should contain inner message: {dbg}"
44        );
45    }
46
47    #[test]
48    fn display_for_io_error_includes_source_message() {
49        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
50        let err: StorageError = io_err.into();
51        let display = err.to_string();
52        assert!(
53            display.contains("io error"),
54            "Display prefix should be present: {display}"
55        );
56        assert!(
57            display.contains("denied"),
58            "Display should include inner io error: {display}"
59        );
60    }
61
62    #[test]
63    fn not_found_display_contains_payload() {
64        let err = StorageError::NotFound("run-42".to_string());
65        let display = err.to_string();
66        assert!(display.contains("not found"), "prefix: {display}");
67        assert!(display.contains("run-42"), "payload: {display}");
68    }
69
70    #[test]
71    fn invalid_display_contains_payload() {
72        let err = StorageError::Invalid("bad json".to_string());
73        let display = err.to_string();
74        assert!(display.contains("invalid input"), "prefix: {display}");
75        assert!(display.contains("bad json"), "payload: {display}");
76    }
77
78    #[test]
79    fn from_serde_json_error() {
80        let serde_err: serde_json::Error = serde_json::from_str::<i32>("not a number").unwrap_err();
81        let err: StorageError = serde_err.into();
82        matches!(err, StorageError::Serde(_));
83        let display = err.to_string();
84        assert!(
85            display.contains("serialization error"),
86            "Display should include prefix: {display}"
87        );
88    }
89
90    #[test]
91    fn from_io_error_yields_io_variant() {
92        let io_err = std::io::Error::other("boom");
93        let err: StorageError = io_err.into();
94        assert!(matches!(err, StorageError::Io(_)));
95    }
96
97    #[test]
98    fn storage_result_ok_and_err_shortcuts() {
99        let ok: StorageResult<i32> = Ok(7);
100        assert_eq!(ok.unwrap(), 7);
101
102        let err: StorageResult<i32> = Err(StorageError::Invalid("x".into()));
103        assert!(err.is_err());
104    }
105
106    #[test]
107    fn sqlx_error_conversion_via_question_mark() {
108        fn propagate() -> StorageResult<()> {
109            let io_err = std::io::Error::other("inner");
110            let _x: StorageError = StorageError::Io(io_err);
111            Ok(())
112        }
113        assert!(propagate().is_ok());
114    }
115
116    #[test]
117    fn variants_are_distinct_patterns() {
118        let io_err = std::io::Error::other("io");
119        let err: StorageError = StorageError::Io(io_err);
120        match err {
121            StorageError::Sqlx(_) => panic!("should not be Sqlx"),
122            StorageError::Migration(_) => panic!("should not be Migration"),
123            StorageError::Serde(_) => panic!("should not be Serde"),
124            StorageError::Io(_) => {}
125            StorageError::NotFound(_) => panic!("should not be NotFound"),
126            StorageError::Invalid(_) => panic!("should not be Invalid"),
127        }
128    }
129}