aivcs_core/multi_repo/
model.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
9pub struct RepoId {
10 pub name: String,
12}
13
14impl RepoId {
15 pub fn new(name: impl Into<String>) -> Self {
16 Self { name: name.into() }
17 }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct RepoDependency {
23 pub dependent: RepoId,
24 pub dependency: RepoId,
25}
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct CrossRepoGraph {
31 pub repos: Vec<RepoId>,
33 pub dependencies: Vec<RepoDependency>,
35}
36
37impl CrossRepoGraph {
38 pub fn new(repos: Vec<RepoId>, dependencies: Vec<RepoDependency>) -> Self {
39 Self {
40 repos,
41 dependencies,
42 }
43 }
44
45 pub fn execution_order(&self) -> Result<Vec<RepoId>, String> {
48 use std::collections::{HashMap, HashSet, VecDeque};
49
50 let names: HashSet<&str> = self.repos.iter().map(|r| r.name.as_str()).collect();
51 let mut in_degree: HashMap<&str, u32> = names.iter().map(|&n| (n, 0)).collect();
52 let mut out_edges: HashMap<&str, Vec<&str>> =
53 names.iter().map(|&n| (n, Vec::new())).collect();
54
55 for d in &self.dependencies {
56 if !names.contains(d.dependency.name.as_str())
57 || !names.contains(d.dependent.name.as_str())
58 {
59 continue;
60 }
61 out_edges
62 .get_mut(d.dependency.name.as_str())
63 .unwrap()
64 .push(d.dependent.name.as_str());
65 *in_degree.get_mut(d.dependent.name.as_str()).unwrap() += 1;
66 }
67
68 let mut queue: VecDeque<&str> = in_degree
69 .iter()
70 .filter(|(_, &d)| d == 0)
71 .map(|(&n, _)| n)
72 .collect();
73 let mut order = Vec::with_capacity(self.repos.len());
74
75 while let Some(n) = queue.pop_front() {
76 order.push(RepoId::new(n));
77 for &m in out_edges.get(n).unwrap_or(&vec![]) {
78 let deg = in_degree.get_mut(m).unwrap();
79 *deg -= 1;
80 if *deg == 0 {
81 queue.push_back(m);
82 }
83 }
84 }
85
86 if order.len() != self.repos.len() {
87 return Err("cycle detected in repo dependencies".to_string());
88 }
89 Ok(order)
90 }
91
92 pub fn has_cycle(&self) -> bool {
94 self.execution_order().is_err()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_repo_id_equality() {
104 let a = RepoId::new("org/a");
105 let b = RepoId::new("org/a");
106 let c = RepoId::new("org/c");
107 assert_eq!(a, b);
108 assert_ne!(a, c);
109 }
110
111 #[test]
112 fn test_execution_order_linear() {
113 let a = RepoId::new("a");
114 let b = RepoId::new("b");
115 let c = RepoId::new("c");
116 let graph = CrossRepoGraph::new(
117 vec![a.clone(), b.clone(), c.clone()],
118 vec![
119 RepoDependency {
120 dependent: b.clone(),
121 dependency: a.clone(),
122 },
123 RepoDependency {
124 dependent: c.clone(),
125 dependency: b.clone(),
126 },
127 ],
128 );
129 let order = graph.execution_order().expect("no cycle");
130 assert_eq!(order.len(), 3);
131 assert_eq!(order[0].name, "a");
132 assert_eq!(order[1].name, "b");
133 assert_eq!(order[2].name, "c");
134 }
135
136 #[test]
137 fn test_execution_order_diamond() {
138 let a = RepoId::new("a");
139 let b = RepoId::new("b");
140 let c = RepoId::new("c");
141 let d = RepoId::new("d");
142 let graph = CrossRepoGraph::new(
143 vec![a.clone(), b.clone(), c.clone(), d.clone()],
144 vec![
145 RepoDependency {
146 dependent: b.clone(),
147 dependency: a.clone(),
148 },
149 RepoDependency {
150 dependent: c.clone(),
151 dependency: a.clone(),
152 },
153 RepoDependency {
154 dependent: d.clone(),
155 dependency: b.clone(),
156 },
157 RepoDependency {
158 dependent: d.clone(),
159 dependency: c.clone(),
160 },
161 ],
162 );
163 let order = graph.execution_order().expect("no cycle");
164 assert_eq!(order.len(), 4);
165 assert_eq!(order[0].name, "a");
166 let pos_b = order.iter().position(|r| r.name == "b").unwrap();
167 let pos_c = order.iter().position(|r| r.name == "c").unwrap();
168 let pos_d = order.iter().position(|r| r.name == "d").unwrap();
169 assert!(pos_b < pos_d && pos_c < pos_d);
170 }
171
172 #[test]
173 fn test_cycle_detected() {
174 let a = RepoId::new("a");
175 let b = RepoId::new("b");
176 let graph = CrossRepoGraph::new(
177 vec![a.clone(), b.clone()],
178 vec![
179 RepoDependency {
180 dependent: b.clone(),
181 dependency: a.clone(),
182 },
183 RepoDependency {
184 dependent: a.clone(),
185 dependency: b.clone(),
186 },
187 ],
188 );
189 assert!(graph.execution_order().is_err());
190 assert!(graph.has_cycle());
191 }
192
193 #[test]
194 fn test_serde_repo_id() {
195 let id = RepoId::new("stevedores-org/aivcs");
196 let json = serde_json::to_string(&id).expect("serialize");
197 let back: RepoId = serde_json::from_str(&json).expect("deserialize");
198 assert_eq!(id, back);
199 }
200}