guild_cli/graph/
project.rs1use std::collections::{HashMap, HashSet};
2
3use crate::config::{ProjectConfig, ProjectName};
4use crate::error::GraphError;
5
6#[derive(Debug)]
8pub struct ProjectGraph {
9 projects: HashMap<ProjectName, ProjectConfig>,
11 edges: HashMap<ProjectName, HashSet<ProjectName>>,
13}
14
15impl ProjectGraph {
16 pub fn build(projects: Vec<ProjectConfig>) -> Result<Self, GraphError> {
20 let mut project_map: HashMap<ProjectName, ProjectConfig> = HashMap::new();
21 let mut edges: HashMap<ProjectName, HashSet<ProjectName>> = HashMap::new();
22
23 for project in projects {
25 let name = project.name().clone();
26 edges.insert(name.clone(), HashSet::new());
27 project_map.insert(name, project);
28 }
29
30 for (name, project) in &project_map {
32 for dep in project.depends_on() {
33 if !project_map.contains_key(dep) {
34 return Err(GraphError::UnknownProject {
35 name: dep.to_string(),
36 referenced_by: name.to_string(),
37 });
38 }
39 edges.get_mut(name).unwrap().insert(dep.clone());
40 }
41 }
42
43 let graph = Self {
44 projects: project_map,
45 edges,
46 };
47
48 graph.check_cycles()?;
49
50 Ok(graph)
51 }
52
53 pub fn topological_order(&self) -> Result<Vec<&ProjectName>, GraphError> {
55 let mut visited = HashSet::new();
56 let mut temp_visited = HashSet::new();
57 let mut order = Vec::new();
58
59 for name in self.projects.keys() {
60 if !visited.contains(name) {
61 self.topo_visit(name, &mut visited, &mut temp_visited, &mut order)?;
62 }
63 }
64
65 Ok(order)
66 }
67
68 pub fn get(&self, name: &ProjectName) -> Option<&ProjectConfig> {
70 self.projects.get(name)
71 }
72
73 pub fn project_names(&self) -> impl Iterator<Item = &ProjectName> {
75 self.projects.keys()
76 }
77
78 pub fn dependencies(&self, name: &ProjectName) -> Option<&HashSet<ProjectName>> {
80 self.edges.get(name)
81 }
82
83 pub fn len(&self) -> usize {
85 self.projects.len()
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.projects.is_empty()
91 }
92
93 fn check_cycles(&self) -> Result<(), GraphError> {
94 self.topological_order().map(|_| ())
96 }
97
98 fn topo_visit<'a>(
99 &'a self,
100 name: &'a ProjectName,
101 visited: &mut HashSet<ProjectName>,
102 temp_visited: &mut HashSet<ProjectName>,
103 order: &mut Vec<&'a ProjectName>,
104 ) -> Result<(), GraphError> {
105 if temp_visited.contains(name) {
106 return Err(GraphError::CycleDetected {
107 cycle: name.to_string(),
108 });
109 }
110 if visited.contains(name) {
111 return Ok(());
112 }
113
114 temp_visited.insert(name.clone());
115
116 if let Some(deps) = self.edges.get(name) {
117 for dep in deps {
118 self.topo_visit(dep, visited, temp_visited, order)?;
119 }
120 }
121
122 temp_visited.remove(name);
123 visited.insert(name.clone());
124 order.push(name);
125
126 Ok(())
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::config::ProjectConfig;
134 use std::path::PathBuf;
135
136 fn make_project(name: &str, deps: &[&str]) -> ProjectConfig {
137 let deps_str = if deps.is_empty() {
138 String::new()
139 } else {
140 let dep_list: Vec<String> = deps.iter().map(|d| format!("\"{d}\"")).collect();
141 format!("depends_on = [{}]", dep_list.join(", "))
142 };
143 let toml = format!(
144 "[project]\nname = \"{name}\"\n{deps_str}\n\n[targets.build]\ncommand = \"echo build\"\n"
145 );
146 ProjectConfig::from_str(&toml, PathBuf::from(format!("/tmp/{name}"))).unwrap()
147 }
148
149 #[test]
150 fn test_build_simple_graph() {
151 let projects = vec![make_project("app", &["lib"]), make_project("lib", &[])];
152 let graph = ProjectGraph::build(projects).unwrap();
153 assert_eq!(graph.len(), 2);
154 }
155
156 #[test]
157 fn test_topological_order() {
158 let projects = vec![make_project("app", &["lib"]), make_project("lib", &[])];
159 let graph = ProjectGraph::build(projects).unwrap();
160 let order = graph.topological_order().unwrap();
161 let names: Vec<&str> = order.iter().map(|n| n.as_str()).collect();
162 let lib_idx = names.iter().position(|&n| n == "lib").unwrap();
163 let app_idx = names.iter().position(|&n| n == "app").unwrap();
164 assert!(lib_idx < app_idx);
165 }
166
167 #[test]
168 fn test_cycle_detection() {
169 let projects = vec![make_project("a", &["b"]), make_project("b", &["a"])];
170 assert!(ProjectGraph::build(projects).is_err());
171 }
172
173 #[test]
174 fn test_unknown_dependency() {
175 let projects = vec![make_project("app", &["nonexistent"])];
176 assert!(ProjectGraph::build(projects).is_err());
177 }
178
179 #[test]
180 fn test_empty_graph() {
181 let graph = ProjectGraph::build(vec![]).unwrap();
182 assert!(graph.is_empty());
183 }
184
185 #[test]
186 fn test_diamond_dependency() {
187 let projects = vec![
188 make_project("app", &["lib-a", "lib-b"]),
189 make_project("lib-a", &["core"]),
190 make_project("lib-b", &["core"]),
191 make_project("core", &[]),
192 ];
193 let graph = ProjectGraph::build(projects).unwrap();
194 let order = graph.topological_order().unwrap();
195 let names: Vec<&str> = order.iter().map(|n| n.as_str()).collect();
196 let core_idx = names.iter().position(|&n| n == "core").unwrap();
197 let app_idx = names.iter().position(|&n| n == "app").unwrap();
198 assert!(core_idx < app_idx);
199 }
200}