use super::helpers::*;
use crate::core::types;
use std::path::Path;
pub(crate) fn cmd_validate_check_path_conflicts(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: crate::core::types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let mut path_owners: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (name, resource) in &config.resources {
if let Some(ref path) = resource.path {
path_owners
.entry(path.clone())
.or_default()
.push(name.clone());
}
}
let mut conflicts = Vec::new();
for (path, owners) in &path_owners {
if owners.len() > 1 {
conflicts.push(format!("Path '{}' claimed by: {}", path, owners.join(", ")));
}
}
if json {
print!("{{\"conflicts\":[");
for (i, c) in conflicts.iter().enumerate() {
if i > 0 {
print!(",");
}
print!(r#""{}""#, c.replace('"', "\\\""));
}
println!("]}}");
} else if conflicts.is_empty() {
println!("No file path conflicts detected");
} else {
println!("Path conflicts ({}):", conflicts.len());
for c in &conflicts {
println!(" - {c}");
}
return Err(format!("{} path conflict(s)", conflicts.len()));
}
Ok(())
}
pub(super) fn find_undefined_vars(
field: &str,
name: &str,
params: &std::collections::HashSet<String>,
undefined: &mut Vec<(String, String)>,
) {
let mut rest = field;
while let Some(start) = rest.find("{{") {
if let Some(end) = rest[start..].find("}}") {
let var = rest[start + 2..start + end].trim();
if var.starts_with("secrets.")
|| var.starts_with("machine.")
|| var.starts_with("data.")
|| var.contains('(')
{
rest = &rest[start + end + 2..];
continue;
}
let key = var.strip_prefix("params.").unwrap_or(var);
if !params.contains(key) {
undefined.push((name.to_string(), key.to_string()));
}
rest = &rest[start + end + 2..];
} else {
break;
}
}
}
pub(crate) fn cmd_validate_check_template_vars(file: &Path, json: bool) -> Result<(), String> {
let cfg = parse_and_validate(file)?;
let mut undefined: Vec<(String, String)> = Vec::new();
let params: std::collections::HashSet<String> = cfg.params.keys().cloned().collect();
for (name, resource) in &cfg.resources {
let fields = [
resource.path.as_deref(),
resource.content.as_deref(),
resource.owner.as_deref(),
];
for field in fields.into_iter().flatten() {
find_undefined_vars(field, name, ¶ms, &mut undefined);
}
}
if json {
println!(
"{{\"check\":\"template_vars\",\"undefined_count\":{},\"undefined\":[{}]}}",
undefined.len(),
undefined
.iter()
.map(|(r, v)| format!("{{\"resource\":\"{r}\",\"var\":\"{v}\"}}"))
.collect::<Vec<_>>()
.join(",")
);
} else if undefined.is_empty() {
println!("All template variables are defined.");
} else {
println!("Undefined template variables:");
for (resource, var) in &undefined {
println!(" {resource} -> {var}");
}
return Err(format!(
"{} undefined template variable(s)",
undefined.len()
));
}
Ok(())
}
pub(crate) fn cmd_validate_check_mode_consistency(file: &Path, json: bool) -> Result<(), String> {
let cfg = parse_and_validate(file)?;
let dir_modes = collect_dir_modes(&cfg);
let inconsistencies = find_mode_inconsistencies(&dir_modes);
if json {
let entries: Vec<String> = inconsistencies
.iter()
.map(|(dir, name, mode)| {
format!("{{\"directory\":\"{dir}\",\"resource\":\"{name}\",\"mode\":\"{mode}\"}}")
})
.collect();
println!(
"{{\"check\":\"mode_consistency\",\"inconsistency_count\":{},\"details\":[{}]}}",
inconsistencies.len(),
entries.join(",")
);
} else if inconsistencies.is_empty() {
println!("All file modes are consistent.");
} else {
println!("File mode inconsistencies found:");
for (dir, name, mode) in &inconsistencies {
println!(" {name} in {dir} — mode {mode}");
}
}
Ok(())
}
pub(super) fn collect_dir_modes(
cfg: &types::ForjarConfig,
) -> std::collections::HashMap<String, Vec<(String, String)>> {
let mut dir_modes: std::collections::HashMap<String, Vec<(String, String)>> =
std::collections::HashMap::new();
for (name, resource) in &cfg.resources {
if let Some(ref path) = resource.path {
if let Some(ref mode) = resource.mode {
let parent = std::path::Path::new(path)
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
dir_modes
.entry(parent)
.or_default()
.push((name.clone(), mode.clone()));
}
}
}
dir_modes
}
pub(super) fn find_mode_inconsistencies(
dir_modes: &std::collections::HashMap<String, Vec<(String, String)>>,
) -> Vec<(String, String, String)> {
let mut inconsistencies: Vec<(String, String, String)> = Vec::new();
for (dir, entries) in dir_modes {
if entries.len() > 1 {
let modes: std::collections::HashSet<&str> =
entries.iter().map(|(_, m)| m.as_str()).collect();
if modes.len() > 1 {
for (name, mode) in entries {
inconsistencies.push((dir.clone(), name.clone(), mode.clone()));
}
}
}
}
inconsistencies
}
pub(crate) fn cmd_validate_check_group_consistency(file: &Path, json: bool) -> Result<(), String> {
let cfg = parse_and_validate(file)?;
let mut owner_groups: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (name, resource) in &cfg.resources {
if let Some(ref owner) = resource.owner {
owner_groups
.entry(owner.clone())
.or_default()
.push(name.clone());
}
}
if json {
let entries: Vec<String> = owner_groups
.iter()
.map(|(owner, resources)| {
let res: Vec<String> = resources.iter().map(|r| format!("\"{r}\"")).collect();
format!(
"{{\"owner\":\"{}\",\"resource_count\":{},\"resources\":[{}]}}",
owner,
resources.len(),
res.join(",")
)
})
.collect();
println!(
"{{\"check\":\"group_consistency\",\"owners\":[{}]}}",
entries.join(",")
);
} else if owner_groups.is_empty() {
println!("No owner fields specified in resources.");
} else {
println!("Owner/group consistency:");
for (owner, resources) in &owner_groups {
println!(
" {} — {} resource(s): {}",
owner,
resources.len(),
resources.join(", ")
);
}
}
Ok(())
}
pub(crate) fn cmd_validate_check_mount_points(file: &Path, json: bool) -> Result<(), String> {
let cfg = parse_and_validate(file)?;
let mut mount_paths: Vec<(String, String)> = Vec::new();
for (name, resource) in &cfg.resources {
if format!("{:?}", resource.resource_type).contains("Mount") {
if let Some(ref path) = resource.path {
mount_paths.push((name.clone(), path.clone()));
}
}
}
let conflicts = find_mount_conflicts(&mount_paths);
if json {
let entries: Vec<String> = conflicts
.iter()
.map(|(a, b)| format!("{{\"resource_a\":\"{a}\",\"resource_b\":\"{b}\"}}"))
.collect();
println!(
"{{\"check\":\"mount_points\",\"conflict_count\":{},\"conflicts\":[{}]}}",
conflicts.len(),
entries.join(",")
);
} else if conflicts.is_empty() {
println!("No mount point conflicts found.");
} else {
println!("Mount point conflicts:");
for (a, b) in &conflicts {
println!(" {a} <-> {b}");
}
}
Ok(())
}
pub(super) use super::validate_paths_b::*;