1use std::collections::BTreeSet;
19
20use oxgraph::algo::{strongly_connected_components, topological_sort};
21use oxgraph::graph_build::{GraphBuildError, GraphBuilder};
22use serde::Serialize;
23
24use super::error::DefError;
25use super::interp::{self, Reference};
26use super::model::StackDef;
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
29#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
30pub enum Node {
31 Integration(String),
32 Service(String),
33}
34
35impl Node {
36 pub fn name(&self) -> &str {
37 match self {
38 Self::Integration(name) | Self::Service(name) => name,
39 }
40 }
41}
42
43#[derive(Debug, Serialize)]
45pub struct DependencyGraph {
46 nodes: Vec<Node>,
47 startup_order: Vec<Node>,
49 wiring: Vec<(Node, Node)>,
52}
53
54impl DependencyGraph {
55 pub fn derive(def: &StackDef) -> Result<Self, DefError> {
60 let nodes: Vec<Node> = def
63 .integrations
64 .keys()
65 .map(|name| Node::Integration(name.clone()))
66 .chain(def.services.keys().map(|name| Node::Service(name.clone())))
67 .collect();
68 let index_of =
69 |node: &Node| -> Option<u32> { nodes.iter().position(|n| n == node).map(|i| i as u32) };
70
71 let mut ordering_edges: BTreeSet<(u32, u32)> = BTreeSet::new();
72 let mut wiring: BTreeSet<(u32, u32)> = BTreeSet::new();
73 for (service_name, service) in &def.services {
74 let service_node = Node::Service(service_name.clone());
75 let Some(service_idx) = index_of(&service_node) else {
76 continue;
77 };
78 let mut values: Vec<(String, String)> = service
79 .env
80 .iter()
81 .map(|(k, v)| (format!("services.{service_name}.env.{k}"), v.clone()))
82 .collect();
83 for substrate in service.substrates.keys() {
84 for (k, v) in service.substrate_env(service_name, substrate)? {
85 values.push((format!("services.{service_name}.{substrate}.env.{k}"), v));
86 }
87 }
88 for (location, value) in &values {
89 for reference in interp::references(value, location)? {
90 let target = match reference {
91 Reference::ServiceOrigin(name) => Node::Service(name),
92 Reference::IntegrationOutput { integration, .. } => {
93 Node::Integration(integration)
94 }
95 Reference::StackName
99 | Reference::InstanceName
100 | Reference::Secret(_)
101 | Reference::DatastoreUrl(_) => {
102 continue;
103 }
104 };
105 let Some(target_idx) = index_of(&target) else {
106 continue;
107 };
108 wiring.insert((service_idx, target_idx));
109 if matches!(target, Node::Integration(_)) {
110 ordering_edges.insert((target_idx, service_idx));
113 }
114 }
115 }
116 }
117
118 if let Some(verify) = &def.stack.verify {
119 for (key, value) in &verify.env {
120 let location = format!("stack.verify.env.{key}");
121 for reference in interp::references(value, &location)? {
122 let Reference::IntegrationOutput { integration, .. } = reference else {
123 continue;
124 };
125 let target = Node::Integration(integration);
126 let Some(target_idx) = index_of(&target) else {
127 continue;
128 };
129 let _ = target_idx;
130 }
131 }
132 for (tier, spec) in &verify.tiers {
133 for (key, value) in &spec.env {
134 let location = format!("stack.verify.tiers.{tier}.env.{key}");
135 for reference in interp::references(value, &location)? {
136 let Reference::IntegrationOutput { integration, .. } = reference else {
137 continue;
138 };
139 let target = Node::Integration(integration);
140 let Some(target_idx) = index_of(&target) else {
141 continue;
142 };
143 let _ = target_idx;
144 }
145 }
146 }
147 }
148
149 let mut builder = GraphBuilder::<u32, u32>::new();
150 let mut node_ids = Vec::with_capacity(nodes.len());
151 for _ in &nodes {
152 node_ids.push(builder.add_node().map_err(internal_layout_error)?);
153 }
154 for &(from, to) in &ordering_edges {
155 builder
156 .add_edge(node_ids[from as usize], node_ids[to as usize])
157 .map_err(internal_layout_error)?;
158 }
159 let graph = builder.freeze().map_err(internal_layout_error)?;
160
161 let order = match topological_sort(&graph, &node_ids) {
162 Ok(order) => order,
163 Err(_) => {
166 let cycle = strongly_connected_components(&graph, &node_ids)
167 .into_iter()
168 .filter(|component| component.len() > 1)
169 .flatten()
170 .map(|id| nodes[id.get() as usize].name().to_owned())
171 .collect::<Vec<_>>()
172 .join(" -> ");
173 return Err(DefError::WiringCycle { nodes: cycle });
174 }
175 };
176
177 let startup_order = order
178 .into_iter()
179 .map(|id| nodes[id.get() as usize].clone())
180 .collect();
181 let wiring = wiring
182 .into_iter()
183 .map(|(from, to)| (nodes[from as usize].clone(), nodes[to as usize].clone()))
184 .collect();
185 Ok(Self {
186 nodes,
187 startup_order,
188 wiring,
189 })
190 }
191
192 pub fn nodes(&self) -> &[Node] {
193 &self.nodes
194 }
195
196 pub fn startup_order(&self) -> &[Node] {
197 &self.startup_order
198 }
199
200 pub fn wiring(&self) -> &[(Node, Node)] {
201 &self.wiring
202 }
203}
204
205fn internal_layout_error(err: GraphBuildError<u32, u32>) -> DefError {
208 DefError::WiringCycle {
209 nodes: format!("internal graph layout error: {err:?}"),
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use oxgraph::algo::ToposortError;
217 use oxgraph::graph_build::{FrozenGraph, GraphNodeId};
218
219 fn build(
220 node_count: u32,
221 edges: &[(u32, u32)],
222 ) -> (FrozenGraph<u32, u32>, Vec<GraphNodeId<u32>>) {
223 let mut builder = GraphBuilder::<u32, u32>::new();
224 let ids: Vec<_> = (0..node_count)
225 .map(|_| builder.add_node().unwrap())
226 .collect();
227 for &(from, to) in edges {
228 builder
229 .add_edge(ids[from as usize], ids[to as usize])
230 .unwrap();
231 }
232 (builder.freeze().unwrap(), ids)
233 }
234
235 #[test]
236 fn topological_sort_orders_a_dag() {
237 let (graph, ids) = build(4, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
239 let order: Vec<u32> = topological_sort(&graph, &ids)
240 .unwrap()
241 .into_iter()
242 .map(|id| id.get())
243 .collect();
244 assert_eq!(order, vec![0, 1, 2, 3]);
245 }
246
247 #[test]
248 fn scc_reports_the_cycle_members() {
249 let (graph, ids) = build(4, &[(0, 1), (1, 2), (2, 1)]);
251 assert_eq!(
252 topological_sort(&graph, &ids).unwrap_err(),
253 ToposortError::Cycle
254 );
255 let mut cycle: Vec<u32> = strongly_connected_components(&graph, &ids)
256 .into_iter()
257 .filter(|component| component.len() > 1)
258 .flatten()
259 .map(|id| id.get())
260 .collect();
261 cycle.sort_unstable();
262 assert_eq!(cycle, vec![1, 2]);
263 }
264
265 #[test]
266 fn topological_sort_handles_no_edges() {
267 let (graph, ids) = build(3, &[]);
268 assert_eq!(topological_sort(&graph, &ids).unwrap().len(), 3);
269 }
270}