use std::collections::BTreeMap;
use serde_json::Value;
use thiserror::Error;
use crate::genome::normalizers::{NormalizationDiagnostics, Normalizer};
use crate::genome::schema::GenomeSchemaVersion;
use crate::genome::validators::{ValidationReport, Validator};
pub mod chain;
pub mod v2_to_v3;
pub use chain::ChainRunner;
pub use v2_to_v3::V2ToV3Migrator;
#[derive(Debug, Error)]
pub enum MigrationError {
#[error("Migrator '{name}' ({from} -> {to}) failed: {reason}")]
StepFailed {
name: &'static str,
from: GenomeSchemaVersion,
to: GenomeSchemaVersion,
reason: String,
},
#[error("No migrator registered with from_version={from} (needed to reach v{target})")]
MissingMigrator {
from: GenomeSchemaVersion,
target: GenomeSchemaVersion,
},
#[error("Registry violates the contiguity invariant: {0}")]
InvalidRegistry(String),
#[error("Cannot migrate downward: genome is at v{from} but target is v{target}")]
DowngradeRefused {
from: GenomeSchemaVersion,
target: GenomeSchemaVersion,
},
#[error("Failed to detect genome schema version: {0}")]
DetectionFailed(String),
}
#[derive(Debug, Clone)]
pub struct MigrationStepDiagnostics {
pub from_version: GenomeSchemaVersion,
pub to_version: GenomeSchemaVersion,
pub transformations: Vec<String>,
}
impl MigrationStepDiagnostics {
pub fn new(from: GenomeSchemaVersion, to: GenomeSchemaVersion) -> Self {
Self {
from_version: from,
to_version: to,
transformations: Vec::new(),
}
}
pub fn record(&mut self, msg: impl Into<String>) {
self.transformations.push(msg.into());
}
}
#[allow(clippy::wrong_self_convention)]
pub trait Migrator: Send + Sync {
fn from_version(&self) -> GenomeSchemaVersion;
fn to_version(&self) -> GenomeSchemaVersion;
fn name(&self) -> &'static str;
fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError>;
}
#[derive(Debug, Clone)]
pub struct ChainResult {
pub from_version: GenomeSchemaVersion,
pub to_version: GenomeSchemaVersion,
pub migrators_applied: Vec<&'static str>,
pub normalizers_applied: Vec<&'static str>,
pub per_step_diagnostics: Vec<MigrationStepDiagnostics>,
pub per_normalizer_diagnostics: Vec<NormalizationDiagnostics>,
pub advisory_warnings: Vec<String>,
pub blocking_errors: Vec<String>,
}
impl ChainResult {
pub fn is_blocking_clean(&self) -> bool {
self.blocking_errors.is_empty()
}
}
pub struct ChainRegistry {
migrators: BTreeMap<u32, Box<dyn Migrator>>,
normalizers: BTreeMap<u32, Box<dyn Normalizer>>,
validators: BTreeMap<u32, Box<dyn Validator>>,
}
impl ChainRegistry {
pub fn new() -> Self {
Self {
migrators: BTreeMap::new(),
normalizers: BTreeMap::new(),
validators: BTreeMap::new(),
}
}
pub fn register_migrator(&mut self, migrator: Box<dyn Migrator>) -> Result<(), MigrationError> {
let from = migrator.from_version();
let to = migrator.to_version();
if to.as_u32() != from.as_u32().saturating_add(1) {
return Err(MigrationError::InvalidRegistry(format!(
"migrator '{}' declares from={} to={}, expected to=from+1",
migrator.name(),
from,
to
)));
}
if self.migrators.contains_key(&from.as_u32()) {
return Err(MigrationError::InvalidRegistry(format!(
"duplicate migrator with from_version={from}"
)));
}
self.migrators.insert(from.as_u32(), migrator);
Ok(())
}
pub fn register_normalizer(
&mut self,
normalizer: Box<dyn Normalizer>,
) -> Result<(), MigrationError> {
let v = normalizer.schema_version();
if self.normalizers.contains_key(&v.as_u32()) {
return Err(MigrationError::InvalidRegistry(format!(
"duplicate normalizer at schema_version={v}"
)));
}
self.normalizers.insert(v.as_u32(), normalizer);
Ok(())
}
pub fn register_validator(&mut self, validator: Box<dyn Validator>) {
let v = validator.schema_version().as_u32();
self.validators.insert(v, validator);
}
pub fn migrator_for(&self, from: GenomeSchemaVersion) -> Option<&dyn Migrator> {
self.migrators.get(&from.as_u32()).map(|b| b.as_ref())
}
pub fn normalizer_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Normalizer> {
self.normalizers.get(&version.as_u32()).map(|b| b.as_ref())
}
pub fn validator_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Validator> {
self.validators.get(&version.as_u32()).map(|b| b.as_ref())
}
pub fn run_validator(&self, version: GenomeSchemaVersion, genome: &Value) -> ValidationReport {
match self.validator_for(version) {
Some(v) => v.validate(genome),
None => ValidationReport::new(version),
}
}
pub fn migrator_count(&self) -> usize {
self.migrators.len()
}
pub fn normalizer_count(&self) -> usize {
self.normalizers.len()
}
pub fn validator_count(&self) -> usize {
self.validators.len()
}
}
impl Default for ChainRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
pub(super) mod test_support {
use super::*;
use serde_json::json;
pub struct SyntheticMigrator {
from: GenomeSchemaVersion,
to: GenomeSchemaVersion,
name: &'static str,
fail: bool,
}
impl SyntheticMigrator {
pub fn ok(from: u32, name: &'static str) -> Box<Self> {
Box::new(Self {
from: GenomeSchemaVersion(from),
to: GenomeSchemaVersion(from + 1),
name,
fail: false,
})
}
pub fn failing(from: u32, name: &'static str) -> Box<Self> {
Box::new(Self {
from: GenomeSchemaVersion(from),
to: GenomeSchemaVersion(from + 1),
name,
fail: true,
})
}
}
impl Migrator for SyntheticMigrator {
fn from_version(&self) -> GenomeSchemaVersion {
self.from
}
fn to_version(&self) -> GenomeSchemaVersion {
self.to
}
fn name(&self) -> &'static str {
self.name
}
fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError> {
if self.fail {
return Err(MigrationError::StepFailed {
name: self.name,
from: self.from,
to: self.to,
reason: "synthetic failure".to_string(),
});
}
let mut diag = MigrationStepDiagnostics::new(self.from, self.to);
let count = genome
.get("step_count")
.and_then(|v| v.as_u64())
.unwrap_or(0)
+ 1;
genome
.as_object_mut()
.expect("test genome must be a JSON object")
.insert("step_count".to_string(), json!(count));
diag.record(format!("incremented step_count to {count}"));
Ok(diag)
}
}
pub fn make_ok(from: u32, name: &'static str) -> Box<dyn Migrator> {
SyntheticMigrator::ok(from, name)
}
pub fn make_failing(from: u32, name: &'static str) -> Box<dyn Migrator> {
SyntheticMigrator::failing(from, name)
}
}
#[cfg(test)]
mod tests {
use super::test_support::SyntheticMigrator;
use super::*;
use serde_json::json;
#[test]
fn registry_accepts_a_well_formed_migrator() {
let mut reg = ChainRegistry::new();
reg.register_migrator(SyntheticMigrator::ok(2, "v2_to_v3"))
.unwrap();
assert_eq!(reg.migrator_count(), 1);
assert!(reg.migrator_for(GenomeSchemaVersion(2)).is_some());
assert!(reg.migrator_for(GenomeSchemaVersion(3)).is_none());
}
#[test]
fn registry_rejects_to_version_not_equal_to_from_plus_one() {
struct Skipping;
impl Migrator for Skipping {
fn from_version(&self) -> GenomeSchemaVersion {
GenomeSchemaVersion(2)
}
fn to_version(&self) -> GenomeSchemaVersion {
GenomeSchemaVersion(4)
}
fn name(&self) -> &'static str {
"skip"
}
fn migrate(
&self,
_genome: &mut Value,
) -> Result<MigrationStepDiagnostics, MigrationError> {
unreachable!()
}
}
let mut reg = ChainRegistry::new();
let err = reg.register_migrator(Box::new(Skipping)).unwrap_err();
assert!(matches!(err, MigrationError::InvalidRegistry(_)));
}
#[test]
fn registry_rejects_duplicate_from_version() {
let mut reg = ChainRegistry::new();
reg.register_migrator(SyntheticMigrator::ok(2, "first"))
.unwrap();
let err = reg
.register_migrator(SyntheticMigrator::ok(2, "second"))
.unwrap_err();
assert!(matches!(err, MigrationError::InvalidRegistry(_)));
}
#[test]
fn migration_step_diagnostics_records_transformations() {
let mut diag =
MigrationStepDiagnostics::new(GenomeSchemaVersion(2), GenomeSchemaVersion(3));
diag.record("converted blueprint keys");
diag.record("renamed legacy fields");
assert_eq!(diag.transformations.len(), 2);
assert_eq!(diag.from_version, GenomeSchemaVersion(2));
assert_eq!(diag.to_version, GenomeSchemaVersion(3));
}
#[test]
fn run_validator_returns_empty_when_unregistered() {
let reg = ChainRegistry::new();
let report = reg.run_validator(GenomeSchemaVersion(3), &json!({}));
assert_eq!(report.schema_version, Some(GenomeSchemaVersion(3)));
assert!(report.is_clean());
}
}