Skip to main content

confex_core/
migrate.rs

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