use std::collections::{HashMap, VecDeque};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AspectVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl AspectVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn parse(s: &str) -> Result<Self, String> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(format!(
"invalid version format '{}': expected 'major.minor.patch'",
s
));
}
let major = parts[0]
.parse::<u32>()
.map_err(|_| format!("invalid major component '{}' in '{}'", parts[0], s))?;
let minor = parts[1]
.parse::<u32>()
.map_err(|_| format!("invalid minor component '{}' in '{}'", parts[1], s))?;
let patch = parts[2]
.parse::<u32>()
.map_err(|_| format!("invalid patch component '{}' in '{}'", parts[2], s))?;
Ok(Self {
major,
minor,
patch,
})
}
pub fn is_compatible_with(&self, other: &AspectVersion) -> bool {
if self.major != other.major {
return false;
}
if self.minor > other.minor {
return true;
}
if self.minor == other.minor && self.patch >= other.patch {
return true;
}
false
}
pub fn is_breaking_change(&self, other: &AspectVersion) -> bool {
self.major != other.major
}
}
impl fmt::Display for AspectVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MigrationStep {
pub from: AspectVersion,
pub to: AspectVersion,
pub description: String,
pub breaking: bool,
}
#[derive(Debug, Clone)]
pub struct VersionedAspect<T> {
pub aspect: T,
pub version: AspectVersion,
pub created_at: u64,
pub deprecated: bool,
}
impl<T> VersionedAspect<T> {
pub fn new(aspect: T, version: AspectVersion, created_at: u64) -> Self {
Self {
aspect,
version,
created_at,
deprecated: false,
}
}
pub fn deprecate(mut self) -> Self {
self.deprecated = true;
self
}
pub fn is_current(&self, current: &AspectVersion) -> bool {
!self.deprecated && self.version == *current
}
pub fn migration_path(
from: &AspectVersion,
to: &AspectVersion,
registry: &AspectMigrationRegistry,
) -> Vec<MigrationStep> {
registry.find_path(from, to).unwrap_or_default()
}
}
#[derive(Debug, Default)]
pub struct AspectMigrationRegistry {
steps: Vec<MigrationStep>,
}
impl AspectMigrationRegistry {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn register_migration(&mut self, step: MigrationStep) {
self.steps.push(step);
}
pub fn find_path(
&self,
from: &AspectVersion,
to: &AspectVersion,
) -> Option<Vec<MigrationStep>> {
if from == to {
return Some(Vec::new());
}
let mut visited: HashMap<AspectVersion, bool> = HashMap::new();
let mut queue: VecDeque<(AspectVersion, Vec<MigrationStep>)> = VecDeque::new();
queue.push_back((from.clone(), Vec::new()));
visited.insert(from.clone(), true);
while let Some((current, path)) = queue.pop_front() {
for step in &self.steps {
if step.from == current {
if visited.contains_key(&step.to) {
continue;
}
let mut new_path = path.clone();
new_path.push(step.clone());
if step.to == *to {
return Some(new_path);
}
visited.insert(step.to.clone(), true);
queue.push_back((step.to.clone(), new_path));
}
}
}
None
}
pub fn can_migrate(&self, from: &AspectVersion, to: &AspectVersion) -> bool {
self.find_path(from, to).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &str) -> AspectVersion {
AspectVersion::parse(s).expect("valid version string")
}
#[test]
fn test_aspect_version_new() {
let ver = AspectVersion::new(1, 2, 3);
assert_eq!(ver.major, 1);
assert_eq!(ver.minor, 2);
assert_eq!(ver.patch, 3);
}
#[test]
fn test_aspect_version_parse_valid() {
let ver = v("2.5.11");
assert_eq!(ver.major, 2);
assert_eq!(ver.minor, 5);
assert_eq!(ver.patch, 11);
}
#[test]
fn test_aspect_version_parse_invalid_format() {
let result = AspectVersion::parse("1.2");
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("invalid version format"));
}
#[test]
fn test_aspect_version_parse_non_numeric() {
let result = AspectVersion::parse("1.x.3");
assert!(result.is_err());
}
#[test]
fn test_aspect_version_parse_too_many_parts() {
let result = AspectVersion::parse("1.2.3.4");
assert!(result.is_err());
}
#[test]
fn test_aspect_version_is_compatible_same_version() {
let ver = v("1.0.0");
assert!(ver.is_compatible_with(&ver));
}
#[test]
fn test_aspect_version_is_compatible_minor_higher() {
let newer = v("1.2.0");
let older = v("1.1.0");
assert!(newer.is_compatible_with(&older));
}
#[test]
fn test_aspect_version_is_compatible_patch_higher() {
let newer = v("1.0.5");
let older = v("1.0.3");
assert!(newer.is_compatible_with(&older));
}
#[test]
fn test_aspect_version_not_compatible_different_major() {
let v2 = v("2.0.0");
let v1 = v("1.0.0");
assert!(!v2.is_compatible_with(&v1));
assert!(!v1.is_compatible_with(&v2));
}
#[test]
fn test_aspect_version_not_compatible_older_minor() {
let older = v("1.0.0");
let newer = v("1.1.0");
assert!(!older.is_compatible_with(&newer));
}
#[test]
fn test_aspect_version_is_breaking_change_major_bump() {
let v2 = v("2.0.0");
let v1 = v("1.0.0");
assert!(v2.is_breaking_change(&v1));
}
#[test]
fn test_aspect_version_not_breaking_minor_bump() {
let v1_1 = v("1.1.0");
let v1_0 = v("1.0.0");
assert!(!v1_1.is_breaking_change(&v1_0));
}
#[test]
fn test_aspect_version_not_breaking_patch_bump() {
let v1_0_1 = v("1.0.1");
let v1_0_0 = v("1.0.0");
assert!(!v1_0_1.is_breaking_change(&v1_0_0));
}
#[test]
fn test_aspect_version_display() {
let ver = AspectVersion::new(3, 4, 5);
assert_eq!(ver.to_string(), "3.4.5");
}
#[test]
fn test_aspect_version_ordering() {
let mut versions = [v("1.2.0"), v("1.0.0"), v("2.0.0"), v("1.1.0")];
versions.sort();
assert_eq!(versions[0], v("1.0.0"));
assert_eq!(versions[1], v("1.1.0"));
assert_eq!(versions[2], v("1.2.0"));
assert_eq!(versions[3], v("2.0.0"));
}
#[test]
fn test_migration_step_fields() {
let step = MigrationStep {
from: v("1.0.0"),
to: v("2.0.0"),
description: "Breaking update".to_string(),
breaking: true,
};
assert_eq!(step.from, v("1.0.0"));
assert_eq!(step.to, v("2.0.0"));
assert!(step.breaking);
assert_eq!(step.description, "Breaking update");
}
#[test]
fn test_versioned_aspect_new() {
let va: VersionedAspect<String> =
VersionedAspect::new("MyAspect".to_string(), v("1.0.0"), 1_700_000_000);
assert_eq!(va.aspect, "MyAspect");
assert_eq!(va.version, v("1.0.0"));
assert_eq!(va.created_at, 1_700_000_000);
assert!(!va.deprecated);
}
#[test]
fn test_versioned_aspect_deprecate() {
let va: VersionedAspect<String> =
VersionedAspect::new("A".to_string(), v("1.0.0"), 0).deprecate();
assert!(va.deprecated);
}
#[test]
fn test_versioned_aspect_is_current_true() {
let va: VersionedAspect<i32> = VersionedAspect::new(42, v("1.2.3"), 0);
assert!(va.is_current(&v("1.2.3")));
}
#[test]
fn test_versioned_aspect_is_current_false_deprecated() {
let va: VersionedAspect<i32> = VersionedAspect::new(42, v("1.0.0"), 0).deprecate();
assert!(!va.is_current(&v("1.0.0")));
}
#[test]
fn test_versioned_aspect_is_current_false_different_version() {
let va: VersionedAspect<i32> = VersionedAspect::new(42, v("1.0.0"), 0);
assert!(!va.is_current(&v("2.0.0")));
}
#[test]
fn test_migration_registry_empty_path_same_version() {
let registry = AspectMigrationRegistry::new();
let path = registry.find_path(&v("1.0.0"), &v("1.0.0"));
assert_eq!(path, Some(vec![]));
}
#[test]
fn test_migration_registry_direct_path() {
let mut registry = AspectMigrationRegistry::new();
registry.register_migration(MigrationStep {
from: v("1.0.0"),
to: v("2.0.0"),
description: "Major upgrade".to_string(),
breaking: true,
});
let path = registry
.find_path(&v("1.0.0"), &v("2.0.0"))
.expect("path should exist");
assert_eq!(path.len(), 1);
assert_eq!(path[0].from, v("1.0.0"));
assert_eq!(path[0].to, v("2.0.0"));
}
#[test]
fn test_migration_registry_multi_step_path() {
let mut registry = AspectMigrationRegistry::new();
registry.register_migration(MigrationStep {
from: v("1.0.0"),
to: v("1.1.0"),
description: "Feature add".to_string(),
breaking: false,
});
registry.register_migration(MigrationStep {
from: v("1.1.0"),
to: v("2.0.0"),
description: "Major upgrade".to_string(),
breaking: true,
});
let path = registry
.find_path(&v("1.0.0"), &v("2.0.0"))
.expect("path should exist");
assert_eq!(path.len(), 2);
assert_eq!(path[0].from, v("1.0.0"));
assert_eq!(path[1].to, v("2.0.0"));
}
#[test]
fn test_migration_registry_no_path() {
let registry = AspectMigrationRegistry::new();
let path = registry.find_path(&v("1.0.0"), &v("3.0.0"));
assert!(path.is_none());
}
#[test]
fn test_can_migrate_true() {
let mut registry = AspectMigrationRegistry::new();
registry.register_migration(MigrationStep {
from: v("1.0.0"),
to: v("2.0.0"),
description: "".to_string(),
breaking: true,
});
assert!(registry.can_migrate(&v("1.0.0"), &v("2.0.0")));
}
#[test]
fn test_can_migrate_false() {
let registry = AspectMigrationRegistry::new();
assert!(!registry.can_migrate(&v("1.0.0"), &v("9.9.9")));
}
#[test]
fn test_migration_path_via_versioned_aspect() {
let mut registry = AspectMigrationRegistry::new();
registry.register_migration(MigrationStep {
from: v("1.0.0"),
to: v("2.0.0"),
description: "bump".to_string(),
breaking: true,
});
let path = VersionedAspect::<()>::migration_path(&v("1.0.0"), &v("2.0.0"), ®istry);
assert_eq!(path.len(), 1);
}
#[test]
fn test_migration_path_no_path_returns_empty() {
let registry = AspectMigrationRegistry::new();
let path = VersionedAspect::<()>::migration_path(&v("1.0.0"), &v("5.0.0"), ®istry);
assert!(path.is_empty());
}
#[test]
fn test_migration_registry_three_hop_path() {
let mut registry = AspectMigrationRegistry::new();
registry.register_migration(MigrationStep {
from: v("1.0.0"),
to: v("1.1.0"),
description: "step 1".to_string(),
breaking: false,
});
registry.register_migration(MigrationStep {
from: v("1.1.0"),
to: v("1.2.0"),
description: "step 2".to_string(),
breaking: false,
});
registry.register_migration(MigrationStep {
from: v("1.2.0"),
to: v("2.0.0"),
description: "step 3".to_string(),
breaking: true,
});
let path = registry
.find_path(&v("1.0.0"), &v("2.0.0"))
.expect("path should exist");
assert_eq!(path.len(), 3);
}
#[test]
fn test_aspect_version_equality() {
assert_eq!(v("1.2.3"), v("1.2.3"));
assert_ne!(v("1.2.3"), v("1.2.4"));
}
#[test]
fn test_migration_step_non_breaking() {
let step = MigrationStep {
from: v("1.0.0"),
to: v("1.1.0"),
description: "minor bump".to_string(),
breaking: false,
};
assert!(!step.breaking);
}
}