use std::collections::BTreeSet;
use super::graph::{ModuleDescriptor, ModuleNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphError {
DuplicateModule { name: &'static str },
UnknownImport {
module: &'static str,
unknown_import: &'static str,
},
CircularDependency { cycle: Vec<&'static str> },
}
impl std::fmt::Display for GraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateModule { name } => {
write!(f, "duplicate module `{name}`")
}
Self::UnknownImport {
module,
unknown_import,
} => {
write!(
f,
"module `{module}` imports unknown module `{unknown_import}`"
)
}
Self::CircularDependency { cycle } => {
let path = cycle
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" -> ");
write!(f, "circular dependency: {path}")
}
}
}
}
impl std::error::Error for GraphError {}
#[derive(Debug, Clone)]
pub struct ApplicationGraph {
modules: Vec<ModuleDescriptor>,
}
impl ApplicationGraph {
pub fn new(modules: Vec<ModuleDescriptor>) -> Result<Self, GraphError> {
check_duplicates(&modules)?;
check_unknown_imports(&modules)?;
check_cycles(&modules)?;
Ok(Self { modules })
}
pub fn new_unchecked(modules: Vec<ModuleDescriptor>) -> Self {
Self { modules }
}
pub fn modules(&self) -> &[ModuleDescriptor] {
&self.modules
}
pub fn nodes(&self) -> Vec<ModuleNode> {
self.modules.iter().map(ModuleNode::from).collect()
}
pub fn find(&self, name: &str) -> Option<&ModuleDescriptor> {
self.modules.iter().find(|m| m.name == name)
}
pub fn len(&self) -> usize {
self.modules.len()
}
pub fn is_empty(&self) -> bool {
self.modules.is_empty()
}
}
fn check_duplicates(modules: &[ModuleDescriptor]) -> Result<(), GraphError> {
let mut seen = BTreeSet::new();
for m in modules {
if !seen.insert(m.name) {
return Err(GraphError::DuplicateModule { name: m.name });
}
}
Ok(())
}
fn check_unknown_imports(modules: &[ModuleDescriptor]) -> Result<(), GraphError> {
let known: BTreeSet<&'static str> = modules.iter().map(|m| m.name).collect();
for m in modules {
for &import in m.imports {
if !known.contains(import) {
return Err(GraphError::UnknownImport {
module: m.name,
unknown_import: import,
});
}
}
}
Ok(())
}
fn check_cycles(modules: &[ModuleDescriptor]) -> Result<(), GraphError> {
let node_map = super::graph::module_node_map(modules);
let mut visited: BTreeSet<&'static str> = BTreeSet::new();
let mut stack: Vec<&'static str> = Vec::new();
let mut on_stack: BTreeSet<&'static str> = BTreeSet::new();
for m in modules {
if visited.contains(m.name) {
continue;
}
if let Some(cycle) =
dfs_find_cycle(m.name, &node_map, &mut visited, &mut stack, &mut on_stack)
{
return Err(GraphError::CircularDependency { cycle });
}
}
Ok(())
}
fn dfs_find_cycle(
node: &'static str,
node_map: &std::collections::BTreeMap<&'static str, ModuleNode>,
visited: &mut BTreeSet<&'static str>,
stack: &mut Vec<&'static str>,
on_stack: &mut BTreeSet<&'static str>,
) -> Option<Vec<&'static str>> {
visited.insert(node);
stack.push(node);
on_stack.insert(node);
if let Some(n) = node_map.get(node) {
for &import in &n.imports {
if !visited.contains(import) {
if let Some(cycle) = dfs_find_cycle(import, node_map, visited, stack, on_stack) {
return Some(cycle);
}
} else if on_stack.contains(import) {
let cycle_start = stack.iter().position(|&s| s == import).unwrap();
let mut cycle: Vec<&'static str> = stack[cycle_start..].to_vec();
cycle.push(import); return Some(cycle);
}
}
}
stack.pop();
on_stack.remove(node);
None
}
#[cfg(test)]
mod tests {
use super::*;
fn mod_desc(name: &'static str, imports: &'static [&'static str]) -> ModuleDescriptor {
ModuleDescriptor {
name,
imports,
exports: &[],
controllers: &[],
controller_methods: &[],
services: &[],
policies: &[],
routes: &[],
listeners: &[],
jobs: &[],
commands: &[],
schedules: &[],
}
}
#[test]
fn empty_graph_is_valid() {
let g = ApplicationGraph::new(vec![]).unwrap();
assert!(g.is_empty());
}
#[test]
fn single_module_is_valid() {
let g = ApplicationGraph::new(vec![mod_desc("Accounts", &[])]).unwrap();
assert_eq!(g.len(), 1);
}
#[test]
fn duplicate_module_is_error() {
let err = ApplicationGraph::new(vec![mod_desc("Accounts", &[]), mod_desc("Accounts", &[])])
.unwrap_err();
assert_eq!(err, GraphError::DuplicateModule { name: "Accounts" });
}
#[test]
fn unknown_import_is_error() {
let err = ApplicationGraph::new(vec![mod_desc("Billing", &["Nonexistent"])]).unwrap_err();
assert_eq!(
err,
GraphError::UnknownImport {
module: "Billing",
unknown_import: "Nonexistent"
}
);
}
#[test]
fn linear_dependency_is_valid() {
let g = ApplicationGraph::new(vec![
mod_desc("Accounts", &[]),
mod_desc("Billing", &["Accounts"]),
mod_desc("Checkout", &["Billing"]),
])
.unwrap();
assert_eq!(g.len(), 3);
}
#[test]
fn two_module_cycle_is_detected() {
let err =
ApplicationGraph::new(vec![mod_desc("A", &["B"]), mod_desc("B", &["A"])]).unwrap_err();
match err {
GraphError::CircularDependency { cycle } => {
assert!(
cycle.len() >= 3,
"cycle should have at least 3 nodes: {cycle:?}"
);
assert_eq!(
cycle.first(),
cycle.last(),
"cycle should be closed: {cycle:?}"
);
}
other => panic!("expected CircularDependency, got {other:?}"),
}
}
#[test]
fn three_module_cycle_is_detected() {
let err = ApplicationGraph::new(vec![
mod_desc("A", &["B"]),
mod_desc("B", &["C"]),
mod_desc("C", &["A"]),
])
.unwrap_err();
assert!(matches!(err, GraphError::CircularDependency { .. }));
}
#[test]
fn find_module_by_name() {
let g = ApplicationGraph::new(vec![mod_desc("Accounts", &[])]).unwrap();
assert!(g.find("Accounts").is_some());
assert!(g.find("Nonexistent").is_none());
}
#[test]
fn graph_error_display() {
let err = GraphError::DuplicateModule { name: "Foo" };
assert_eq!(err.to_string(), "duplicate module `Foo`");
}
}