1use std::fmt;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum IoOperationKind {
11 Read,
13 Write,
15 Create,
17 Delete,
19 Rename,
21 CreateDir,
23 ReadDir,
25 Sync,
27}
28
29impl fmt::Display for IoOperationKind {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Read => write!(f, "read"),
33 Self::Write => write!(f, "write"),
34 Self::Create => write!(f, "create"),
35 Self::Delete => write!(f, "delete"),
36 Self::Rename => write!(f, "rename"),
37 Self::CreateDir => write!(f, "create directory"),
38 Self::ReadDir => write!(f, "read directory"),
39 Self::Sync => write!(f, "sync"),
40 }
41 }
42}
43
44fn format_io_error(
46 operation: &IoOperationKind,
47 path: &str,
48 context: &Option<String>,
49 error: &str,
50) -> String {
51 if let Some(ctx) = context {
52 format!("Failed to {} {} at '{}': {}", operation, ctx, path, error)
53 } else {
54 format!("Failed to {} file at '{}': {}", operation, path, error)
55 }
56}
57
58#[derive(Error, Debug)]
60#[non_exhaustive]
61pub enum MigrationError {
62 #[error("Failed to deserialize: {0}")]
64 DeserializationError(String),
65
66 #[error("Failed to serialize: {0}")]
68 SerializationError(String),
69
70 #[error("Entity '{0}' not found")]
72 EntityNotFound(String),
73
74 #[error("No migration path defined for entity '{entity}' version '{version}'")]
76 MigrationPathNotDefined {
77 entity: String,
79 version: String,
81 },
82
83 #[error("Migration failed from '{from}' to '{to}': {error}")]
85 MigrationStepFailed {
86 from: String,
88 to: String,
90 error: String,
92 },
93
94 #[error("Circular migration path detected in entity '{entity}': {path}")]
96 CircularMigrationPath {
97 entity: String,
99 path: String,
101 },
102
103 #[error("Invalid version order in entity '{entity}': '{from}' -> '{to}' (versions must increase according to semver)")]
105 InvalidVersionOrder {
106 entity: String,
108 from: String,
110 to: String,
112 },
113
114 #[error("{}", format_io_error(.operation, .path, .context, .error))]
119 IoError {
120 operation: IoOperationKind,
122 path: String,
124 context: Option<String>,
126 error: String,
128 },
129
130 #[error("Failed to acquire file lock for '{path}': {error}")]
132 LockError {
133 path: String,
135 error: String,
137 },
138
139 #[error("Failed to parse TOML: {0}")]
141 TomlParseError(String),
142
143 #[error("Failed to serialize to TOML: {0}")]
145 TomlSerializeError(String),
146
147 #[error("Cannot determine home directory")]
149 HomeDirNotFound,
150
151 #[error("Failed to resolve path: {0}")]
153 PathResolution(String),
154
155 #[error("Failed to encode filename for ID '{id}': {reason}")]
157 FilenameEncoding {
158 id: String,
160 reason: String,
162 },
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_error_display_deserialization() {
171 let err = MigrationError::DeserializationError("invalid JSON".to_string());
172 let display = format!("{}", err);
173 assert!(display.contains("Failed to deserialize"));
174 assert!(display.contains("invalid JSON"));
175 }
176
177 #[test]
178 fn test_error_display_serialization() {
179 let err = MigrationError::SerializationError("invalid data".to_string());
180 let display = format!("{}", err);
181 assert!(display.contains("Failed to serialize"));
182 assert!(display.contains("invalid data"));
183 }
184
185 #[test]
186 fn test_error_display_entity_not_found() {
187 let err = MigrationError::EntityNotFound("user".to_string());
188 let display = format!("{}", err);
189 assert!(display.contains("Entity 'user' not found"));
190 }
191
192 #[test]
193 fn test_error_display_migration_path_not_defined() {
194 let err = MigrationError::MigrationPathNotDefined {
195 entity: "task".to_string(),
196 version: "2.0.0".to_string(),
197 };
198 let display = format!("{}", err);
199 assert!(display.contains("No migration path defined"));
200 assert!(display.contains("task"));
201 assert!(display.contains("2.0.0"));
202 }
203
204 #[test]
205 fn test_error_display_migration_step_failed() {
206 let err = MigrationError::MigrationStepFailed {
207 from: "1.0.0".to_string(),
208 to: "2.0.0".to_string(),
209 error: "field missing".to_string(),
210 };
211 let display = format!("{}", err);
212 assert!(display.contains("Migration failed"));
213 assert!(display.contains("1.0.0"));
214 assert!(display.contains("2.0.0"));
215 assert!(display.contains("field missing"));
216 }
217
218 #[test]
219 fn test_error_debug() {
220 let err = MigrationError::EntityNotFound("test".to_string());
221 let debug = format!("{:?}", err);
222 assert!(debug.contains("EntityNotFound"));
223 }
224
225 #[test]
226 fn test_error_is_std_error() {
227 let err = MigrationError::DeserializationError("test".to_string());
228 let _: &dyn std::error::Error = &err;
230 }
231
232 #[test]
233 fn test_error_display_circular_migration_path() {
234 let err = MigrationError::CircularMigrationPath {
235 entity: "task".to_string(),
236 path: "1.0.0 -> 2.0.0 -> 1.0.0".to_string(),
237 };
238 let display = format!("{}", err);
239 assert!(display.contains("Circular migration path"));
240 assert!(display.contains("task"));
241 assert!(display.contains("1.0.0 -> 2.0.0 -> 1.0.0"));
242 }
243
244 #[test]
245 fn test_error_display_invalid_version_order() {
246 let err = MigrationError::InvalidVersionOrder {
247 entity: "task".to_string(),
248 from: "2.0.0".to_string(),
249 to: "1.0.0".to_string(),
250 };
251 let display = format!("{}", err);
252 assert!(display.contains("Invalid version order"));
253 assert!(display.contains("task"));
254 assert!(display.contains("2.0.0"));
255 assert!(display.contains("1.0.0"));
256 assert!(display.contains("must increase"));
257 }
258
259 #[test]
260 fn test_error_display_io_error_without_context() {
261 let err = MigrationError::IoError {
262 operation: IoOperationKind::Read,
263 path: "/path/to/file.toml".to_string(),
264 context: None,
265 error: "Permission denied".to_string(),
266 };
267 let display = format!("{}", err);
268 assert!(display.contains("Failed to read"));
269 assert!(display.contains("/path/to/file.toml"));
270 assert!(display.contains("Permission denied"));
271 }
272
273 #[test]
274 fn test_error_display_io_error_with_context() {
275 let err = MigrationError::IoError {
276 operation: IoOperationKind::Write,
277 path: "/path/to/tmp.toml".to_string(),
278 context: Some("temporary file".to_string()),
279 error: "Disk full".to_string(),
280 };
281 let display = format!("{}", err);
282 assert!(display.contains("Failed to write"));
283 assert!(display.contains("temporary file"));
284 assert!(display.contains("/path/to/tmp.toml"));
285 assert!(display.contains("Disk full"));
286 }
287
288 #[test]
289 fn test_error_display_io_error_rename_with_retries() {
290 let err = MigrationError::IoError {
291 operation: IoOperationKind::Rename,
292 path: "/path/to/file.toml".to_string(),
293 context: Some("after 3 retries".to_string()),
294 error: "Resource temporarily unavailable".to_string(),
295 };
296 let display = format!("{}", err);
297 assert!(display.contains("Failed to rename"));
298 assert!(display.contains("after 3 retries"));
299 assert!(display.contains("/path/to/file.toml"));
300 assert!(display.contains("Resource temporarily unavailable"));
301 }
302
303 #[test]
304 fn test_io_operation_kind_display() {
305 assert_eq!(IoOperationKind::Read.to_string(), "read");
306 assert_eq!(IoOperationKind::Write.to_string(), "write");
307 assert_eq!(IoOperationKind::Create.to_string(), "create");
308 assert_eq!(IoOperationKind::Delete.to_string(), "delete");
309 assert_eq!(IoOperationKind::Rename.to_string(), "rename");
310 assert_eq!(IoOperationKind::CreateDir.to_string(), "create directory");
311 assert_eq!(IoOperationKind::ReadDir.to_string(), "read directory");
312 assert_eq!(IoOperationKind::Sync.to_string(), "sync");
313 }
314}