1use std::fmt;
4
5use thiserror::Error;
6
7use crate::format_convert::FormatConvertError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum IoOperationKind {
14 Read,
16 Write,
18 Create,
20 Delete,
22 Rename,
24 CreateDir,
26 ReadDir,
28 Sync,
30}
31
32impl fmt::Display for IoOperationKind {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::Read => write!(f, "read"),
36 Self::Write => write!(f, "write"),
37 Self::Create => write!(f, "create"),
38 Self::Delete => write!(f, "delete"),
39 Self::Rename => write!(f, "rename"),
40 Self::CreateDir => write!(f, "create directory"),
41 Self::ReadDir => write!(f, "read directory"),
42 Self::Sync => write!(f, "sync"),
43 }
44 }
45}
46
47fn format_io_error(
49 operation: &IoOperationKind,
50 path: &str,
51 context: &Option<String>,
52 error: &str,
53) -> String {
54 if let Some(ctx) = context {
55 format!("Failed to {} {} at '{}': {}", operation, ctx, path, error)
56 } else {
57 format!("Failed to {} file at '{}': {}", operation, path, error)
58 }
59}
60
61#[derive(Error, Debug)]
63#[non_exhaustive]
64pub enum StoreError {
65 #[error("{}", format_io_error(.operation, .path, .context, .error))]
70 IoError {
71 operation: IoOperationKind,
73 path: String,
75 context: Option<String>,
77 error: String,
79 },
80
81 #[error("Cannot determine home directory")]
83 HomeDirNotFound,
84
85 #[error("Failed to encode filename for ID '{id}': {reason}")]
95 FilenameEncoding {
96 id: String,
98 reason: String,
100 },
101
102 #[error("format conversion: {0}")]
106 FormatConvert(#[from] FormatConvertError),
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_store_error_io_error_display_without_context() {
115 let err = StoreError::IoError {
116 operation: IoOperationKind::Read,
117 path: "/path/to/file.toml".to_string(),
118 context: None,
119 error: "Permission denied".to_string(),
120 };
121 let display = format!("{}", err);
122 assert!(display.contains("Failed to read"));
123 assert!(display.contains("/path/to/file.toml"));
124 assert!(display.contains("Permission denied"));
125 }
126
127 #[test]
128 fn test_store_error_io_error_display_with_context() {
129 let err = StoreError::IoError {
130 operation: IoOperationKind::Write,
131 path: "/path/to/tmp.toml".to_string(),
132 context: Some("temporary file".to_string()),
133 error: "Disk full".to_string(),
134 };
135 let display = format!("{}", err);
136 assert!(display.contains("Failed to write"));
137 assert!(display.contains("temporary file"));
138 assert!(display.contains("/path/to/tmp.toml"));
139 assert!(display.contains("Disk full"));
140 }
141
142 #[test]
143 fn test_store_error_home_dir_not_found_display() {
144 let err = StoreError::HomeDirNotFound;
145 let display = format!("{}", err);
146 assert!(display.contains("Cannot determine home directory"));
147 }
148
149 #[test]
150 fn test_store_error_is_std_error() {
151 let err = StoreError::HomeDirNotFound;
152 let _: &dyn std::error::Error = &err;
153 }
154
155 #[test]
156 fn test_io_operation_kind_display() {
157 assert_eq!(IoOperationKind::Read.to_string(), "read");
158 assert_eq!(IoOperationKind::Write.to_string(), "write");
159 assert_eq!(IoOperationKind::Create.to_string(), "create");
160 assert_eq!(IoOperationKind::Delete.to_string(), "delete");
161 assert_eq!(IoOperationKind::Rename.to_string(), "rename");
162 assert_eq!(IoOperationKind::CreateDir.to_string(), "create directory");
163 assert_eq!(IoOperationKind::ReadDir.to_string(), "read directory");
164 assert_eq!(IoOperationKind::Sync.to_string(), "sync");
165 }
166
167 #[test]
168 fn test_store_error_filename_encoding_display() {
169 let err = StoreError::FilenameEncoding {
170 id: "my/id".to_string(),
171 reason: "ID contains invalid characters for Direct encoding".to_string(),
172 };
173 let display = format!("{}", err);
174 assert!(display.contains("my/id"), "display should contain id");
175 assert!(
176 display.contains("invalid characters"),
177 "display should contain reason"
178 );
179 }
180
181 #[test]
182 fn test_store_error_io_error_rename_with_retries() {
183 let err = StoreError::IoError {
184 operation: IoOperationKind::Rename,
185 path: "/path/to/file.toml".to_string(),
186 context: Some("after 3 retries".to_string()),
187 error: "Resource temporarily unavailable".to_string(),
188 };
189 let display = format!("{}", err);
190 assert!(display.contains("Failed to rename"));
191 assert!(display.contains("after 3 retries"));
192 assert!(display.contains("/path/to/file.toml"));
193 assert!(display.contains("Resource temporarily unavailable"));
194 }
195}