/ crates / acdc / src / commands / cfg.rs
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  }