use serde::{Deserialize, Serialize};
use super::{DriftReport, MissingTool, VersionChange};
pub struct DriftFixer {
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixResult {
pub status: FixStatus,
pub target: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FixStatus {
Success,
Skipped,
Failed,
DryRun,
}
impl DriftFixer {
pub fn new(dry_run: bool) -> Self {
Self { dry_run }
}
pub fn fix_all(&self, report: &DriftReport) -> Vec<FixResult> {
let mut results = Vec::new();
for tool in &report.missing_tools {
results.push(self.fix_missing_tool(tool));
}
for change in &report.version_changes {
results.push(self.fix_version_change(change));
}
for file in &report.changed_files {
results.push(FixResult {
status: FixStatus::Skipped,
target: file.path.clone(),
message: "file changes require manual review".to_string(),
});
}
results
}
fn fix_missing_tool(&self, tool: &MissingTool) -> FixResult {
if !tool.auto_fixable {
return FixResult {
status: FixStatus::Skipped,
target: tool.tool.clone(),
message: "tool requires manual installation".to_string(),
};
}
if self.dry_run {
return FixResult {
status: FixStatus::DryRun,
target: tool.tool.clone(),
message: format!("would install {} {}", tool.tool, tool.expected_version),
};
}
match self.install_tool(&tool.tool, &tool.expected_version) {
Ok(()) => FixResult {
status: FixStatus::Success,
target: tool.tool.clone(),
message: format!("installed {} {}", tool.tool, tool.expected_version),
},
Err(e) => FixResult {
status: FixStatus::Failed,
target: tool.tool.clone(),
message: format!("failed to install: {}", e),
},
}
}
fn fix_version_change(&self, change: &VersionChange) -> FixResult {
if !change.auto_fixable {
return FixResult {
status: FixStatus::Skipped,
target: change.tool.clone(),
message: "version change requires manual intervention".to_string(),
};
}
if self.dry_run {
return FixResult {
status: FixStatus::DryRun,
target: change.tool.clone(),
message: format!(
"would reinstall {} {} (currently {})",
change.tool, change.expected, change.actual
),
};
}
match self.install_tool(&change.tool, &change.expected) {
Ok(()) => FixResult {
status: FixStatus::Success,
target: change.tool.clone(),
message: format!(
"reinstalled {} {} (was {})",
change.tool, change.expected, change.actual
),
},
Err(e) => FixResult {
status: FixStatus::Failed,
target: change.tool.clone(),
message: format!("failed to reinstall: {}", e),
},
}
}
fn install_tool(&self, tool_name: &str, version: &str) -> Result<(), String> {
use crate::tools::registry::get_tool;
if let Some(handler) = get_tool(tool_name) {
handler(version).map_err(|e| format!("{}", e))
} else {
Err(format!("no handler found for tool '{}'", tool_name))
}
}
pub fn print_summary(results: &[FixResult]) {
let success = results
.iter()
.filter(|r| r.status == FixStatus::Success)
.count();
let failed = results
.iter()
.filter(|r| r.status == FixStatus::Failed)
.count();
let skipped = results
.iter()
.filter(|r| r.status == FixStatus::Skipped)
.count();
let dry_run = results
.iter()
.filter(|r| r.status == FixStatus::DryRun)
.count();
println!();
println!("\x1b[1mFix Summary:\x1b[0m");
for result in results {
let symbol = match result.status {
FixStatus::Success => "\x1b[32m✓\x1b[0m",
FixStatus::Failed => "\x1b[31m✗\x1b[0m",
FixStatus::Skipped => "\x1b[33m-\x1b[0m",
FixStatus::DryRun => "\x1b[36m○\x1b[0m",
};
println!(" {} {}: {}", symbol, result.target, result.message);
}
println!();
if dry_run > 0 {
println!(
" Dry run: {} action{} would be taken",
dry_run,
if dry_run == 1 { "" } else { "s" }
);
} else {
println!(
" {} succeeded, {} failed, {} skipped",
success, failed, skipped
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::drift::VersionDirection;
#[test]
fn test_fix_result_serialization() {
let result = FixResult {
status: FixStatus::Success,
target: "node".to_string(),
message: "installed successfully".to_string(),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"success\""));
}
#[test]
fn test_dry_run_missing_tool() {
let fixer = DriftFixer::new(true);
let tool = MissingTool {
tool: "node".to_string(),
expected_version: "20.0.0".to_string(),
auto_fixable: true,
};
let result = fixer.fix_missing_tool(&tool);
assert_eq!(result.status, FixStatus::DryRun);
assert!(result.message.contains("would install"));
}
#[test]
fn test_skip_non_fixable() {
let fixer = DriftFixer::new(false);
let tool = MissingTool {
tool: "rustup".to_string(),
expected_version: "1.0.0".to_string(),
auto_fixable: false,
};
let result = fixer.fix_missing_tool(&tool);
assert_eq!(result.status, FixStatus::Skipped);
}
#[test]
fn test_dry_run_version_change() {
let fixer = DriftFixer::new(true);
let change = VersionChange {
tool: "node".to_string(),
expected: "20.0.0".to_string(),
actual: "21.0.0".to_string(),
direction: VersionDirection::Upgrade,
auto_fixable: true,
reason: None,
};
let result = fixer.fix_version_change(&change);
assert_eq!(result.status, FixStatus::DryRun);
assert!(result.message.contains("would reinstall"));
}
}