1use 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}