cfg.rs
1 //! Configuration management command handlers. 2 3 use acdc_cfg::{ 4 migration::{self, MigrationResult}, 5 profiles::{ProfileEnvironment, ProfileManager}, 6 templates::TemplateEngine, 7 validation::{self, ValidationResult, ValidationSeverity}, 8 }; 9 use acdc_core::Result; 10 use console::style; 11 use std::collections::HashMap; 12 use std::fs; 13 14 fn json_output<T: serde::Serialize>(value: &T) -> String { 15 serde_json::to_string_pretty(value).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)) 16 } 17 18 /// Validate a configuration file. 19 pub async fn validate(path: &str, json: bool) -> Result<()> { 20 let result = validation::validate_file(path)?; 21 22 if json { 23 println!("{}", json_output(&result)); 24 return Ok(()); 25 } 26 27 print_validation_result(path, &result); 28 Ok(()) 29 } 30 31 fn print_validation_result(path: &str, result: &ValidationResult) { 32 println!("{}", style("Configuration Validation").bold().cyan()); 33 println!("{}", style("─".repeat(50)).dim()); 34 println!("Path: {}", path); 35 println!("Errors: {}", result.error_count); 36 println!("Warnings: {}", result.warning_count); 37 println!(); 38 39 if result.valid { 40 println!("{}", style("Configuration is valid!").green().bold()); 41 } else { 42 println!("{}", style("Validation failed:").red().bold()); 43 for issue in &result.issues { 44 if issue.severity == ValidationSeverity::Error { 45 println!(" {} {}", style("✗").red(), issue.error); 46 if let Some(suggestion) = &issue.suggestion { 47 println!(" {}: {}", style("Suggestion").dim(), suggestion); 48 } 49 } 50 } 51 } 52 53 let warnings: Vec<_> = result 54 .issues 55 .iter() 56 .filter(|i| i.severity == ValidationSeverity::Warning) 57 .collect(); 58 59 if !warnings.is_empty() { 60 println!(); 61 println!("{}", style("Warnings:").yellow().bold()); 62 for issue in warnings { 63 println!(" {} {}", style("⚠").yellow(), issue.error); 64 if let Some(suggestion) = &issue.suggestion { 65 println!(" {}: {}", style("Suggestion").dim(), suggestion); 66 } 67 } 68 } 69 } 70 71 /// Migrate a configuration file. 72 pub async fn migrate(path: &str, preview: bool) -> Result<()> { 73 let result = if preview { 74 migration::preview_migration(path)? 75 } else { 76 migration::migrate_to_latest(path)? 77 }; 78 79 print_migration_result(&result, preview); 80 Ok(()) 81 } 82 83 fn print_migration_result(result: &MigrationResult, preview: bool) { 84 println!("{}", style("Configuration Migration").bold().cyan()); 85 println!("{}", style("─".repeat(50)).dim()); 86 println!("From: {}", result.from_version); 87 println!("To: {}", result.to_version); 88 println!(); 89 90 if result.migrations_applied.is_empty() { 91 println!( 92 "{}", 93 style("No migration needed - already at latest version").green() 94 ); 95 return; 96 } 97 98 if preview { 99 println!("{}", style("Preview (no changes made):").yellow().bold()); 100 } else { 101 println!("{}", style("Migrations applied:").green().bold()); 102 } 103 104 for migration in &result.migrations_applied { 105 println!(" {} {}", style("→").cyan(), migration); 106 } 107 108 if !result.fields_added.is_empty() { 109 println!(); 110 println!("{}", style("Fields added:").bold()); 111 for field in &result.fields_added { 112 println!( 113 " {} {} = {}", 114 style("+").green(), 115 style(&field.path).bold(), 116 field.default_value.as_deref().unwrap_or("(default)") 117 ); 118 } 119 } 120 121 if !result.fields_removed.is_empty() { 122 println!(); 123 println!("{}", style("Fields removed:").bold()); 124 for field in &result.fields_removed { 125 println!( 126 " {} {} ({})", 127 style("-").red(), 128 style(&field.path).bold(), 129 field.reason 130 ); 131 } 132 } 133 134 if !result.fields_renamed.is_empty() { 135 println!(); 136 println!("{}", style("Fields renamed:").bold()); 137 for field in &result.fields_renamed { 138 println!( 139 " {} {} {} {}", 140 style(&field.old_path).dim(), 141 style("→").cyan(), 142 style(&field.new_path).bold(), 143 style(format!("(v{})", field.version)).dim() 144 ); 145 } 146 } 147 148 if let Some(backup_path) = &result.backup_path { 149 println!(); 150 println!("{}: {:?}", style("Backup created").dim(), backup_path); 151 } 152 } 153 154 /// List available profiles. 155 pub async fn profile_list() -> Result<()> { 156 let manager = ProfileManager::new(); 157 let profiles = manager.list(); 158 159 println!("{}", style("Available Profiles").bold().cyan()); 160 println!("{}", style("─".repeat(60)).dim()); 161 162 for profile in profiles { 163 let env_style = match profile.environment { 164 ProfileEnvironment::Production => style(format!("[{}]", profile.environment)).red(), 165 ProfileEnvironment::Development => style(format!("[{}]", profile.environment)).green(), 166 ProfileEnvironment::Testing => style(format!("[{}]", profile.environment)).blue(), 167 ProfileEnvironment::Staging => style(format!("[{}]", profile.environment)).yellow(), 168 ProfileEnvironment::Custom => style(format!("[{}]", profile.environment)).magenta(), 169 }; 170 171 println!(); 172 println!("{} {}", style(&profile.name).bold(), env_style); 173 println!(" {}", profile.description); 174 } 175 176 Ok(()) 177 } 178 179 /// Apply a profile to a configuration file. 180 pub async fn profile_apply(profile: &str, path: &str) -> Result<()> { 181 let content = fs::read_to_string(path)?; 182 let mut config: toml::Value = toml::from_str(&content)?; 183 184 let manager = ProfileManager::new(); 185 let applied = manager.apply_to_config(profile, &mut config)?; 186 187 let output = toml::to_string_pretty(&config)?; 188 fs::write(path, output)?; 189 190 println!( 191 "{}", 192 style(format!("Applied profile '{}' to {}", profile, path)) 193 .green() 194 .bold() 195 ); 196 println!(); 197 println!("{}", style("Changes:").bold()); 198 for change in &applied { 199 println!(" {} {}", style("→").cyan(), change); 200 } 201 202 Ok(()) 203 } 204 205 /// Show profile details. 206 pub async fn profile_show(name: &str, json: bool) -> Result<()> { 207 let manager = ProfileManager::new(); 208 let profile = manager 209 .get(name) 210 .ok_or_else(|| acdc_core::Error::Config(format!("Unknown profile: {}", name)))?; 211 212 if json { 213 println!("{}", json_output(&profile)); 214 return Ok(()); 215 } 216 217 println!("{}", style(&profile.name).bold().cyan()); 218 println!("{}", style("─".repeat(50)).dim()); 219 println!("Environment: {:?}", profile.environment); 220 println!("Description: {}", profile.description); 221 println!(); 222 println!("{}", style("Overrides:").bold()); 223 224 let mut sorted_overrides: Vec<_> = profile.overrides.iter().collect(); 225 sorted_overrides.sort_by_key(|(k, _)| *k); 226 227 for (key, value) in sorted_overrides { 228 println!(" {} = {}", style(key).bold(), format_toml_value(value)); 229 } 230 231 Ok(()) 232 } 233 234 fn format_toml_value(value: &toml::Value) -> String { 235 match value { 236 toml::Value::String(s) => format!("\"{}\"", s), 237 toml::Value::Integer(i) => i.to_string(), 238 toml::Value::Float(f) => f.to_string(), 239 toml::Value::Boolean(b) => b.to_string(), 240 toml::Value::Array(arr) => { 241 let items: Vec<String> = arr.iter().map(format_toml_value).collect(); 242 format!("[{}]", items.join(", ")) 243 } 244 toml::Value::Table(_) => "{...}".to_string(), 245 toml::Value::Datetime(dt) => dt.to_string(), 246 } 247 } 248 249 /// List available templates. 250 pub async fn template_list() -> Result<()> { 251 let engine = TemplateEngine::new(); 252 let templates = engine.list(); 253 254 println!("{}", style("Available Templates").bold().cyan()); 255 println!("{}", style("─".repeat(60)).dim()); 256 257 for template in templates { 258 println!(); 259 println!("{}", style(&template.name).bold()); 260 println!(" {}", template.description); 261 262 let required: Vec<_> = template 263 .variables 264 .iter() 265 .filter(|v| v.required && v.default.is_none()) 266 .collect(); 267 268 if !required.is_empty() { 269 println!( 270 " {}: {}", 271 style("Required vars").dim(), 272 required 273 .iter() 274 .map(|v| v.name.as_str()) 275 .collect::<Vec<_>>() 276 .join(", ") 277 ); 278 } 279 } 280 281 Ok(()) 282 } 283 284 /// Show template details. 285 pub async fn template_show(name: &str, json: bool) -> Result<()> { 286 let engine = TemplateEngine::new(); 287 let template = engine 288 .get(name) 289 .ok_or_else(|| acdc_core::Error::Config(format!("Unknown template: {}", name)))?; 290 291 if json { 292 println!("{}", json_output(&template)); 293 return Ok(()); 294 } 295 296 println!("{}", style(&template.name).bold().cyan()); 297 println!("{}", style("─".repeat(60)).dim()); 298 println!("{}", template.description); 299 println!(); 300 println!("{}", style("Variables:").bold()); 301 302 for var in &template.variables { 303 let required_str = if var.required && var.default.is_none() { 304 style("(required)").red() 305 } else { 306 style("(optional)").dim() 307 }; 308 309 println!(); 310 println!(" {} {}", style(&var.name).bold(), required_str); 311 println!(" {}", var.description); 312 313 if let Some(default) = &var.default { 314 println!(" Default: {}", style(default).green()); 315 } 316 317 if let Some(pattern) = &var.pattern { 318 println!(" Pattern: {}", style(pattern).dim()); 319 } 320 } 321 322 println!(); 323 println!("{}", style("Usage:").bold()); 324 println!( 325 " ac-dc cfg template render {} --var node_name=my-node [--var key=value ...]", 326 name 327 ); 328 329 Ok(()) 330 } 331 332 /// Render a template. 333 pub async fn template_render( 334 template_name: &str, 335 output: Option<String>, 336 vars: Vec<(String, String)>, 337 ) -> Result<()> { 338 let engine = TemplateEngine::new(); 339 340 // Convert vars to HashMap 341 let vars_map: HashMap<String, String> = vars.into_iter().collect(); 342 343 // Check for missing required vars 344 let missing = engine.get_missing_vars(template_name, &vars_map)?; 345 if !missing.is_empty() { 346 println!("{}", style("Missing required variables:").red().bold()); 347 for var in &missing { 348 println!(" {} - {}", style(&var.name).bold(), var.description); 349 } 350 println!(); 351 println!("Use --var {}=VALUE to provide", missing[0].name); 352 return Err(acdc_core::Error::Config( 353 "Missing required variables".to_string(), 354 )); 355 } 356 357 let rendered = engine.render(template_name, &vars_map)?; 358 359 if let Some(path) = output { 360 fs::write(&path, &rendered)?; 361 println!( 362 "{}", 363 style(format!("Configuration written to {}", path)) 364 .green() 365 .bold() 366 ); 367 } else { 368 println!("{}", rendered); 369 } 370 371 Ok(()) 372 }