pub trait Migratable: Sized {
const CURRENT_VERSION: u8;
fn migrate_from(json: &str, from_version: u8) -> Result<Self, MigrationError>;
}
#[derive(Debug, Clone)]
pub enum MigrationError {
UnknownVersion { found: u8, max_supported: u8 },
DeserializationFailed(String),
MigrationStepFailed { from: u8, to: u8, reason: String },
}
impl core::fmt::Display for MigrationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownVersion {
found,
max_supported,
} => {
write!(
f,
"Unknown schema version {} (max supported: {})",
found, max_supported
)
}
Self::DeserializationFailed(msg) => write!(f, "Deserialization failed: {}", msg),
Self::MigrationStepFailed { from, to, reason } => {
write!(f, "Migration v{} → v{} failed: {}", from, to, reason)
}
}
}
}
pub const fn needs_migration(entry_version: u8, current_version: u8) -> bool {
entry_version < current_version
}
pub const fn is_future_version(entry_version: u8, current_version: u8) -> bool {
entry_version > current_version
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn needs_migration_detects_old_version() {
assert!(needs_migration(0, 1));
assert!(needs_migration(1, 2));
assert!(!needs_migration(1, 1));
assert!(!needs_migration(2, 1));
}
#[test]
fn is_future_version_detects_newer() {
assert!(is_future_version(2, 1));
assert!(!is_future_version(1, 1));
assert!(!is_future_version(0, 1));
}
#[test]
fn migration_error_display() {
let err = MigrationError::UnknownVersion {
found: 5,
max_supported: 3,
};
assert!(err.to_string().contains("5"));
assert!(err.to_string().contains("3"));
}
}