Skip to main content

version_migrate/
errors.rs

1//! Error types for migration operations.
2
3use thiserror::Error;
4
5pub use local_store::{IoOperationKind, StoreError};
6
7/// Error types that can occur during migration operations.
8#[derive(Error, Debug)]
9#[non_exhaustive]
10pub enum MigrationError {
11    /// Failed to deserialize the data.
12    #[error("Failed to deserialize: {0}")]
13    DeserializationError(String),
14
15    /// Failed to serialize the data.
16    #[error("Failed to serialize: {0}")]
17    SerializationError(String),
18
19    /// The requested entity type was not found in the migrator.
20    #[error("Entity '{0}' not found")]
21    EntityNotFound(String),
22
23    /// No migration path is defined for the given entity and version.
24    #[error("No migration path defined for entity '{entity}' version '{version}'")]
25    MigrationPathNotDefined {
26        /// The entity name.
27        entity: String,
28        /// The version that has no migration path.
29        version: String,
30    },
31
32    /// A migration step failed during execution.
33    #[error("Migration failed from '{from}' to '{to}': {error}")]
34    MigrationStepFailed {
35        /// The source version.
36        from: String,
37        /// The target version.
38        to: String,
39        /// The error message.
40        error: String,
41    },
42
43    /// A circular migration path was detected.
44    #[error("Circular migration path detected in entity '{entity}': {path}")]
45    CircularMigrationPath {
46        /// The entity name.
47        entity: String,
48        /// The path that forms a cycle.
49        path: String,
50    },
51
52    /// Version ordering is invalid (not following semver rules).
53    #[error("Invalid version order in entity '{entity}': '{from}' -> '{to}' (versions must increase according to semver)")]
54    InvalidVersionOrder {
55        /// The entity name.
56        entity: String,
57        /// The source version.
58        from: String,
59        /// The target version.
60        to: String,
61    },
62
63    /// File locking error.
64    #[error("Failed to acquire file lock for '{path}': {error}")]
65    LockError {
66        /// The file path.
67        path: String,
68        /// The error message.
69        error: String,
70    },
71
72    /// TOML parsing error.
73    #[error("Failed to parse TOML: {0}")]
74    TomlParseError(String),
75
76    /// TOML serialization error.
77    #[error("Failed to serialize to TOML: {0}")]
78    TomlSerializeError(String),
79
80    /// Failed to resolve path.
81    #[error("Failed to resolve path: {0}")]
82    PathResolution(String),
83
84    /// Failed to encode filename.
85    #[error("Failed to encode filename for ID '{id}': {reason}")]
86    FilenameEncoding {
87        /// The entity ID that failed to encode.
88        id: String,
89        /// The reason for the encoding failure.
90        reason: String,
91    },
92
93    /// Store / path-related error (delegated to local-store crate).
94    #[error(transparent)]
95    Store(#[from] StoreError),
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_error_display_deserialization() {
104        let err = MigrationError::DeserializationError("invalid JSON".to_string());
105        let display = format!("{}", err);
106        assert!(display.contains("Failed to deserialize"));
107        assert!(display.contains("invalid JSON"));
108    }
109
110    #[test]
111    fn test_error_display_serialization() {
112        let err = MigrationError::SerializationError("invalid data".to_string());
113        let display = format!("{}", err);
114        assert!(display.contains("Failed to serialize"));
115        assert!(display.contains("invalid data"));
116    }
117
118    #[test]
119    fn test_error_display_entity_not_found() {
120        let err = MigrationError::EntityNotFound("user".to_string());
121        let display = format!("{}", err);
122        assert!(display.contains("Entity 'user' not found"));
123    }
124
125    #[test]
126    fn test_error_display_migration_path_not_defined() {
127        let err = MigrationError::MigrationPathNotDefined {
128            entity: "task".to_string(),
129            version: "2.0.0".to_string(),
130        };
131        let display = format!("{}", err);
132        assert!(display.contains("No migration path defined"));
133        assert!(display.contains("task"));
134        assert!(display.contains("2.0.0"));
135    }
136
137    #[test]
138    fn test_error_display_migration_step_failed() {
139        let err = MigrationError::MigrationStepFailed {
140            from: "1.0.0".to_string(),
141            to: "2.0.0".to_string(),
142            error: "field missing".to_string(),
143        };
144        let display = format!("{}", err);
145        assert!(display.contains("Migration failed"));
146        assert!(display.contains("1.0.0"));
147        assert!(display.contains("2.0.0"));
148        assert!(display.contains("field missing"));
149    }
150
151    #[test]
152    fn test_error_debug() {
153        let err = MigrationError::EntityNotFound("test".to_string());
154        let debug = format!("{:?}", err);
155        assert!(debug.contains("EntityNotFound"));
156    }
157
158    #[test]
159    fn test_error_is_std_error() {
160        let err = MigrationError::DeserializationError("test".to_string());
161        // MigrationError should implement std::error::Error
162        let _: &dyn std::error::Error = &err;
163    }
164
165    #[test]
166    fn test_error_display_circular_migration_path() {
167        let err = MigrationError::CircularMigrationPath {
168            entity: "task".to_string(),
169            path: "1.0.0 -> 2.0.0 -> 1.0.0".to_string(),
170        };
171        let display = format!("{}", err);
172        assert!(display.contains("Circular migration path"));
173        assert!(display.contains("task"));
174        assert!(display.contains("1.0.0 -> 2.0.0 -> 1.0.0"));
175    }
176
177    #[test]
178    fn test_error_display_invalid_version_order() {
179        let err = MigrationError::InvalidVersionOrder {
180            entity: "task".to_string(),
181            from: "2.0.0".to_string(),
182            to: "1.0.0".to_string(),
183        };
184        let display = format!("{}", err);
185        assert!(display.contains("Invalid version order"));
186        assert!(display.contains("task"));
187        assert!(display.contains("2.0.0"));
188        assert!(display.contains("1.0.0"));
189        assert!(display.contains("must increase"));
190    }
191
192    #[test]
193    fn test_error_display_io_error_without_context() {
194        let err = MigrationError::Store(StoreError::IoError {
195            operation: IoOperationKind::Read,
196            path: "/path/to/file.toml".to_string(),
197            context: None,
198            error: "Permission denied".to_string(),
199        });
200        let display = format!("{}", err);
201        assert!(display.contains("Failed to read"));
202        assert!(display.contains("/path/to/file.toml"));
203        assert!(display.contains("Permission denied"));
204    }
205
206    #[test]
207    fn test_error_display_io_error_with_context() {
208        let err = MigrationError::Store(StoreError::IoError {
209            operation: IoOperationKind::Write,
210            path: "/path/to/tmp.toml".to_string(),
211            context: Some("temporary file".to_string()),
212            error: "Disk full".to_string(),
213        });
214        let display = format!("{}", err);
215        assert!(display.contains("Failed to write"));
216        assert!(display.contains("temporary file"));
217        assert!(display.contains("/path/to/tmp.toml"));
218        assert!(display.contains("Disk full"));
219    }
220
221    #[test]
222    fn test_error_display_io_error_rename_with_retries() {
223        let err = MigrationError::Store(StoreError::IoError {
224            operation: IoOperationKind::Rename,
225            path: "/path/to/file.toml".to_string(),
226            context: Some("after 3 retries".to_string()),
227            error: "Resource temporarily unavailable".to_string(),
228        });
229        let display = format!("{}", err);
230        assert!(display.contains("Failed to rename"));
231        assert!(display.contains("after 3 retries"));
232        assert!(display.contains("/path/to/file.toml"));
233        assert!(display.contains("Resource temporarily unavailable"));
234    }
235}