#![cfg_attr(test, allow(dead_code))]
use crate::adapters::analyzers::architecture::forbidden_rule::CompiledForbiddenRule;
use crate::adapters::analyzers::architecture::layer_rule::{LayerDefinitions, UnmatchedBehavior};
use crate::adapters::analyzers::architecture::trait_contract_rule::CompiledTraitContract;
use crate::config::architecture::{CallParityConfig, SingleTouchpointMode};
use crate::config::ArchitectureConfig;
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use std::collections::{HashMap, HashSet};
type ExternalCrates = (HashMap<String, String>, Vec<(GlobMatcher, String)>);
#[derive(Debug)]
pub struct CompiledArchitecture {
pub layers: LayerDefinitions,
pub reexport_points: GlobSet,
pub unmatched_behavior: UnmatchedBehavior,
pub external_exact: HashMap<String, String>,
pub external_glob: Vec<(GlobMatcher, String)>,
pub forbidden: Vec<CompiledForbiddenRule>,
pub trait_contracts: Vec<CompiledTraitContract>,
pub call_parity: Option<CompiledCallParity>,
}
#[derive(Debug)]
pub struct CompiledCallParity {
pub adapters: Vec<String>,
pub target: String,
pub call_depth: usize,
pub exclude_targets: GlobSet,
pub transparent_wrappers: HashSet<String>,
pub transparent_macros: HashSet<String>,
pub single_touchpoint: SingleTouchpointMode,
}
pub fn compile_architecture(cfg: &ArchitectureConfig) -> Result<CompiledArchitecture, String> {
let layers = compile_layers(cfg)?;
let reexport_points = compile_reexport_points(cfg)?;
let unmatched_behavior = parse_unmatched_behavior(&cfg.layers.unmatched_behavior)?;
let (external_exact, external_glob) = compile_external_crates(&cfg.external_crates)?;
let forbidden = compile_forbidden_rules(&cfg.forbidden_rules)?;
let trait_contracts = compile_trait_contracts(&cfg.trait_contracts)?;
let call_parity = compile_call_parity(cfg.call_parity.as_ref(), &layers)?;
Ok(CompiledArchitecture {
layers,
reexport_points,
unmatched_behavior,
external_exact,
external_glob,
forbidden,
trait_contracts,
call_parity,
})
}
const CALL_DEPTH_MAX: usize = 10;
fn compile_call_parity(
raw: Option<&CallParityConfig>,
layers: &LayerDefinitions,
) -> Result<Option<CompiledCallParity>, String> {
let Some(cp) = raw else {
return Ok(None);
};
if cp.adapters.is_empty() {
return Err("call_parity.adapters must be non-empty".to_string());
}
if !(1..=CALL_DEPTH_MAX).contains(&cp.call_depth) {
return Err(format!(
"call_parity.call_depth must be in 1..={CALL_DEPTH_MAX}, got {}",
cp.call_depth
));
}
let mut seen = HashSet::new();
for name in &cp.adapters {
if !seen.insert(name.as_str()) {
return Err(format!(
"call_parity.adapters must be disjoint — duplicate entry \"{name}\""
));
}
if layers.rank_of(name).is_none() {
return Err(format!(
"call_parity.adapters references unknown layer \"{name}\" — not listed in [architecture.layers]"
));
}
}
if seen.contains(cp.target.as_str()) {
return Err(format!(
"call_parity.target \"{}\" must not appear in call_parity.adapters",
cp.target
));
}
if layers.rank_of(&cp.target).is_none() {
return Err(format!(
"call_parity.target references unknown layer \"{}\" — not listed in [architecture.layers]",
cp.target
));
}
let exclude_targets = build_globset(&cp.exclude_targets)
.map_err(|e| format!("call_parity.exclude_targets: {e}"))?;
let transparent_wrappers: HashSet<String> = cp
.transparent_wrappers
.iter()
.map(|w| last_path_segment(w.trim()).to_string())
.filter(|s| !s.is_empty())
.collect();
let transparent_macros = build_transparent_macros(&cp.transparent_macros);
Ok(Some(CompiledCallParity {
adapters: cp.adapters.clone(),
target: cp.target.clone(),
call_depth: cp.call_depth,
exclude_targets,
transparent_wrappers,
transparent_macros,
single_touchpoint: cp.single_touchpoint,
}))
}
fn last_path_segment(path: &str) -> &str {
let without_generics = path.split('<').next().unwrap_or(path).trim();
without_generics
.rsplit("::")
.next()
.unwrap_or(without_generics)
.trim()
}
fn build_transparent_macros(user: &[String]) -> HashSet<String> {
const DEFAULTS: &[&str] = &[
"instrument", "async_trait", "main", "test", "rstest", "test_case", "pyfunction", "pymethods", "wasm_bindgen", "cfg_attr", ];
let mut set: HashSet<String> = DEFAULTS.iter().map(|s| (*s).to_string()).collect();
set.extend(
user.iter()
.map(|s| last_path_segment(s.trim()).to_string())
.filter(|s| !s.is_empty()),
);
set
}
fn compile_trait_contracts(
raw: &[crate::config::architecture::TraitContract],
) -> Result<Vec<CompiledTraitContract>, String> {
raw.iter()
.map(|tc| {
let scope = Glob::new(&tc.scope)
.map_err(|e| format!("trait_contract.scope \"{}\": {e}", tc.scope))?;
let mut b = GlobSetBuilder::new();
b.add(scope);
let scope = b
.build()
.map_err(|e| format!("trait_contract.scope: {e}"))?;
Ok(CompiledTraitContract {
name: tc.name.clone(),
scope,
receiver_may_be: tc.receiver_may_be.clone(),
required_param_type_contains: tc.required_param_type_contains.clone(),
forbidden_return_type_contains: tc
.forbidden_return_type_contains
.clone()
.unwrap_or_default(),
forbidden_error_variant_contains: tc
.forbidden_error_variant_contains
.clone()
.unwrap_or_default(),
error_types: tc.error_types.clone().unwrap_or_default(),
methods_must_be_async: tc.methods_must_be_async,
must_be_object_safe: tc.must_be_object_safe,
required_supertraits_contain: tc
.required_supertraits_contain
.clone()
.unwrap_or_default(),
})
})
.collect()
}
fn compile_layers(cfg: &ArchitectureConfig) -> Result<LayerDefinitions, String> {
let order = cfg.layers.order.clone();
let mut definitions = Vec::with_capacity(order.len());
for name in &order {
let Some(paths_cfg) = cfg.layers.definitions.get(name) else {
return Err(format!("layer \"{name}\" listed in order but has no paths"));
};
let gs = build_globset(&paths_cfg.paths)
.map_err(|e| format!("layer \"{name}\" glob error: {e}"))?;
definitions.push((name.clone(), gs));
}
Ok(LayerDefinitions::new(order, definitions))
}
fn compile_reexport_points(cfg: &ArchitectureConfig) -> Result<GlobSet, String> {
build_globset(&cfg.reexport_points.paths).map_err(|e| format!("reexport_points: {e}"))
}
fn parse_unmatched_behavior(raw: &str) -> Result<UnmatchedBehavior, String> {
match raw {
"composition_root" => Ok(UnmatchedBehavior::CompositionRoot),
"strict_error" => Ok(UnmatchedBehavior::StrictError),
other => Err(format!(
"unmatched_behavior must be \"composition_root\" or \"strict_error\", got {other:?}"
)),
}
}
fn is_exact_crate_key(key: &str) -> bool {
!key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn compile_external_crates(raw: &HashMap<String, String>) -> Result<ExternalCrates, String> {
let mut exact = HashMap::new();
let mut globs = Vec::new();
for (key, layer) in raw {
if is_exact_crate_key(key) {
exact.insert(key.clone(), layer.clone());
} else {
let matcher = Glob::new(key)
.map_err(|e| format!("external_crates \"{key}\" glob error: {e}"))?
.compile_matcher();
globs.push((matcher, layer.clone()));
}
}
Ok((exact, globs))
}
fn compile_forbidden_rules(
raw: &[crate::config::architecture::ForbiddenRule],
) -> Result<Vec<CompiledForbiddenRule>, String> {
let mut out = Vec::with_capacity(raw.len());
for rule in raw {
let from = Glob::new(&rule.from)
.map_err(|e| format!("forbidden.from \"{}\": {e}", rule.from))?
.compile_matcher();
let to = Glob::new(&rule.to)
.map_err(|e| format!("forbidden.to \"{}\": {e}", rule.to))?
.compile_matcher();
let except = build_globset(&rule.except).map_err(|e| format!("forbidden.except: {e}"))?;
out.push(CompiledForbiddenRule {
from,
to,
except,
reason: rule.reason.clone(),
});
}
Ok(out)
}
fn build_globset(patterns: &[String]) -> Result<GlobSet, String> {
let mut builder = GlobSetBuilder::new();
for pat in patterns {
let g = Glob::new(pat).map_err(|e| format!("glob \"{pat}\": {e}"))?;
builder.add(g);
}
builder.build().map_err(|e| format!("globset build: {e}"))
}