1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[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 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
22 Self {
23 major,
24 minor,
25 patch,
26 }
27 }
28
29 pub const CURRENT: Self = Self::new(0, 1, 0);
31
32 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 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#[derive(Debug, Clone)]
60pub struct Migration {
61 pub from: ConfigVersion,
63
64 pub to: ConfigVersion,
66
67 pub migrate: fn(&mut serde_json::Value) -> Result<()>,
69
70 pub description: String,
72}
73
74pub struct MigrationManager {
76 migrations: Vec<Migration>,
77}
78
79impl MigrationManager {
80 pub fn new() -> Self {
82 let mut manager = Self {
83 migrations: Vec::new(),
84 };
85
86 manager.register_migrations();
88 manager
89 }
90
91 fn register_migrations(&mut self) {
93 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 }
103
104 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 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 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 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 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
196pub 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 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 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 if backup {
232 let backup_path = create_backup(config_path).await?;
233 println!("Created backup: {}", backup_path.display());
234 }
235
236 let manager = MigrationManager::new();
238 manager.migrate(&mut config, ¤t_version, &target)?;
239
240 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
254fn 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 Ok(ConfigVersion::new(0, 0, 1))
262 }
263}
264
265async 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
277pub 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
304pub 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 let version = detect_version(&config)?;
319
320 if verbose {
321 eprintln!("Configuration version: {}", version);
322 }
323
324 validate_required_fields(&config)?;
326
327 println!("Configuration is valid!");
328 Ok(())
329}
330
331fn 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
344fn 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 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 if let Some(models) = obj.get_mut("models") {
368 if let Some(models_obj) = models.as_object_mut() {
369 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 assert!(config.get("telemetry").is_some());
451 assert_eq!(
452 config["telemetry"]["enabled"],
453 serde_json::Value::Bool(false)
454 );
455
456 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}