raz_config/
migration.rs

1use crate::{
2    error::{ConfigError, Result},
3    schema::ConfigVersion,
4};
5use serde_json::Value as JsonValue;
6use std::collections::HashMap;
7
8type MigrationFn = Box<dyn Fn(JsonValue) -> Result<JsonValue>>;
9
10pub struct ConfigMigrator {
11    migrations: HashMap<(u32, u32), MigrationFn>,
12}
13
14impl ConfigMigrator {
15    pub fn new() -> Self {
16        let mut migrator = Self {
17            migrations: HashMap::new(),
18        };
19
20        migrator.register_migrations();
21        migrator
22    }
23
24    fn register_migrations(&mut self) {
25        // Example: v0 to v1 migration (when we have legacy configs)
26        // self.add_migration(0, 1, Box::new(|config| {
27        //     let mut config = config;
28        //     // Perform migration steps
29        //     Ok(config)
30        // }));
31    }
32
33    pub fn add_migration(&mut self, from_version: u32, to_version: u32, migration: MigrationFn) {
34        self.migrations
35            .insert((from_version, to_version), migration);
36    }
37
38    pub fn migrate_to_latest(&self, config_str: &str) -> Result<String> {
39        let mut config: JsonValue = serde_json::from_str(config_str).map_err(|e| {
40            ConfigError::MigrationError(format!("Failed to parse config as JSON: {e}"))
41        })?;
42
43        let current_version = self.detect_version(&config)?;
44        let target_version = ConfigVersion::CURRENT;
45
46        if current_version >= target_version {
47            return Ok(config_str.to_string());
48        }
49
50        config = self.migrate_between(config, current_version, target_version)?;
51
52        let toml_value: toml::Value = serde_json::from_value(config)
53            .map_err(|e| ConfigError::MigrationError(e.to_string()))?;
54
55        toml::to_string_pretty(&toml_value).map_err(ConfigError::SerializeError)
56    }
57
58    fn migrate_between(
59        &self,
60        mut config: JsonValue,
61        from: ConfigVersion,
62        to: ConfigVersion,
63    ) -> Result<JsonValue> {
64        let mut current = from.0;
65        let target = to.0;
66
67        while current < target {
68            let next = current + 1;
69
70            if let Some(migration) = self.migrations.get(&(current, next)) {
71                config = migration(config)?;
72            } else {
73                return Err(ConfigError::MigrationError(format!(
74                    "No migration path from v{current} to v{next}"
75                )));
76            }
77
78            current = next;
79        }
80
81        // Update version in config
82        if let Some(raz) = config.get_mut("raz") {
83            if let Some(version) = raz.get_mut("version") {
84                *version = JsonValue::Number(target.into());
85            }
86        }
87
88        Ok(config)
89    }
90
91    fn detect_version(&self, config: &JsonValue) -> Result<ConfigVersion> {
92        if let Some(raz) = config.get("raz") {
93            if let Some(version) = raz.get("version") {
94                if let Some(v) = version.as_u64() {
95                    return Ok(ConfigVersion(v as u32));
96                }
97            }
98        }
99
100        // If no version field, assume it's v0 (legacy)
101        Ok(ConfigVersion(0))
102    }
103
104    pub fn check_migration_needed(config_str: &str) -> Result<bool> {
105        let config: toml::Value = toml::from_str(config_str)?;
106
107        if let Some(raz) = config.get("raz") {
108            if let Some(version) = raz.get("version") {
109                if let Some(v) = version.as_integer() {
110                    return Ok(ConfigVersion(v as u32) < ConfigVersion::CURRENT);
111                }
112            }
113        }
114
115        // No version means migration needed
116        Ok(true)
117    }
118}
119
120impl Default for ConfigMigrator {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_version_detection() {
132        let migrator = ConfigMigrator::new();
133
134        let config_v1 = r#"{"raz": {"version": 1}}"#;
135        let config: JsonValue = serde_json::from_str(config_v1).unwrap();
136        let version = migrator.detect_version(&config).unwrap();
137        assert_eq!(version.0, 1);
138
139        let config_no_version = r#"{"raz": {}}"#;
140        let config: JsonValue = serde_json::from_str(config_no_version).unwrap();
141        let version = migrator.detect_version(&config).unwrap();
142        assert_eq!(version.0, 0);
143    }
144}