version_migrate/
errors.rs1use thiserror::Error;
4
5pub use local_store::{IoOperationKind, StoreError};
6
7#[derive(Error, Debug)]
9#[non_exhaustive]
10pub enum MigrationError {
11 #[error("Failed to deserialize: {0}")]
13 DeserializationError(String),
14
15 #[error("Failed to serialize: {0}")]
17 SerializationError(String),
18
19 #[error("Entity '{0}' not found")]
21 EntityNotFound(String),
22
23 #[error("No migration path defined for entity '{entity}' version '{version}'")]
25 MigrationPathNotDefined {
26 entity: String,
28 version: String,
30 },
31
32 #[error("Migration failed from '{from}' to '{to}': {error}")]
34 MigrationStepFailed {
35 from: String,
37 to: String,
39 error: String,
41 },
42
43 #[error("Circular migration path detected in entity '{entity}': {path}")]
45 CircularMigrationPath {
46 entity: String,
48 path: String,
50 },
51
52 #[error("Invalid version order in entity '{entity}': '{from}' -> '{to}' (versions must increase according to semver)")]
54 InvalidVersionOrder {
55 entity: String,
57 from: String,
59 to: String,
61 },
62
63 #[error("Failed to acquire file lock for '{path}': {error}")]
65 LockError {
66 path: String,
68 error: String,
70 },
71
72 #[error("Failed to parse TOML: {0}")]
74 TomlParseError(String),
75
76 #[error("Failed to serialize to TOML: {0}")]
78 TomlSerializeError(String),
79
80 #[error("Failed to resolve path: {0}")]
82 PathResolution(String),
83
84 #[error("Failed to encode filename for ID '{id}': {reason}")]
86 FilenameEncoding {
87 id: String,
89 reason: String,
91 },
92
93 #[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 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}