use miette::Result;
use rayon::prelude::*;
use std::path::Path;
use crate::config::load_config;
use crate::config::model::ArchitectureConfig;
use crate::parser::{ProjectFile, discover_projects};
use crate::rules::{is_ignored, resolve_layer};
pub fn run(root: &str, config_path: &str, project: &str) -> Result<()> {
let root_path = Path::new(root);
let config = load_config(Path::new(config_path))?;
let project_paths = discover_projects(root_path)?;
let projects: Vec<ProjectFile> = project_paths
.par_iter()
.filter_map(|p| ProjectFile::parse(p).ok())
.collect();
let known: Vec<&str> = projects.iter().map(|p| p.name.as_str()).collect();
if !known.contains(&project) {
eprintln!(
"Warning: '{}' not found among discovered .csproj files",
project
);
}
print!("{}", describe_project(project, &config, &known));
Ok(())
}
pub fn describe_project(
project: &str,
config: &ArchitectureConfig,
known_projects: &[&str],
) -> String {
let mut out = String::new();
if is_ignored(project, &config.ignore_patterns) {
out.push_str(&format!("Project: {project}\n"));
out.push_str("Layer: (ignored — matches an ignorePattern)\n");
return out;
}
let Some(layer) = resolve_layer(project, &config.layers) else {
out.push_str(&format!("Project: {project}\n"));
out.push_str("Layer: (unmatched — no layer pattern matches this project name)\n");
return out;
};
out.push_str(&format!("Project: {project}\n"));
out.push_str(&format!("Layer: {}\n\n", layer.name));
out.push_str("Dependency rules:\n");
for other in &config.layers {
if other.name == layer.name {
continue;
}
let rule = config
.dependency_rules
.iter()
.find(|r| r.from == layer.name && r.to == other.name);
let (status, tag) = match rule {
Some(r) if r.allowed => ("allowed ", "[explicit]"),
Some(_) => ("forbidden", "[explicit]"),
None => ("forbidden", "[default]"),
};
out.push_str(&format!(" → {:<24} {} {:<10}\n", other.name, status, tag));
}
let siblings: Vec<&str> = known_projects
.iter()
.copied()
.filter(|&p| p != project)
.filter(|p| {
resolve_layer(p, &config.layers)
.map(|l| l.name == layer.name)
.unwrap_or(false)
})
.collect();
if !siblings.is_empty() {
out.push_str("\nOther projects in this layer:\n");
for s in siblings {
out.push_str(&format!(" {s}\n"));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::model::{ArchitectureConfig, DependencyRule, Layer};
fn cfg(layers: &[(&str, &[&str])], rules: &[(&str, &str, bool)]) -> ArchitectureConfig {
ArchitectureConfig {
layers: layers
.iter()
.map(|(name, pats)| Layer {
name: name.to_string(),
patterns: pats.iter().map(|s| s.to_string()).collect(),
namespace_patterns: vec![],
})
.collect(),
dependency_rules: rules
.iter()
.map(|(from, to, allowed)| DependencyRule {
from: from.to_string(),
to: to.to_string(),
allowed: *allowed,
})
.collect(),
package_policies: vec![],
ignore_patterns: vec![],
}
}
#[test]
fn unmatched_project_says_unmatched() {
let config = cfg(&[("Domain", &["*.Domain"])], &[]);
let out = describe_project("MyApp.Unknown", &config, &[]);
assert!(out.contains("unmatched"));
}
#[test]
fn matched_project_shows_layer() {
let config = cfg(
&[
("Domain", &["*.Domain"]),
("Application", &["*.Application"]),
],
&[("Application", "Domain", true)],
);
let out = describe_project("MyApp.Domain", &config, &[]);
assert!(out.contains("Domain"));
assert!(out.contains("Application"));
assert!(out.contains("allowed") || out.contains("forbidden"));
}
#[test]
fn ignored_project_says_ignored() {
let mut config = cfg(&[("Domain", &["*.Domain"])], &[]);
config.ignore_patterns = vec!["*.Tests".to_string()];
let out = describe_project("MyApp.Tests", &config, &[]);
assert!(out.contains("ignored"));
}
#[test]
fn siblings_listed_when_present() {
let config = cfg(&[("Domain", &["*.Domain"])], &[]);
let out = describe_project(
"MyApp.Domain",
&config,
&["MyApp.Domain", "MyApp.Core.Domain"],
);
assert!(out.contains("MyApp.Core.Domain"));
}
}