Files
archlens/crates/domain/src/value_objects/source/module_name.rs
Gabriel Kaszewski fc8ad0ebc0
Some checks failed
CI / Check / Test (push) Failing after 43s
Architecture Docs / Generate diagrams (push) Successful in 3m20s
refactor: five architectural deepening improvements
Candidate 1 (NormalizedGraph): qualify→resolve→filter is now a single
named operation returning a distinct type; raw CodeGraph cannot call
module_edges/subgraph_by_module — pipeline order enforced at compile time.

Candidate 2 (Use cases): GenerateDiagram, CheckFreshness, DiffDiagram
extracted to application/src/use_cases/; presentation is now a thin CLI
dispatcher (~100 lines less, three fewer local functions).

Candidate 3 (ExtractionContext): shared accumulator for both Rust and
Python extractors replaces parallel Vec<> + 4-arg passing chains.

Candidate 4 (ModuleAssignment): ModuleName::assign() returns
ModuleAssignment { Explicit | Inferred | Unresolved } instead of Option,
callers can distinguish resolution strategies.

Candidate 5 (SplitRenderer): append_cross_module_deps removed from
DiagramRenderer port; replaced by render_for_module() default impl —
port interface now reflects what all renderers actually share.
2026-06-17 11:24:22 +02:00

131 lines
4.0 KiB
Rust

use std::collections::HashMap;
use std::path::Path;
use crate::DomainError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ModuleName(String);
/// How a module name was assigned to a source file.
#[derive(Debug, Clone)]
pub enum ModuleAssignment {
/// Matched an explicit user-configured mapping in `archlens.toml`.
Explicit(ModuleName),
/// Derived from file path structure by heuristic (e.g. `crates/<name>/...`).
Inferred(ModuleName),
/// No mapping matched and the heuristic could not determine a module.
Unresolved(&'static str),
}
impl ModuleAssignment {
pub fn module_name(&self) -> Option<&ModuleName> {
match self {
Self::Explicit(m) | Self::Inferred(m) => Some(m),
Self::Unresolved(_) => None,
}
}
pub fn into_module_name(self) -> Option<ModuleName> {
match self {
Self::Explicit(m) | Self::Inferred(m) => Some(m),
Self::Unresolved(_) => None,
}
}
pub fn reason(&self) -> Option<&'static str> {
match self {
Self::Unresolved(r) => Some(r),
_ => None,
}
}
}
impl ModuleName {
pub fn new(value: &str) -> Result<Self, DomainError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(DomainError::EmptyValue("ModuleName"));
}
Ok(Self(trimmed.to_string()))
}
/// Assign a module to a file path, returning a typed assignment that
/// distinguishes explicit mappings, heuristic inference, and failure.
pub fn assign(
file_path: &str,
root: &Path,
module_mappings: &HashMap<String, String>,
) -> ModuleAssignment {
let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let root_str = canonical_root.to_str().unwrap_or("");
let relative = file_path
.strip_prefix(root_str)
.unwrap_or(file_path)
.trim_start_matches('/');
// 1. Explicit mapping
for (pattern, module_name) in module_mappings {
if relative.starts_with(pattern.as_str())
&& let Ok(m) = Self::new(module_name)
{
return ModuleAssignment::Explicit(m);
}
}
// 2. Heuristic inference from path structure
let parts: Vec<&str> = relative.split('/').collect();
if parts.len() <= 1 {
return ModuleAssignment::Unresolved("path has no directory component");
}
let module_dir = if (parts[0] == "crates" || parts[0] == "src") && parts.len() > 2 {
parts[1]
} else if parts[0] != "src" && parts.len() > 1 {
parts[0]
} else {
return ModuleAssignment::Unresolved("path under src/ with no further structure");
};
match Self::new(&Self::capitalize(module_dir)) {
Ok(m) => ModuleAssignment::Inferred(m),
Err(_) => ModuleAssignment::Unresolved("inferred directory name was empty"),
}
}
/// Convenience wrapper — returns None for Unresolved.
/// Prefer `assign()` when you want to distinguish strategies.
pub fn from_path(
file_path: &str,
root: &Path,
module_mappings: &HashMap<String, String>,
) -> Option<Self> {
Self::assign(file_path, root, module_mappings).into_module_name()
}
pub fn from_directory_group(member_path: &str) -> Option<Self> {
let parts: Vec<&str> = member_path.split('/').collect();
if parts.len() < 3 {
return None;
}
let group = parts[parts.len() - 2];
Self::new(&Self::capitalize(group)).ok()
}
pub fn capitalize(s: &str) -> String {
s.split('-')
.map(|seg| {
if seg.is_empty() {
String::new()
} else {
format!("{}{}", seg[..1].to_uppercase(), &seg[1..])
}
})
.collect::<Vec<_>>()
.join("-")
}
pub fn as_str(&self) -> &str {
&self.0
}
}