use thiserror::Error;
pub use local_store::{IoOperationKind, StoreError};
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum MigrationError {
#[error("Failed to deserialize: {0}")]
DeserializationError(String),
#[error("Failed to serialize: {0}")]
SerializationError(String),
#[error("Entity '{0}' not found")]
EntityNotFound(String),
#[error("No migration path defined for entity '{entity}' version '{version}'")]
MigrationPathNotDefined {
entity: String,
version: String,
},
#[error("Migration failed from '{from}' to '{to}': {error}")]
MigrationStepFailed {
from: String,
to: String,
error: String,
},
#[error("Circular migration path detected in entity '{entity}': {path}")]
CircularMigrationPath {
entity: String,
path: String,
},
#[error("Invalid version order in entity '{entity}': '{from}' -> '{to}' (versions must increase according to semver)")]
InvalidVersionOrder {
entity: String,
from: String,
to: String,
},
#[error("Failed to acquire file lock for '{path}': {error}")]
LockError {
path: String,
error: String,
},
#[error("Failed to parse TOML: {0}")]
TomlParseError(String),
#[error("Failed to serialize to TOML: {0}")]
TomlSerializeError(String),
#[error("Failed to resolve path: {0}")]
PathResolution(String),
#[error("Failed to encode filename for ID '{id}': {reason}")]
FilenameEncoding {
id: String,
reason: String,
},
#[error(transparent)]
Store(#[from] StoreError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display_deserialization() {
let err = MigrationError::DeserializationError("invalid JSON".to_string());
let display = format!("{}", err);
assert!(display.contains("Failed to deserialize"));
assert!(display.contains("invalid JSON"));
}
#[test]
fn test_error_display_serialization() {
let err = MigrationError::SerializationError("invalid data".to_string());
let display = format!("{}", err);
assert!(display.contains("Failed to serialize"));
assert!(display.contains("invalid data"));
}
#[test]
fn test_error_display_entity_not_found() {
let err = MigrationError::EntityNotFound("user".to_string());
let display = format!("{}", err);
assert!(display.contains("Entity 'user' not found"));
}
#[test]
fn test_error_display_migration_path_not_defined() {
let err = MigrationError::MigrationPathNotDefined {
entity: "task".to_string(),
version: "2.0.0".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("No migration path defined"));
assert!(display.contains("task"));
assert!(display.contains("2.0.0"));
}
#[test]
fn test_error_display_migration_step_failed() {
let err = MigrationError::MigrationStepFailed {
from: "1.0.0".to_string(),
to: "2.0.0".to_string(),
error: "field missing".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Migration failed"));
assert!(display.contains("1.0.0"));
assert!(display.contains("2.0.0"));
assert!(display.contains("field missing"));
}
#[test]
fn test_error_debug() {
let err = MigrationError::EntityNotFound("test".to_string());
let debug = format!("{:?}", err);
assert!(debug.contains("EntityNotFound"));
}
#[test]
fn test_error_is_std_error() {
let err = MigrationError::DeserializationError("test".to_string());
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_error_display_circular_migration_path() {
let err = MigrationError::CircularMigrationPath {
entity: "task".to_string(),
path: "1.0.0 -> 2.0.0 -> 1.0.0".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Circular migration path"));
assert!(display.contains("task"));
assert!(display.contains("1.0.0 -> 2.0.0 -> 1.0.0"));
}
#[test]
fn test_error_display_invalid_version_order() {
let err = MigrationError::InvalidVersionOrder {
entity: "task".to_string(),
from: "2.0.0".to_string(),
to: "1.0.0".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Invalid version order"));
assert!(display.contains("task"));
assert!(display.contains("2.0.0"));
assert!(display.contains("1.0.0"));
assert!(display.contains("must increase"));
}
#[test]
fn test_error_display_io_error_without_context() {
let err = MigrationError::Store(StoreError::IoError {
operation: IoOperationKind::Read,
path: "/path/to/file.toml".to_string(),
context: None,
error: "Permission denied".to_string(),
});
let display = format!("{}", err);
assert!(display.contains("Failed to read"));
assert!(display.contains("/path/to/file.toml"));
assert!(display.contains("Permission denied"));
}
#[test]
fn test_error_display_io_error_with_context() {
let err = MigrationError::Store(StoreError::IoError {
operation: IoOperationKind::Write,
path: "/path/to/tmp.toml".to_string(),
context: Some("temporary file".to_string()),
error: "Disk full".to_string(),
});
let display = format!("{}", err);
assert!(display.contains("Failed to write"));
assert!(display.contains("temporary file"));
assert!(display.contains("/path/to/tmp.toml"));
assert!(display.contains("Disk full"));
}
#[test]
fn test_error_display_io_error_rename_with_retries() {
let err = MigrationError::Store(StoreError::IoError {
operation: IoOperationKind::Rename,
path: "/path/to/file.toml".to_string(),
context: Some("after 3 retries".to_string()),
error: "Resource temporarily unavailable".to_string(),
});
let display = format!("{}", err);
assert!(display.contains("Failed to rename"));
assert!(display.contains("after 3 retries"));
assert!(display.contains("/path/to/file.toml"));
assert!(display.contains("Resource temporarily unavailable"));
}
}