use crate::model::{Domain, ProjectSpec};
use std::collections::{BTreeMap, BTreeSet};
pub fn cluster_domains(spec: &mut ProjectSpec) {
let mut prefix_groups: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for cap in &spec.capabilities {
let prefix = extract_domain_prefix(&cap.name);
prefix_groups
.entry(prefix)
.or_default()
.insert(cap.name.clone());
}
let merged = merge_connected_groups(&prefix_groups, &spec.dependencies);
let mut domains = Vec::new();
for (domain_name, caps) in &merged {
let mut external_deps = BTreeSet::new();
for dep in &spec.dependencies {
if caps.contains(&dep.from) && !caps.contains(&dep.to) {
for (other_domain, other_caps) in &merged {
if other_caps.contains(&dep.to) && other_domain != domain_name {
external_deps.insert(other_domain.clone());
}
}
}
}
domains.push(Domain {
name: domain_name.clone(),
capabilities: caps.iter().cloned().collect(),
external_dependencies: external_deps.into_iter().collect(),
});
}
domains.sort_by(|a, b| a.name.cmp(&b.name));
spec.domains = domains;
}
fn extract_domain_prefix(name: &str) -> String {
let suffixes = [
"-controller",
"-service",
"-repository",
"-entity",
"-rest",
"-db-context",
];
for suffix in &suffixes {
if let Some(prefix) = name.strip_suffix(suffix) {
return prefix.to_string();
}
}
name.to_string()
}
fn merge_connected_groups(
groups: &BTreeMap<String, BTreeSet<String>>,
dependencies: &[crate::model::DependencyEdge],
) -> BTreeMap<String, BTreeSet<String>> {
let mut result = groups.clone();
let small_groups: Vec<String> = result
.iter()
.filter(|(_, caps)| caps.len() == 1)
.map(|(name, _)| name.clone())
.collect();
for small_name in &small_groups {
let small_caps = result.get(small_name).cloned().unwrap_or_default();
let cap_name = small_caps.iter().next().cloned().unwrap_or_default();
let mut best_target: Option<String> = None;
for dep in dependencies {
let other = if dep.from == cap_name {
&dep.to
} else if dep.to == cap_name {
&dep.from
} else {
continue;
};
for (group_name, group_caps) in &result {
if group_caps.contains(other) && group_name != small_name && group_caps.len() > 1 {
best_target = Some(group_name.clone());
break;
}
}
if best_target.is_some() {
break;
}
}
if let Some(target) = best_target {
let caps_to_move = result.remove(small_name).unwrap_or_default();
if let Some(target_caps) = result.get_mut(&target) {
target_caps.extend(caps_to_move);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::*;
#[test]
fn test_domain_clustering_by_prefix() {
let mut spec = ProjectSpec::new("test");
spec.capabilities
.push(Capability::new("user-controller", "UserController.java"));
spec.capabilities
.push(Capability::new("user-entity", "User.java"));
spec.capabilities
.push(Capability::new("user-service", "UserService.java"));
spec.capabilities
.push(Capability::new("order-controller", "OrderController.java"));
spec.capabilities
.push(Capability::new("order-entity", "Order.java"));
cluster_domains(&mut spec);
assert_eq!(spec.domains.len(), 2);
let user_domain = spec.domains.iter().find(|d| d.name == "user").unwrap();
assert_eq!(user_domain.capabilities.len(), 3);
let order_domain = spec.domains.iter().find(|d| d.name == "order").unwrap();
assert_eq!(order_domain.capabilities.len(), 2);
}
#[test]
fn test_domain_external_dependencies() {
let mut spec = ProjectSpec::new("test");
spec.capabilities
.push(Capability::new("order-service", "OrderService.java"));
spec.capabilities
.push(Capability::new("user-service", "UserService.java"));
spec.dependencies.push(DependencyEdge {
from: "order-service".to_string(),
to: "user-service".to_string(),
kind: DependencyKind::Calls,
references: vec![],
});
cluster_domains(&mut spec);
let order_domain = spec.domains.iter().find(|d| d.name == "order").unwrap();
assert!(
order_domain
.external_dependencies
.contains(&"user".to_string())
);
}
}