use std::collections::{HashMap, HashSet};
use crate::config::{ProjectConfig, ProjectName};
use crate::error::GraphError;
#[derive(Debug)]
pub struct ProjectGraph {
projects: HashMap<ProjectName, ProjectConfig>,
edges: HashMap<ProjectName, HashSet<ProjectName>>,
}
impl ProjectGraph {
pub fn build(projects: Vec<ProjectConfig>) -> Result<Self, GraphError> {
let mut project_map: HashMap<ProjectName, ProjectConfig> = HashMap::new();
let mut edges: HashMap<ProjectName, HashSet<ProjectName>> = HashMap::new();
for project in projects {
let name = project.name().clone();
edges.insert(name.clone(), HashSet::new());
project_map.insert(name, project);
}
for (name, project) in &project_map {
for dep in project.depends_on() {
if !project_map.contains_key(dep) {
return Err(GraphError::UnknownProject {
name: dep.to_string(),
referenced_by: name.to_string(),
});
}
edges.get_mut(name).unwrap().insert(dep.clone());
}
}
let graph = Self {
projects: project_map,
edges,
};
graph.check_cycles()?;
Ok(graph)
}
pub fn topological_order(&self) -> Result<Vec<&ProjectName>, GraphError> {
let mut visited = HashSet::new();
let mut temp_visited = HashSet::new();
let mut order = Vec::new();
for name in self.projects.keys() {
if !visited.contains(name) {
self.topo_visit(name, &mut visited, &mut temp_visited, &mut order)?;
}
}
Ok(order)
}
pub fn get(&self, name: &ProjectName) -> Option<&ProjectConfig> {
self.projects.get(name)
}
pub fn project_names(&self) -> impl Iterator<Item = &ProjectName> {
self.projects.keys()
}
pub fn dependencies(&self, name: &ProjectName) -> Option<&HashSet<ProjectName>> {
self.edges.get(name)
}
pub fn len(&self) -> usize {
self.projects.len()
}
pub fn is_empty(&self) -> bool {
self.projects.is_empty()
}
fn check_cycles(&self) -> Result<(), GraphError> {
self.topological_order().map(|_| ())
}
fn topo_visit<'a>(
&'a self,
name: &'a ProjectName,
visited: &mut HashSet<ProjectName>,
temp_visited: &mut HashSet<ProjectName>,
order: &mut Vec<&'a ProjectName>,
) -> Result<(), GraphError> {
if temp_visited.contains(name) {
return Err(GraphError::CycleDetected {
cycle: name.to_string(),
});
}
if visited.contains(name) {
return Ok(());
}
temp_visited.insert(name.clone());
if let Some(deps) = self.edges.get(name) {
for dep in deps {
self.topo_visit(dep, visited, temp_visited, order)?;
}
}
temp_visited.remove(name);
visited.insert(name.clone());
order.push(name);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ProjectConfig;
use std::path::PathBuf;
fn make_project(name: &str, deps: &[&str]) -> ProjectConfig {
let deps_str = if deps.is_empty() {
String::new()
} else {
let dep_list: Vec<String> = deps.iter().map(|d| format!("\"{d}\"")).collect();
format!("depends_on = [{}]", dep_list.join(", "))
};
let toml = format!(
"[project]\nname = \"{name}\"\n{deps_str}\n\n[targets.build]\ncommand = \"echo build\"\n"
);
ProjectConfig::from_str(&toml, PathBuf::from(format!("/tmp/{name}"))).unwrap()
}
#[test]
fn test_build_simple_graph() {
let projects = vec![make_project("app", &["lib"]), make_project("lib", &[])];
let graph = ProjectGraph::build(projects).unwrap();
assert_eq!(graph.len(), 2);
}
#[test]
fn test_topological_order() {
let projects = vec![make_project("app", &["lib"]), make_project("lib", &[])];
let graph = ProjectGraph::build(projects).unwrap();
let order = graph.topological_order().unwrap();
let names: Vec<&str> = order.iter().map(|n| n.as_str()).collect();
let lib_idx = names.iter().position(|&n| n == "lib").unwrap();
let app_idx = names.iter().position(|&n| n == "app").unwrap();
assert!(lib_idx < app_idx);
}
#[test]
fn test_cycle_detection() {
let projects = vec![make_project("a", &["b"]), make_project("b", &["a"])];
assert!(ProjectGraph::build(projects).is_err());
}
#[test]
fn test_unknown_dependency() {
let projects = vec![make_project("app", &["nonexistent"])];
assert!(ProjectGraph::build(projects).is_err());
}
#[test]
fn test_empty_graph() {
let graph = ProjectGraph::build(vec![]).unwrap();
assert!(graph.is_empty());
}
#[test]
fn test_diamond_dependency() {
let projects = vec![
make_project("app", &["lib-a", "lib-b"]),
make_project("lib-a", &["core"]),
make_project("lib-b", &["core"]),
make_project("core", &[]),
];
let graph = ProjectGraph::build(projects).unwrap();
let order = graph.topological_order().unwrap();
let names: Vec<&str> = order.iter().map(|n| n.as_str()).collect();
let core_idx = names.iter().position(|&n| n == "core").unwrap();
let app_idx = names.iter().position(|&n| n == "app").unwrap();
assert!(core_idx < app_idx);
}
}