use super::helpers::*;
use crate::core::{codegen, resolver, types};
use std::path::Path;
fn action_symbol(action: &types::PlanAction) -> String {
match action {
types::PlanAction::Create => green("+"),
types::PlanAction::Update => yellow("~"),
types::PlanAction::Destroy => red("-"),
types::PlanAction::NoOp => dim(" "),
}
}
fn action_desc(action: &types::PlanAction, description: &str) -> String {
match action {
types::PlanAction::Create => green(description),
types::PlanAction::Update => yellow(description),
types::PlanAction::Destroy => red(description),
types::PlanAction::NoOp => dim(description),
}
}
fn print_plan_change(change: &types::PlannedChange, config: Option<&types::ForjarConfig>) {
println!(
" {} {}",
action_symbol(&change.action),
action_desc(&change.action, &change.description)
);
if let Some(cfg) = config {
if matches!(
change.action,
types::PlanAction::Create | types::PlanAction::Update
) {
if let Some(resource) = cfg.resources.get(&change.resource_id) {
if let Some(ref content) = resource.content {
let old_content = if matches!(change.action, types::PlanAction::Update) {
resource
.path
.as_ref()
.and_then(|p| std::fs::read_to_string(p).ok())
} else {
None
};
print_content_diff(content, &change.action, old_content.as_deref());
}
}
}
}
}
fn colored_count(count: u32, color_fn: fn(&str) -> String) -> String {
if count > 0 {
color_fn(&count.to_string())
} else {
count.to_string()
}
}
fn print_plan_summary(plan: &types::ExecutionPlan) {
println!(
"Plan: {} to add, {} to change, {} to destroy, {} unchanged.",
colored_count(plan.to_create, green),
colored_count(plan.to_update, yellow),
colored_count(plan.to_destroy, red),
plan.unchanged
);
}
pub(crate) fn print_plan(
plan: &types::ExecutionPlan,
machine_filter: Option<&str>,
config: Option<&types::ForjarConfig>,
) {
println!("Planning: {} ({} resources)", plan.name, plan.changes.len());
println!();
let mut current_machine = String::new();
for change in &plan.changes {
if let Some(filter) = machine_filter {
if change.machine != filter {
continue;
}
}
if change.machine != current_machine {
current_machine.clone_from(&change.machine);
println!("{current_machine}:");
}
print_plan_change(change, config);
}
println!();
print_plan_summary(plan);
}
pub(crate) fn print_content_diff(
content: &str,
action: &types::PlanAction,
old_content: Option<&str>,
) {
if matches!(action, types::PlanAction::Update) {
if let Some(old) = old_content {
print_unified_diff(old, content);
return;
}
}
let lines: Vec<&str> = content.lines().collect();
let prefix = match action {
types::PlanAction::Create => "+",
types::PlanAction::Update => "~",
_ => " ",
};
let max_lines = 50;
let show = lines.len().min(max_lines);
println!(" ---");
for line in &lines[..show] {
println!(" {prefix} {line}");
}
if lines.len() > max_lines {
println!(" [... {} more lines]", lines.len() - max_lines);
}
println!(" ---");
}
pub(crate) fn print_unified_diff(old: &str, new: &str) {
let old_lines: Vec<&str> = old.lines().collect();
let new_lines: Vec<&str> = new.lines().collect();
let max_lines = 50;
let mut shown = 0;
println!(" ---");
let max_len = old_lines.len().max(new_lines.len());
for i in 0..max_len {
if shown >= max_lines {
println!(" [... {} more lines]", max_len - shown);
break;
}
match (old_lines.get(i), new_lines.get(i)) {
(Some(o), Some(n)) if o == n => {
println!(" {o}");
shown += 1;
}
(Some(o), Some(n)) => {
println!(" {} {}", red("-"), o);
println!(" {} {}", green("+"), n);
shown += 2;
}
(Some(o), None) => {
println!(" {} {}", red("-"), o);
shown += 1;
}
(None, Some(n)) => {
println!(" {} {}", green("+"), n);
shown += 1;
}
(None, None) => break,
}
}
println!(" ---");
}
pub(crate) fn export_scripts(config: &types::ForjarConfig, dir: &Path) -> Result<(), String> {
std::fs::create_dir_all(dir)
.map_err(|e| format!("cannot create output dir {}: {}", dir.display(), e))?;
let mut count = 0;
for (id, resource) in &config.resources {
let resolved =
resolver::resolve_resource_templates(resource, &config.params, &config.machines)?;
let safe_id = id.replace('/', "--");
let machine_str = match &resource.machine {
types::MachineTarget::Single(m) => m.clone(),
types::MachineTarget::Multiple(ms) => ms.join(","),
};
let mut header = format!(
"# forjar: {} ({})\n# machine: {}\n# type: {}\n",
id, config.name, machine_str, resource.resource_type
);
if let Some(ref rg) = resource.resource_group {
header.push_str(&format!("# group: {rg}\n"));
}
if !resource.tags.is_empty() {
header.push_str(&format!("# tags: {}\n", resource.tags.join(", ")));
}
if !resource.depends_on.is_empty() {
header.push_str(&format!(
"# depends_on: {}\n",
resource.depends_on.join(", ")
));
}
if let Ok(script) = codegen::check_script(&resolved) {
let path = dir.join(format!("{safe_id}.check.sh"));
std::fs::write(&path, format!("{header}{script}"))
.map_err(|e| format!("write {}: {}", path.display(), e))?;
count += 1;
}
if let Ok(script) = codegen::apply_script(&resolved) {
let path = dir.join(format!("{safe_id}.apply.sh"));
std::fs::write(&path, format!("{header}{script}"))
.map_err(|e| format!("write {}: {}", path.display(), e))?;
count += 1;
}
if let Ok(script) = codegen::state_query_script(&resolved) {
let path = dir.join(format!("{safe_id}.state_query.sh"));
std::fs::write(&path, format!("{header}{script}"))
.map_err(|e| format!("write {}: {}", path.display(), e))?;
count += 1;
}
}
println!("Exported {} scripts to {}", count, dir.display());
Ok(())
}