use crate::error::{ConfigError, ConfigResult};
use crate::types::AnnotatedValue;
use std::collections::{BTreeMap, HashMap};
pub trait Versioned {
const VERSION: u32;
}
pub type MigrationFn = Box<dyn FnMut(AnnotatedValue) -> ConfigResult<AnnotatedValue> + Send + Sync>;
pub struct MigrationRegistry {
migrations: BTreeMap<(u32, u32), MigrationFn>,
path_cache: HashMap<(u32, u32), Vec<(u32, u32)>>,
versions: BTreeMap<u32, Vec<u32>>,
}
impl MigrationRegistry {
pub fn new() -> Self {
Self {
migrations: BTreeMap::new(),
path_cache: HashMap::new(),
versions: BTreeMap::new(),
}
}
pub fn builder() -> MigrationRegistryBuilder {
MigrationRegistryBuilder::new()
}
pub fn with_migrations(migrations: HashMap<(u32, u32), MigrationFn>) -> Self {
let mut registry = Self::new();
for ((from, to), f) in migrations {
registry.register(from, to, f);
}
registry
}
pub fn register<F>(&mut self, from: u32, to: u32, f: F) -> &mut Self
where
F: FnMut(AnnotatedValue) -> ConfigResult<AnnotatedValue> + Send + Sync + 'static,
{
let boxed: MigrationFn = Box::new(f);
self.migrations.insert((from, to), boxed);
self.versions.entry(from).or_default().push(to);
self.versions.entry(to).or_default();
self.path_cache.clear();
self
}
pub fn migrations(&self) -> &BTreeMap<(u32, u32), MigrationFn> {
&self.migrations
}
pub fn precompute_paths(&mut self) {
for (&from, targets) in &self.versions {
for &to in targets {
self.path_cache.insert((from, to), vec![(from, to)]);
}
}
let all_versions: std::collections::HashSet<u32> = {
let mut set = std::collections::HashSet::new();
for (&from, targets) in &self.versions {
set.insert(from);
for &to in targets {
set.insert(to);
}
}
set
};
let versions: Vec<u32> = all_versions.into_iter().collect();
for _ in 0..versions.len() {
let mut updated = false;
for &mid in &versions {
for &from in &versions {
for &to in &versions {
if from == to {
continue;
}
let path_from_mid = self.path_cache.get(&(from, mid)).cloned();
let path_mid_to = self.path_cache.get(&(mid, to)).cloned();
if let (Some(p1), Some(p2)) = (path_from_mid, path_mid_to) {
let new_path_len = p1.len() + p2.len();
let should_update = match self.path_cache.get(&(from, to)) {
Some(existing) => new_path_len < existing.len(),
None => true,
};
if should_update {
let mut new_path = p1;
new_path.extend(p2);
self.path_cache.insert((from, to), new_path);
updated = true;
}
}
}
}
}
if !updated {
break;
}
}
}
pub fn get_migration_path(&self, from: u32, to: u32) -> Option<Vec<u32>> {
if from == to {
return Some(vec![from]);
}
let edges = self.path_cache.get(&(from, to))?;
let mut versions = vec![from];
for &(_v_from, v_to) in edges {
versions.push(v_to);
}
Some(versions)
}
pub fn migrate(
&mut self,
mut value: AnnotatedValue,
from: u32,
to: u32,
) -> ConfigResult<AnnotatedValue> {
if from == to {
return Ok(value);
}
let path = self
.get_migration_path(from, to)
.ok_or_else(|| ConfigError::migration_not_found(from, to))?;
for i in 0..path.len() - 1 {
let current_version = path[i];
let next_version = path[i + 1];
let migration = self
.migrations
.get_mut(&(current_version, next_version))
.ok_or_else(|| ConfigError::migration_not_found(current_version, next_version))?;
value = migration(value)?;
}
Ok(value)
}
}
impl Default for MigrationRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct MigrationRegistryBuilder {
migrations: HashMap<(u32, u32), MigrationFn>,
}
impl MigrationRegistryBuilder {
pub fn new() -> Self {
Self {
migrations: HashMap::new(),
}
}
pub fn register<F>(mut self, from: u32, to: u32, f: F) -> Self
where
F: FnMut(AnnotatedValue) -> ConfigResult<AnnotatedValue> + Send + Sync + 'static,
{
let boxed: MigrationFn = Box::new(f);
self.migrations.insert((from, to), boxed);
self
}
pub fn build(self) -> MigrationRegistry {
MigrationRegistry::with_migrations(self.migrations)
}
}
impl Default for MigrationRegistryBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MigrationOnReload {
Always,
#[default]
OnVersionChange,
Disabled,
}
impl ConfigError {
pub fn migration_not_found(from: u32, to: u32) -> Self {
ConfigError::MigrationFailed {
from,
to,
reason: format!(
"No migration path found from version {} to version {}",
from, to
),
source: None,
}
}
pub fn migration_failed(from: u32, to: u32, reason: impl Into<String>) -> Self {
ConfigError::MigrationFailed {
from,
to,
reason: reason.into(),
source: None,
}
}
}