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 }
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 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 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 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}