1use std::collections::{HashMap, HashSet, VecDeque};
2
3use crate::error::{DeployError, Result};
4use crate::types::ServiceConfigView;
5
6#[derive(Debug, Clone)]
7pub struct ServiceNode {
8 pub name: String,
9 pub depends_on: Vec<String>,
10}
11
12#[derive(Debug, Clone, Default)]
13pub struct ServiceGraph {
14 pub nodes: HashMap<String, ServiceNode>,
15}
16
17impl ServiceGraph {
18 pub fn from_services(services: &[ServiceConfigView]) -> Self {
19 let mut nodes = HashMap::new();
20 for svc in services {
21 nodes.insert(
22 svc.name.clone(),
23 ServiceNode {
24 name: svc.name.clone(),
25 depends_on: svc.depends_on.clone(),
26 },
27 );
28 }
29 Self { nodes }
30 }
31}
32
33pub fn order_services(
39 selected: &[String],
40 group_order: Option<&[String]>,
41 graph: &ServiceGraph,
42) -> Result<Vec<String>> {
43 if let Some(order) = group_order {
44 if !order.is_empty() {
45 let selected_set: HashSet<&str> = selected.iter().map(String::as_str).collect();
46 let mut out = Vec::new();
47 for name in order {
48 if selected_set.contains(name.as_str()) {
49 out.push(name.clone());
50 }
51 }
52 for name in selected {
54 if !out.iter().any(|n| n == name) {
55 out.push(name.clone());
56 }
57 }
58 return Ok(out);
59 }
60 }
61
62 let selected_set: HashSet<String> = selected.iter().cloned().collect();
64 for name in selected {
65 let Some(node) = graph.nodes.get(name) else {
66 continue;
67 };
68 for dep in &node.depends_on {
69 if !graph.nodes.contains_key(dep) {
70 return Err(DeployError::Graph(format!(
71 "service `{name}` depends on missing service `{dep}`"
72 )));
73 }
74 }
75 }
76
77 let mut indegree: HashMap<String, usize> = selected
79 .iter()
80 .map(|n| (n.clone(), 0usize))
81 .collect();
82 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
83
84 for name in selected {
85 let deps = graph
86 .nodes
87 .get(name)
88 .map(|n| n.depends_on.clone())
89 .unwrap_or_default();
90 for dep in deps {
91 if !selected_set.contains(&dep) {
92 continue;
93 }
94 adj.entry(dep.clone()).or_default().push(name.clone());
95 *indegree.entry(name.clone()).or_default() += 1;
96 }
97 }
98
99 let mut queue: VecDeque<String> = selected
100 .iter()
101 .filter(|n| indegree.get(*n).copied().unwrap_or(0) == 0)
102 .cloned()
103 .collect();
104
105 queue.make_contiguous().sort_by_key(|n| {
107 selected.iter().position(|s| s == n).unwrap_or(usize::MAX)
108 });
109
110 let mut ordered = Vec::new();
111 while let Some(node) = queue.pop_front() {
112 ordered.push(node.clone());
113 if let Some(children) = adj.get(&node) {
114 for child in children {
115 if let Some(d) = indegree.get_mut(child) {
116 *d = d.saturating_sub(1);
117 if *d == 0 {
118 queue.push_back(child.clone());
119 }
120 }
121 }
122 }
123 }
124
125 if ordered.len() != selected.len() {
126 return Err(DeployError::Graph(
127 "dependency cycle detected among selected services".into(),
128 ));
129 }
130 Ok(ordered)
131}
132
133pub fn enforce_athena_operator_before_runtime(order: &mut Vec<String>) {
135 let op = order.iter().position(|s| s == "athena-operator");
136 let rt = order.iter().position(|s| s == "athena-runtime");
137 if let (Some(i), Some(j)) = (op, rt) {
138 if i > j {
139 order.swap(i, j);
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn svc(name: &str, deps: &[&str]) -> ServiceConfigView {
149 ServiceConfigView {
150 name: name.into(),
151 root_directory: None,
152 version: None,
153 depends_on: deps.iter().map(|s| (*s).into()).collect(),
154 oci: None,
155 deploy: None,
156 }
157 }
158
159 #[test]
160 fn group_order_authoritative() {
161 let services = vec![svc("a", &[]), svc("b", &[]), svc("c", &[])];
162 let graph = ServiceGraph::from_services(&services);
163 let order = order_services(
164 &["a".into(), "b".into(), "c".into()],
165 Some(&["c".into(), "a".into(), "b".into()]),
166 &graph,
167 )
168 .unwrap();
169 assert_eq!(order, vec!["c", "a", "b"]);
170 }
171
172 #[test]
173 fn topo_respects_depends_on() {
174 let services = vec![svc("a", &[]), svc("b", &["a"]), svc("c", &["b"])];
175 let graph = ServiceGraph::from_services(&services);
176 let order = order_services(
177 &["c".into(), "b".into(), "a".into()],
178 None,
179 &graph,
180 )
181 .unwrap();
182 assert_eq!(order, vec!["a", "b", "c"]);
183 }
184
185 #[test]
186 fn cycle_errors() {
187 let services = vec![svc("a", &["b"]), svc("b", &["a"])];
188 let graph = ServiceGraph::from_services(&services);
189 let err = order_services(&["a".into(), "b".into()], None, &graph).unwrap_err();
190 assert!(err.to_string().contains("cycle"));
191 }
192
193 #[test]
194 fn athena_guardrail() {
195 let mut order = vec!["athena-runtime".into(), "athena-operator".into()];
196 enforce_athena_operator_before_runtime(&mut order);
197 assert_eq!(order[0], "athena-operator");
198 }
199}