Skip to main content

voirs_cli/commands/
config_migrate.rs

1//! Configuration migration tool for VoiRS CLI
2//!
3//! Handles automatic migration of configuration files between versions,
4//! ensuring backward compatibility and smooth upgrades.
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11/// Configuration version
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub struct ConfigVersion {
14    pub major: u32,
15    pub minor: u32,
16    pub patch: u32,
17}
18
19impl ConfigVersion {
20    /// Create a new version
21    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
22        Self {
23            major,
24            minor,
25            patch,
26        }
27    }
28
29    /// Current config version
30    pub const CURRENT: Self = Self::new(0, 1, 0);
31
32    /// Parse version from string (e.g., "0.1.0")
33    pub fn parse(s: &str) -> Option<Self> {
34        let parts: Vec<&str> = s.split('.').collect();
35        if parts.len() != 3 {
36            return None;
37        }
38
39        Some(Self {
40            major: parts[0].parse().ok()?,
41            minor: parts[1].parse().ok()?,
42            patch: parts[2].parse().ok()?,
43        })
44    }
45
46    /// Check if this version needs migration to target
47    pub fn needs_migration(&self, target: &ConfigVersion) -> bool {
48        self < target
49    }
50}
51
52impl std::fmt::Display for ConfigVersion {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
55    }
56}
57
58/// Migration operation
59#[derive(Debug, Clone)]
60pub struct Migration {
61    /// Source version
62    pub from: ConfigVersion,
63
64    /// Target version
65    pub to: ConfigVersion,
66
67    /// Migration function
68    pub migrate: fn(&mut serde_json::Value) -> Result<()>,
69
70    /// Description of changes
71    pub description: String,
72}
73
74/// Migration manager
75pub struct MigrationManager {
76    migrations: Vec<Migration>,
77}
78
79impl MigrationManager {
80    /// Create a new migration manager
81    pub fn new() -> Self {
82        let mut manager = Self {
83            migrations: Vec::new(),
84        };
85
86        // Register all migrations
87        manager.register_migrations();
88        manager
89    }
90
91    /// Register all available migrations
92    fn register_migrations(&mut self) {
93        // Example: Migration from 0.0.1 to 0.1.0
94        self.add_migration(
95            ConfigVersion::new(0, 0, 1),
96            ConfigVersion::new(0, 1, 0),
97            migrate_0_0_1_to_0_1_0,
98            "Add telemetry configuration and update model paths",
99        );
100
101        // Add more migrations as needed
102    }
103
104    /// Add a migration
105    pub fn add_migration(
106        &mut self,
107        from: ConfigVersion,
108        to: ConfigVersion,
109        migrate_fn: fn(&mut serde_json::Value) -> Result<()>,
110        description: impl Into<String>,
111    ) {
112        self.migrations.push(Migration {
113            from,
114            to,
115            migrate: migrate_fn,
116            description: description.into(),
117        });
118    }
119
120    /// Get migration path from source to target version
121    pub fn get_migration_path(
122        &self,
123        from: &ConfigVersion,
124        to: &ConfigVersion,
125    ) -> Result<Vec<&Migration>> {
126        let mut path = Vec::new();
127        let mut current = *from;
128
129        while current < *to {
130            // Find next migration step
131            let next = self
132                .migrations
133                .iter()
134                .filter(|m| m.from == current && m.to <= *to)
135                .min_by_key(|m| m.to);
136
137            match next {
138                Some(migration) => {
139                    path.push(migration);
140                    current = migration.to;
141                }
142                None => {
143                    anyhow::bail!("No migration path found from {} to {}", from, to);
144                }
145            }
146        }
147
148        Ok(path)
149    }
150
151    /// Migrate configuration from one version to another
152    pub fn migrate(
153        &self,
154        config: &mut serde_json::Value,
155        from: &ConfigVersion,
156        to: &ConfigVersion,
157    ) -> Result<()> {
158        if from == to {
159            return Ok(());
160        }
161
162        let path = self.get_migration_path(from, to)?;
163
164        for migration in path {
165            eprintln!(
166                "Migrating from {} to {}: {}",
167                migration.from, migration.to, migration.description
168            );
169
170            (migration.migrate)(config).with_context(|| {
171                format!(
172                    "Failed to migrate from {} to {}",
173                    migration.from, migration.to
174                )
175            })?;
176        }
177
178        // Update version field
179        if let Some(obj) = config.as_object_mut() {
180            obj.insert(
181                "version".to_string(),
182                serde_json::Value::String(to.to_string()),
183            );
184        }
185
186        Ok(())
187    }
188}
189
190impl Default for MigrationManager {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196/// Migrate configuration file
197pub async fn migrate_config_file(
198    config_path: &Path,
199    target_version: Option<ConfigVersion>,
200    backup: bool,
201    verbose: bool,
202) -> Result<()> {
203    let target = target_version.unwrap_or(ConfigVersion::CURRENT);
204
205    if verbose {
206        eprintln!("Reading configuration from {}...", config_path.display());
207    }
208
209    // Read existing config
210    let config_str = tokio::fs::read_to_string(config_path)
211        .await
212        .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
213
214    let mut config: serde_json::Value = serde_json::from_str(&config_str)
215        .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
216
217    // Detect current version
218    let current_version = detect_version(&config)?;
219
220    if verbose {
221        eprintln!("Current config version: {}", current_version);
222        eprintln!("Target config version: {}", target);
223    }
224
225    if !current_version.needs_migration(&target) {
226        println!("Configuration is already up to date ({})!", current_version);
227        return Ok(());
228    }
229
230    // Create backup if requested
231    if backup {
232        let backup_path = create_backup(config_path).await?;
233        println!("Created backup: {}", backup_path.display());
234    }
235
236    // Perform migration
237    let manager = MigrationManager::new();
238    manager.migrate(&mut config, &current_version, &target)?;
239
240    // Write updated config
241    let new_config_str = serde_json::to_string_pretty(&config)?;
242    tokio::fs::write(config_path, new_config_str)
243        .await
244        .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
245
246    println!(
247        "Successfully migrated configuration from {} to {}",
248        current_version, target
249    );
250
251    Ok(())
252}
253
254/// Detect configuration version
255fn detect_version(config: &serde_json::Value) -> Result<ConfigVersion> {
256    if let Some(version_str) = config.get("version").and_then(|v| v.as_str()) {
257        ConfigVersion::parse(version_str)
258            .ok_or_else(|| anyhow::anyhow!("Invalid version format: {}", version_str))
259    } else {
260        // Assume oldest version if no version field
261        Ok(ConfigVersion::new(0, 0, 1))
262    }
263}
264
265/// Create backup of configuration file
266async fn create_backup(config_path: &Path) -> Result<PathBuf> {
267    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
268    let backup_path = config_path.with_extension(format!("toml.backup.{}", timestamp));
269
270    tokio::fs::copy(config_path, &backup_path)
271        .await
272        .with_context(|| format!("Failed to create backup at {}", backup_path.display()))?;
273
274    Ok(backup_path)
275}
276
277/// Restore configuration from backup
278pub async fn restore_from_backup(
279    config_path: &Path,
280    backup_path: &Path,
281    verbose: bool,
282) -> Result<()> {
283    if verbose {
284        eprintln!(
285            "Restoring configuration from backup: {}",
286            backup_path.display()
287        );
288    }
289
290    tokio::fs::copy(backup_path, config_path)
291        .await
292        .with_context(|| {
293            format!(
294                "Failed to restore from backup {} to {}",
295                backup_path.display(),
296                config_path.display()
297            )
298        })?;
299
300    println!("Configuration restored successfully");
301    Ok(())
302}
303
304/// Validate configuration file
305pub async fn validate_config(config_path: &Path, verbose: bool) -> Result<()> {
306    if verbose {
307        eprintln!("Validating configuration: {}", config_path.display());
308    }
309
310    let config_str = tokio::fs::read_to_string(config_path)
311        .await
312        .with_context(|| format!("Failed to read config: {}", config_path.display()))?;
313
314    let config: serde_json::Value =
315        serde_json::from_str(&config_str).with_context(|| "Failed to parse configuration")?;
316
317    // Detect version
318    let version = detect_version(&config)?;
319
320    if verbose {
321        eprintln!("Configuration version: {}", version);
322    }
323
324    // Validate required fields
325    validate_required_fields(&config)?;
326
327    println!("Configuration is valid!");
328    Ok(())
329}
330
331/// Validate required configuration fields
332fn validate_required_fields(config: &serde_json::Value) -> Result<()> {
333    let required_fields = vec!["version"];
334
335    for field in required_fields {
336        if config.get(field).is_none() {
337            anyhow::bail!("Missing required field: {}", field);
338        }
339    }
340
341    Ok(())
342}
343
344// Migration functions
345
346/// Migrate from 0.0.1 to 0.1.0
347fn migrate_0_0_1_to_0_1_0(config: &mut serde_json::Value) -> Result<()> {
348    let obj = config
349        .as_object_mut()
350        .ok_or_else(|| anyhow::anyhow!("Config must be an object"))?;
351
352    // Add telemetry configuration if missing
353    if !obj.contains_key("telemetry") {
354        let mut telemetry = serde_json::Map::new();
355        telemetry.insert("enabled".to_string(), serde_json::Value::Bool(false));
356        telemetry.insert(
357            "level".to_string(),
358            serde_json::Value::String("basic".to_string()),
359        );
360        obj.insert(
361            "telemetry".to_string(),
362            serde_json::Value::Object(telemetry),
363        );
364    }
365
366    // Update model paths to use new structure
367    if let Some(models) = obj.get_mut("models") {
368        if let Some(models_obj) = models.as_object_mut() {
369            // Convert old "path" to new "paths" array
370            if let Some(path) = models_obj.remove("path") {
371                models_obj.insert("paths".to_string(), serde_json::Value::Array(vec![path]));
372            }
373        }
374    }
375
376    Ok(())
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_version_parsing() {
385        let v = ConfigVersion::parse("0.1.0").unwrap();
386        assert_eq!(v.major, 0);
387        assert_eq!(v.minor, 1);
388        assert_eq!(v.patch, 0);
389    }
390
391    #[test]
392    fn test_version_comparison() {
393        let v1 = ConfigVersion::new(0, 0, 1);
394        let v2 = ConfigVersion::new(0, 1, 0);
395        assert!(v1 < v2);
396        assert!(v1.needs_migration(&v2));
397    }
398
399    #[test]
400    fn test_version_to_string() {
401        let v = ConfigVersion::new(1, 2, 3);
402        assert_eq!(v.to_string(), "1.2.3");
403    }
404
405    #[test]
406    fn test_migration_path() {
407        let manager = MigrationManager::new();
408        let from = ConfigVersion::new(0, 0, 1);
409        let to = ConfigVersion::new(0, 1, 0);
410
411        let path = manager.get_migration_path(&from, &to).unwrap();
412        assert_eq!(path.len(), 1);
413        assert_eq!(path[0].from, from);
414        assert_eq!(path[0].to, to);
415    }
416
417    #[test]
418    fn test_detect_version() {
419        let config = serde_json::json!({
420            "version": "0.1.0",
421            "models": {}
422        });
423
424        let version = detect_version(&config).unwrap();
425        assert_eq!(version, ConfigVersion::new(0, 1, 0));
426    }
427
428    #[test]
429    fn test_detect_version_missing() {
430        let config = serde_json::json!({
431            "models": {}
432        });
433
434        let version = detect_version(&config).unwrap();
435        assert_eq!(version, ConfigVersion::new(0, 0, 1));
436    }
437
438    #[test]
439    fn test_migrate_0_0_1_to_0_1_0() {
440        let mut config = serde_json::json!({
441            "version": "0.0.1",
442            "models": {
443                "path": "/path/to/models"
444            }
445        });
446
447        migrate_0_0_1_to_0_1_0(&mut config).unwrap();
448
449        // Check telemetry was added
450        assert!(config.get("telemetry").is_some());
451        assert_eq!(
452            config["telemetry"]["enabled"],
453            serde_json::Value::Bool(false)
454        );
455
456        // Check paths conversion
457        assert!(config["models"]["paths"].is_array());
458        assert_eq!(
459            config["models"]["paths"][0],
460            serde_json::Value::String("/path/to/models".to_string())
461        );
462    }
463
464    #[test]
465    fn test_validate_required_fields() {
466        let config = serde_json::json!({
467            "version": "0.1.0"
468        });
469
470        assert!(validate_required_fields(&config).is_ok());
471    }
472
473    #[test]
474    fn test_validate_required_fields_missing() {
475        let config = serde_json::json!({
476            "models": {}
477        });
478
479        assert!(validate_required_fields(&config).is_err());
480    }
481
482    #[test]
483    fn test_migration_manager_default() {
484        let manager = MigrationManager::default();
485        assert!(!manager.migrations.is_empty());
486    }
487}