#[allow(unused_imports)]
use crate::core::{codegen, executor, migrate, parser, planner, resolver, secrets, state, types};
use std::path::Path;
pub(crate) fn cmd_validate_check_resource_dependency_ordering(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let issues = find_ordering_issues(&config);
if json {
let items: Vec<String> = issues
.iter()
.map(|(n, r)| format!("{{\"resource\":\"{n}\",\"issue\":\"{r}\"}}"))
.collect();
println!("{{\"ordering_issues\":[{}]}}", items.join(","));
} else if issues.is_empty() {
println!("All resource dependencies are topologically valid.");
} else {
println!("Dependency ordering issues:");
for (n, r) in &issues {
println!(" {n} — {r}");
}
}
Ok(())
}
pub(super) fn find_ordering_issues(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut issues = Vec::new();
let names: std::collections::HashSet<&str> =
config.resources.keys().map(|k| k.as_str()).collect();
for (name, res) in &config.resources {
for dep in &res.depends_on {
if !names.contains(dep.as_str()) {
issues.push((name.clone(), format!("depends on non-existent '{dep}'")));
}
if dep == name {
issues.push((name.clone(), "self-dependency".to_string()));
}
}
}
issues.sort_by(|a, b| a.0.cmp(&b.0));
issues
}
pub(crate) fn cmd_validate_check_resource_tag_completeness(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let missing = find_missing_tags(&config);
if json {
let items: Vec<String> = missing
.iter()
.map(|(n, c)| format!("{{\"resource\":\"{n}\",\"tag_count\":{c}}}"))
.collect();
println!("{{\"tag_completeness\":[{}]}}", items.join(","));
} else if missing.is_empty() {
println!("All resources have tags.");
} else {
println!("Resources missing tags:");
for (n, _) in &missing {
println!(" {n}");
}
}
Ok(())
}
pub(super) fn find_missing_tags(config: &types::ForjarConfig) -> Vec<(String, usize)> {
let mut missing: Vec<(String, usize)> = config
.resources
.iter()
.filter(|(_, res)| res.tags.is_empty())
.map(|(name, _)| (name.clone(), 0))
.collect();
missing.sort_by(|a, b| a.0.cmp(&b.0));
missing
}
pub(crate) fn cmd_validate_check_resource_naming_standards(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let violations = find_naming_violations(&config);
if json {
let items: Vec<String> = violations
.iter()
.map(|(n, r)| format!("{{\"resource\":\"{n}\",\"issue\":\"{r}\"}}"))
.collect();
println!("{{\"naming_violations\":[{}]}}", items.join(","));
} else if violations.is_empty() {
println!("All resource names follow naming conventions.");
} else {
println!("Naming convention violations:");
for (n, r) in &violations {
println!(" {n} — {r}");
}
}
Ok(())
}
pub(super) fn find_naming_violations(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut violations = Vec::new();
for name in config.resources.keys() {
if name.contains(' ') {
violations.push((name.clone(), "contains spaces".to_string()));
}
if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
violations.push((name.clone(), "starts with uppercase".to_string()));
}
if name.contains("__") {
violations.push((name.clone(), "contains double underscore".to_string()));
}
}
violations.sort_by(|a, b| a.0.cmp(&b.0));
violations
}
pub(crate) fn cmd_validate_check_resource_dependency_symmetry(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let asymmetries = find_dependency_asymmetries(&config);
if json {
let items: Vec<String> = asymmetries
.iter()
.map(|(a, b)| format!("{{\"from\":\"{a}\",\"to\":\"{b}\"}}"))
.collect();
println!("{{\"asymmetric_dependencies\":[{}]}}", items.join(","));
} else if asymmetries.is_empty() {
println!("No asymmetric dependencies detected.");
} else {
println!("Asymmetric dependencies:");
for (a, b) in &asymmetries {
println!(" {a} depends on {b} (but not vice versa)");
}
}
Ok(())
}
pub(super) fn find_dependency_asymmetries(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut pairs = Vec::new();
for (name, res) in &config.resources {
for dep in &res.depends_on {
if let Some(dep_res) = config.resources.get(dep) {
if !dep_res.depends_on.contains(name) {
pairs.push((name.clone(), dep.clone()));
}
}
}
}
pairs.sort();
pairs.dedup();
pairs
}
pub(crate) fn cmd_validate_check_resource_circular_alias(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let cycles = find_circular_aliases(&config);
if json {
let items: Vec<String> = cycles
.iter()
.map(|(a, b)| format!("[\"{a}\",\"{b}\"]"))
.collect();
println!("{{\"circular_aliases\":[{}]}}", items.join(","));
} else if cycles.is_empty() {
println!("No circular alias references detected.");
} else {
println!("Circular alias references:");
for (a, b) in &cycles {
println!(" {a} ↔ {b}");
}
}
Ok(())
}
pub(super) fn find_circular_aliases(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut cycles = Vec::new();
for (name, res) in &config.resources {
for dep in &res.depends_on {
if let Some(dep_res) = config.resources.get(dep) {
if dep_res.depends_on.contains(name) && name < dep {
cycles.push((name.clone(), dep.clone()));
}
}
}
}
cycles.sort();
cycles
}
pub(crate) fn cmd_validate_check_resource_dependency_depth_limit(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let limit = 5;
let violations = find_depth_limit_violations(&config, limit);
if json {
let items: Vec<String> = violations
.iter()
.map(|(r, d)| format!("{{\"resource\":\"{r}\",\"depth\":{d}}}"))
.collect();
println!(
"{{\"depth_limit\":{},\"violations\":[{}]}}",
limit,
items.join(",")
);
} else if violations.is_empty() {
println!("All dependency chains within depth limit ({limit}).");
} else {
println!("Dependency depth violations (limit {limit}):");
for (r, d) in &violations {
println!(" {r} — depth {d}");
}
}
Ok(())
}
pub(super) fn find_depth_limit_violations(
config: &types::ForjarConfig,
limit: usize,
) -> Vec<(String, usize)> {
let names: Vec<&String> = config.resources.keys().collect();
let mut violations = Vec::new();
for name in &names {
let depth = compute_depth(config, name, &mut std::collections::HashSet::new());
if depth > limit {
violations.push(((*name).clone(), depth));
}
}
violations.sort_by(|a, b| a.0.cmp(&b.0));
violations
}
pub(super) fn compute_depth(
config: &types::ForjarConfig,
name: &str,
visited: &mut std::collections::HashSet<String>,
) -> usize {
if visited.contains(name) {
return 0;
}
visited.insert(name.to_string());
let res = match config.resources.get(name) {
Some(r) => r,
None => return 0,
};
let mut max_dep = 0;
for dep in &res.depends_on {
let d = compute_depth(config, dep, visited);
if d + 1 > max_dep {
max_dep = d + 1;
}
}
max_dep
}
pub(crate) fn cmd_validate_check_resource_unused_params(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let defined: Vec<String> = config.params.keys().cloned().collect();
let mut used = std::collections::HashSet::new();
for res in config.resources.values() {
if let Some(ref c) = res.content {
for p in &defined {
if c.contains(&format!("{{{{{p}}}}}")) || c.contains(&format!("${{{p}}}")) {
used.insert(p.clone());
}
}
}
}
let unused: Vec<&String> = defined.iter().filter(|p| !used.contains(*p)).collect();
if json {
let items: Vec<String> = unused.iter().map(|p| format!("\"{p}\"")).collect();
println!("{{\"unused_params\":[{}]}}", items.join(","));
} else if unused.is_empty() {
println!("No unused parameters detected.");
} else {
println!("Unused parameters:");
for p in &unused {
println!(" {p}");
}
}
Ok(())
}
pub(crate) fn cmd_validate_check_resource_content_hash_consistency(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let mut mismatches = Vec::new();
for (name, res) in &config.resources {
if let (Some(ref declared), Some(ref actual_content)) = (&res.checksum, &res.content) {
let computed = crate::core::planner::hash_desired_state(res);
if &computed != declared {
mismatches.push((name.clone(), declared.clone(), computed));
}
let _ = actual_content;
}
}
if json {
let items: Vec<String> = mismatches
.iter()
.map(|(n, d, c)| {
format!("{{\"resource\":\"{n}\",\"declared\":\"{d}\",\"computed\":\"{c}\"}}")
})
.collect();
println!("{{\"hash_mismatches\":[{}]}}", items.join(","));
} else if mismatches.is_empty() {
println!("All content hashes are consistent.");
} else {
println!("Content hash mismatches:");
for (n, d, c) in &mismatches {
println!(
" {} — declared: {} computed: {}",
n,
&d[..8.min(d.len())],
&c[..8.min(c.len())]
);
}
}
Ok(())
}
pub(super) use super::validate_ordering_b::*;