#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WorkflowVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl WorkflowVersion {
#[must_use]
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return None;
}
let major = parts[0].parse().ok()?;
let minor = parts[1].parse().ok()?;
let patch = parts[2].parse().ok()?;
Some(Self {
major,
minor,
patch,
})
}
#[must_use]
pub fn is_compatible_with(&self, other: &Self) -> bool {
self.major == other.major && other.minor >= self.minor
}
#[must_use]
pub fn to_string_repr(&self) -> String {
format!("{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl std::fmt::Display for WorkflowVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone)]
pub struct VersionMigration {
pub from: WorkflowVersion,
pub to: WorkflowVersion,
pub description: String,
}
impl VersionMigration {
#[must_use]
pub fn new(from: WorkflowVersion, to: WorkflowVersion, description: impl Into<String>) -> Self {
Self {
from,
to,
description: description.into(),
}
}
#[must_use]
pub fn from_version(&self) -> &WorkflowVersion {
&self.from
}
#[must_use]
pub fn to_version(&self) -> &WorkflowVersion {
&self.to
}
#[must_use]
pub fn is_major_migration(&self) -> bool {
self.from.major != self.to.major
}
}
#[derive(Debug, Default)]
pub struct WorkflowVersionRegistry {
versions: Vec<WorkflowVersion>,
migrations: HashMap<(WorkflowVersion, WorkflowVersion), VersionMigration>,
}
impl WorkflowVersionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, version: WorkflowVersion) {
if !self.versions.contains(&version) {
self.versions.push(version.clone());
self.versions.sort();
}
}
pub fn add_migration(&mut self, migration: VersionMigration) {
self.register(migration.from.clone());
self.register(migration.to.clone());
let key = (migration.from.clone(), migration.to.clone());
self.migrations.insert(key, migration);
}
#[must_use]
pub fn latest(&self) -> Option<&WorkflowVersion> {
self.versions.last()
}
#[must_use]
pub fn migrate(
&self,
from: &WorkflowVersion,
to: &WorkflowVersion,
) -> Option<&VersionMigration> {
self.migrations.get(&(from.clone(), to.clone()))
}
#[must_use]
pub fn all_versions(&self) -> &[WorkflowVersion] {
&self.versions
}
#[must_use]
pub fn all_migrations(&self) -> Vec<&VersionMigration> {
self.migrations.values().collect()
}
#[must_use]
pub fn version_count(&self) -> usize {
self.versions.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_new_and_display() {
let v = WorkflowVersion::new(1, 2, 3);
assert_eq!(v.to_string(), "1.2.3");
}
#[test]
fn test_version_parse_valid() {
let v = WorkflowVersion::parse("2.0.1").expect("should succeed in test");
assert_eq!(v.major, 2);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 1);
}
#[test]
fn test_version_parse_invalid() {
assert!(WorkflowVersion::parse("1.2").is_none());
assert!(WorkflowVersion::parse("abc").is_none());
assert!(WorkflowVersion::parse("1.x.3").is_none());
}
#[test]
fn test_version_ordering() {
let v1 = WorkflowVersion::new(1, 0, 0);
let v2 = WorkflowVersion::new(1, 1, 0);
let v3 = WorkflowVersion::new(2, 0, 0);
assert!(v1 < v2);
assert!(v2 < v3);
}
#[test]
fn test_is_compatible_with_same_major() {
let v1 = WorkflowVersion::new(1, 2, 0);
let v2 = WorkflowVersion::new(1, 3, 0);
assert!(v1.is_compatible_with(&v2)); }
#[test]
fn test_is_compatible_with_older_minor() {
let v1 = WorkflowVersion::new(1, 3, 0);
let v2 = WorkflowVersion::new(1, 2, 0);
assert!(!v1.is_compatible_with(&v2)); }
#[test]
fn test_is_compatible_different_major() {
let v1 = WorkflowVersion::new(1, 0, 0);
let v2 = WorkflowVersion::new(2, 0, 0);
assert!(!v1.is_compatible_with(&v2));
}
#[test]
fn test_migration_from_version() {
let from = WorkflowVersion::new(1, 0, 0);
let to = WorkflowVersion::new(1, 1, 0);
let m = VersionMigration::new(from.clone(), to, "add field");
assert_eq!(m.from_version(), &from);
}
#[test]
fn test_migration_is_major() {
let m = VersionMigration::new(
WorkflowVersion::new(1, 0, 0),
WorkflowVersion::new(2, 0, 0),
"major bump",
);
assert!(m.is_major_migration());
}
#[test]
fn test_migration_not_major() {
let m = VersionMigration::new(
WorkflowVersion::new(1, 0, 0),
WorkflowVersion::new(1, 1, 0),
"minor bump",
);
assert!(!m.is_major_migration());
}
#[test]
fn test_registry_register_and_latest() {
let mut reg = WorkflowVersionRegistry::new();
reg.register(WorkflowVersion::new(1, 0, 0));
reg.register(WorkflowVersion::new(1, 1, 0));
reg.register(WorkflowVersion::new(2, 0, 0));
assert_eq!(
reg.latest().expect("should succeed in test"),
&WorkflowVersion::new(2, 0, 0)
);
}
#[test]
fn test_registry_add_migration_and_lookup() {
let mut reg = WorkflowVersionRegistry::new();
let from = WorkflowVersion::new(1, 0, 0);
let to = WorkflowVersion::new(1, 1, 0);
reg.add_migration(VersionMigration::new(from.clone(), to.clone(), "test"));
let m = reg.migrate(&from, &to).expect("should succeed in test");
assert_eq!(m.description, "test");
}
#[test]
fn test_registry_missing_migration() {
let reg = WorkflowVersionRegistry::new();
let v = WorkflowVersion::new(1, 0, 0);
assert!(reg.migrate(&v, &v).is_none());
}
#[test]
fn test_registry_version_count() {
let mut reg = WorkflowVersionRegistry::new();
reg.register(WorkflowVersion::new(1, 0, 0));
reg.register(WorkflowVersion::new(1, 0, 0)); assert_eq!(reg.version_count(), 1);
}
#[test]
fn test_registry_empty_latest() {
let reg = WorkflowVersionRegistry::new();
assert!(reg.latest().is_none());
}
}