1use crate::core::version::{VersionConstraint, VersionError};
4use std::collections::{HashMap, HashSet, VecDeque};
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Dependency {
10 pub skill_id: String,
11 pub version_constraint: Option<VersionConstraint>,
12}
13
14impl Dependency {
15 pub fn parse(dep_str: &str) -> Result<Self, DependencyError> {
17 if let Some(at_pos) = dep_str.find('@') {
18 let skill_id = dep_str[..at_pos].trim().to_string();
19 let constraint_str = dep_str[at_pos + 1..].trim();
20
21 if constraint_str.is_empty() {
22 return Err(DependencyError::InvalidFormat(
23 "Empty version constraint after @".to_string(),
24 ));
25 }
26
27 let constraint =
28 VersionConstraint::parse(constraint_str).map_err(DependencyError::VersionError)?;
29
30 Ok(Dependency {
31 skill_id,
32 version_constraint: Some(constraint),
33 })
34 } else {
35 Ok(Dependency {
36 skill_id: dep_str.trim().to_string(),
37 version_constraint: None,
38 })
39 }
40 }
41
42 pub fn parse_multiple(dep_strings: &[String]) -> Result<Vec<Self>, DependencyError> {
44 dep_strings.iter().map(|s| Self::parse(s)).collect()
45 }
46}
47
48#[derive(Debug, Error)]
50pub enum DependencyError {
51 #[error("Circular dependency detected: {0}")]
52 CircularDependency(String),
53
54 #[error("Dependency not found: {0}")]
55 NotFound(String),
56
57 #[error("Version error: {0}")]
58 VersionError(#[from] VersionError),
59
60 #[error("Invalid dependency format: {0}")]
61 InvalidFormat(String),
62}
63
64pub struct DependencyGraph {
66 graph: HashMap<String, Vec<Dependency>>,
68 reverse_graph: HashMap<String, Vec<String>>,
70}
71
72impl DependencyGraph {
73 pub fn new() -> Self {
75 Self {
76 graph: HashMap::new(),
77 reverse_graph: HashMap::new(),
78 }
79 }
80
81 pub fn add_skill(&mut self, skill_id: String, dependencies: Vec<Dependency>) {
83 self.graph.insert(skill_id.clone(), dependencies.clone());
85
86 for dep in dependencies {
88 self.reverse_graph
89 .entry(dep.skill_id.clone())
90 .or_default()
91 .push(skill_id.clone());
92 }
93 }
94
95 pub fn build_graph(skills: Vec<(String, Vec<Dependency>)>) -> Self {
97 let mut graph = Self::new();
98 for (skill_id, deps) in skills {
99 graph.add_skill(skill_id, deps);
100 }
101 graph
102 }
103
104 pub fn detect_cycles(&self) -> Result<(), DependencyError> {
106 let mut visited = HashSet::new();
107 let mut rec_stack = HashSet::new();
108
109 for skill_id in self.graph.keys() {
110 if !visited.contains(skill_id)
111 && self.dfs_cycle_detect(skill_id, &mut visited, &mut rec_stack)
112 {
113 return Err(DependencyError::CircularDependency(
114 "Circular dependency detected in graph".to_string(),
115 ));
116 }
117 }
118
119 Ok(())
120 }
121
122 fn dfs_cycle_detect(
124 &self,
125 skill_id: &str,
126 visited: &mut HashSet<String>,
127 rec_stack: &mut HashSet<String>,
128 ) -> bool {
129 self.dfs_cycle_detect_internal(skill_id, visited, rec_stack)
130 }
131
132 fn dfs_cycle_detect_internal(
134 &self,
135 skill_id: &str,
136 visited: &mut HashSet<String>,
137 rec_stack: &mut HashSet<String>,
138 ) -> bool {
139 if rec_stack.contains(skill_id) {
140 return true; }
142
143 if visited.contains(skill_id) {
144 return false; }
146
147 visited.insert(skill_id.to_string());
148 rec_stack.insert(skill_id.to_string());
149
150 if let Some(deps) = self.graph.get(skill_id) {
151 for dep in deps {
152 if !self.graph.contains_key(&dep.skill_id) {
154 continue;
155 }
156
157 if self.dfs_cycle_detect_internal(&dep.skill_id, visited, rec_stack) {
158 return true;
159 }
160 }
161 }
162
163 rec_stack.remove(skill_id);
164 false
165 }
166
167 pub fn topological_sort(&self) -> Result<Vec<String>, DependencyError> {
170 self.detect_cycles()?;
172
173 let mut in_degree: HashMap<String, usize> = HashMap::new();
176
177 for skill_id in self.graph.keys() {
179 let dep_count = self
181 .graph
182 .get(skill_id)
183 .map(|deps| {
184 deps.iter()
185 .filter(|dep| self.graph.contains_key(&dep.skill_id))
186 .count()
187 })
188 .unwrap_or(0);
189 in_degree.insert(skill_id.clone(), dep_count);
190 }
191
192 let mut queue: VecDeque<String> = in_degree
194 .iter()
195 .filter(|(_, °ree)| degree == 0)
196 .map(|(skill_id, _)| skill_id.clone())
197 .collect();
198
199 let mut result = Vec::new();
200
201 while let Some(skill_id) = queue.pop_front() {
203 result.push(skill_id.clone());
204
205 if let Some(dependents) = self.reverse_graph.get(&skill_id) {
208 for dependent in dependents {
209 if self.graph.contains_key(dependent) {
211 if let Some(degree) = in_degree.get_mut(dependent) {
212 *degree -= 1;
213 if *degree == 0 {
214 queue.push_back(dependent.clone());
215 }
216 }
217 }
218 }
219 }
220 }
221
222 if result.len() != self.graph.len() {
224 return Err(DependencyError::CircularDependency(format!(
227 "Not all skills could be sorted ({} of {} processed)",
228 result.len(),
229 self.graph.len()
230 )));
231 }
232
233 Ok(result)
234 }
235
236 pub fn get_dependencies(&self, skill_id: &str) -> Option<&Vec<Dependency>> {
238 self.graph.get(skill_id)
239 }
240
241 pub fn get_dependents(&self, skill_id: &str) -> Vec<String> {
243 self.reverse_graph
244 .get(skill_id)
245 .cloned()
246 .unwrap_or_default()
247 }
248}
249
250impl Default for DependencyGraph {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_parse_dependency() {
263 let dep = Dependency::parse("my-skill").unwrap();
264 assert_eq!(dep.skill_id, "my-skill");
265 assert_eq!(dep.version_constraint, None);
266
267 let dep = Dependency::parse("my-skill@^1.2.3").unwrap();
268 assert_eq!(dep.skill_id, "my-skill");
269 assert!(dep.version_constraint.is_some());
270 }
271
272 #[test]
273 fn test_cycle_detection() {
274 let mut graph = DependencyGraph::new();
275 graph.add_skill(
276 "a".to_string(),
277 vec![Dependency {
278 skill_id: "b".to_string(),
279 version_constraint: None,
280 }],
281 );
282 graph.add_skill(
283 "b".to_string(),
284 vec![Dependency {
285 skill_id: "a".to_string(),
286 version_constraint: None,
287 }],
288 );
289
290 assert!(graph.detect_cycles().is_err());
291 }
292
293 #[test]
294 fn test_topological_sort() {
295 let mut graph = DependencyGraph::new();
296 graph.add_skill(
299 "a".to_string(),
300 vec![Dependency {
301 skill_id: "b".to_string(),
302 version_constraint: None,
303 }],
304 );
305 graph.add_skill(
306 "b".to_string(),
307 vec![Dependency {
308 skill_id: "c".to_string(),
309 version_constraint: None,
310 }],
311 );
312 graph.add_skill("c".to_string(), vec![]);
313
314 let order = graph.topological_sort().unwrap();
315 assert_eq!(order.len(), 3);
317 assert!(order.contains(&"a".to_string()));
318 assert!(order.contains(&"b".to_string()));
319 assert!(order.contains(&"c".to_string()));
320
321 let c_pos = order.iter().position(|x| x == "c").unwrap();
323 let b_pos = order.iter().position(|x| x == "b").unwrap();
324 let a_pos = order.iter().position(|x| x == "a").unwrap();
325
326 assert!(
327 c_pos < b_pos,
328 "c (pos {}) should come before b (pos {})",
329 c_pos,
330 b_pos
331 );
332 assert!(
333 b_pos < a_pos,
334 "b (pos {}) should come before a (pos {})",
335 b_pos,
336 a_pos
337 );
338 }
339
340 #[test]
341 fn test_topological_sort_branching() {
342 let mut graph = DependencyGraph::new();
343 graph.add_skill(
346 "a".to_string(),
347 vec![
348 Dependency {
349 skill_id: "b".to_string(),
350 version_constraint: None,
351 },
352 Dependency {
353 skill_id: "c".to_string(),
354 version_constraint: None,
355 },
356 ],
357 );
358 graph.add_skill(
359 "b".to_string(),
360 vec![Dependency {
361 skill_id: "d".to_string(),
362 version_constraint: None,
363 }],
364 );
365 graph.add_skill("c".to_string(), vec![]);
366 graph.add_skill("d".to_string(), vec![]);
367
368 let order = graph.topological_sort().unwrap();
369 assert_eq!(order.len(), 4);
370
371 let d_pos = order.iter().position(|x| x == "d").unwrap();
373 let b_pos = order.iter().position(|x| x == "b").unwrap();
374 let a_pos = order.iter().position(|x| x == "a").unwrap();
375
376 assert!(d_pos < b_pos, "d should come before b");
377 assert!(b_pos < a_pos, "b should come before a");
378 let c_pos = order.iter().position(|x| x == "c").unwrap();
380 assert!(c_pos < a_pos, "c should come before a");
381 }
382
383 #[test]
384 fn test_topological_sort_multiple_roots() {
385 let mut graph = DependencyGraph::new();
386 graph.add_skill(
389 "a".to_string(),
390 vec![Dependency {
391 skill_id: "b".to_string(),
392 version_constraint: None,
393 }],
394 );
395 graph.add_skill("b".to_string(), vec![]);
396 graph.add_skill("c".to_string(), vec![]);
397
398 let order = graph.topological_sort().unwrap();
399 assert_eq!(order.len(), 3);
400
401 let b_pos = order.iter().position(|x| x == "b").unwrap();
403 let a_pos = order.iter().position(|x| x == "a").unwrap();
404 assert!(b_pos < a_pos, "b should come before a");
405
406 assert!(order.contains(&"c".to_string()));
408 }
409}