1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
use crate::{error::MigrationError, value::ConfigValue};
/// A single schema migration step.
pub trait Migration: Send + Sync {
// Naming mirrors the `from`/`to` language used throughout the plan docs.
#[allow(clippy::wrong_self_convention)]
fn from_version(&self) -> u32;
fn to_version(&self) -> u32;
/// Transform `value` from `from_version` to `to_version`.
///
/// # Errors
/// Returns [`MigrationError::Step`] if the transformation fails.
fn migrate(&self, value: ConfigValue) -> Result<ConfigValue, MigrationError>;
}
/// A sequence of [`Migration`] steps applied in version order.
#[derive(Default)]
pub struct MigrationChain {
steps: Vec<Box<dyn Migration>>,
}
impl MigrationChain {
#[must_use]
pub fn with_step(mut self, step: impl Migration + 'static) -> Self {
self.steps.push(Box::new(step));
self
}
/// Run migrations from `current_version` up to `target_version`.
///
/// # Errors
/// Returns [`MigrationError::NoPath`] if no step covers the current version,
/// or [`MigrationError::Step`] if a step's transformation fails.
pub fn run(
&self,
mut value: ConfigValue,
mut current_version: u32,
target_version: u32,
) -> Result<ConfigValue, MigrationError> {
while current_version < target_version {
let step = self
.steps
.iter()
.find(|s| s.from_version() == current_version)
.ok_or(MigrationError::NoPath {
from: current_version,
to: target_version,
})?;
value = step.migrate(value)?;
current_version = step.to_version();
}
Ok(value)
}
}
/// Rename a top-level key in a `ConfigValue::Table`. No-op if `from` is absent.
pub fn rename_key(value: &mut ConfigValue, from: &str, to: &str) {
if let ConfigValue::Table(table) = value
&& let Some(v) = table.remove(from)
{
table.insert(to.to_owned(), v);
}
}