composable_runtime/composition/
graph.rs1use anyhow::Result;
2use petgraph::graph::{DiGraph, NodeIndex};
3use petgraph::visit::EdgeRef;
4use std::collections::HashMap;
5use std::ops::{Index, IndexMut};
6use std::path::PathBuf;
7
8use crate::config::loaders::{TomlLoader, WasmLoader};
9use crate::config::processor::ConfigProcessor;
10use crate::types::{CapabilityDefinition, ComponentDefinition};
11
12pub struct ComponentGraph {
15 graph: DiGraph<Node, Edge>,
16 node_map: HashMap<String, NodeIndex>,
17}
18
19impl ComponentGraph {
20 pub fn builder() -> GraphBuilder {
22 GraphBuilder::new()
23 }
24
25 pub(crate) fn build(
28 component_definitions: &[ComponentDefinition],
29 capability_definitions: &[CapabilityDefinition],
30 ) -> Result<Self> {
31 let mut graph = DiGraph::<Node, Edge>::new();
32 let mut node_map = HashMap::<String, NodeIndex>::new();
33
34 for definition in capability_definitions {
35 let index = graph.add_node(Node::Capability(definition.clone()));
36 node_map.insert(definition.name.clone(), index);
37 }
38
39 let interceptor_names: std::collections::HashSet<&str> = component_definitions
42 .iter()
43 .flat_map(|d| d.interceptors.iter().map(|s| s.as_str()))
44 .collect();
45 let imported_names: std::collections::HashSet<&str> = component_definitions
46 .iter()
47 .flat_map(|d| d.imports.iter().map(|s| s.as_str()))
48 .collect();
49
50 for definition in component_definitions {
51 let is_template_only = interceptor_names.contains(definition.name.as_str())
52 && !imported_names.contains(definition.name.as_str());
53 if is_template_only {
54 continue;
55 }
56 let index = graph.add_node(Node::Component(definition.clone()));
57 node_map.insert(definition.name.clone(), index);
58 }
59
60 let mut interceptor_clones = std::collections::HashSet::<NodeIndex>::new();
78
79 for definition in component_definitions {
80 if definition.interceptors.is_empty() {
81 continue;
82 }
83
84 let original_name = &definition.name;
85 let internal_name = format!("_{original_name}$0");
86
87 let component_index = node_map.remove(original_name).unwrap();
89 if let Node::Component(ref mut def) = graph[component_index] {
90 def.name = internal_name.clone();
91 }
92 node_map.insert(internal_name, component_index);
93
94 let mut current = component_index;
95 let interceptor_count = definition.interceptors.len();
96
97 for (position, interceptor_name) in definition.interceptors.iter().rev().enumerate() {
98 let interceptor_def = component_definitions
99 .iter()
100 .find(|d| d.name == *interceptor_name)
101 .ok_or_else(|| {
102 anyhow::anyhow!(
103 "Component '{}' references interceptor '{}', which is not defined.",
104 definition.name,
105 interceptor_name,
106 )
107 })?;
108
109 let is_outermost = position == interceptor_count - 1;
110 let synthetic_name = if is_outermost {
111 original_name.clone()
112 } else {
113 format!("_{original_name}${}", position + 1)
114 };
115
116 let mut cloned_def = interceptor_def.clone();
117 cloned_def.name = synthetic_name.clone();
118
119 let cloned_index = graph.add_node(Node::Component(cloned_def));
120 node_map.insert(synthetic_name, cloned_index);
121 interceptor_clones.insert(cloned_index);
122
123 graph.update_edge(current, cloned_index, Edge::Interceptor(position as i32));
124 current = cloned_index;
125 }
126 }
127
128 for definition in component_definitions {
131 let lookup_name = if definition.interceptors.is_empty() {
132 definition.name.clone()
133 } else {
134 format!("_{}$0", definition.name)
135 };
136
137 let Some(importer_index) = node_map.get(&lookup_name).copied() else {
138 continue; };
140
141 for exporter_name in &definition.imports {
142 if let Some(exporter_index) = node_map.get(exporter_name).copied() {
143 graph.update_edge(exporter_index, importer_index, Edge::Dependency);
144 } else {
145 tracing::warn!(
146 "Component '{}' imports '{}', which is not defined.",
147 definition.name,
148 exporter_name
149 );
150 }
151 }
152 }
153
154 for clone_index in &interceptor_clones {
156 let Node::Component(def) = &graph[*clone_index] else {
157 continue;
158 };
159 let clone_name = def.name.clone();
160 let imports = def.imports.clone();
161 for exporter_name in &imports {
162 if let Some(exporter_index) = node_map.get(exporter_name).copied() {
163 graph.update_edge(exporter_index, *clone_index, Edge::Dependency);
164 } else {
165 tracing::warn!(
166 "Interceptor '{}' imports '{}', which is not defined.",
167 clone_name,
168 exporter_name
169 );
170 }
171 }
172 }
173
174 if let Err(cycle) = petgraph::algo::toposort(&graph, None) {
176 let node_name = match &graph[cycle.node_id()] {
177 Node::Component(def) => &def.name,
178 Node::Capability(def) => &def.name,
179 };
180 return Err(anyhow::anyhow!(
181 "Circular dependency detected involving '{node_name}'"
182 ));
183 }
184
185 Ok(Self { graph, node_map })
186 }
187
188 pub fn write_dot_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
190 let dot_content = self.dot();
191 std::fs::write(path, dot_content)
192 .map_err(|e| anyhow::anyhow!("Failed to write DOT file: {e}"))?;
193 Ok(())
194 }
195
196 pub fn nodes(&self) -> impl Iterator<Item = &petgraph::graph::Node<Node>> {
197 self.graph.raw_nodes().iter()
198 }
199
200 pub fn get_build_order(&self) -> Vec<NodeIndex> {
201 petgraph::algo::toposort(&self.graph, None).unwrap()
202 }
203
204 pub fn get_node_index(&self, name: &str) -> Option<NodeIndex> {
205 self.node_map.get(name).copied()
206 }
207
208 pub fn get_dependencies(&self, index: NodeIndex) -> impl Iterator<Item = (NodeIndex, &Edge)> {
209 self.graph
210 .edges_directed(index, petgraph::Direction::Incoming)
211 .map(|edge_ref| (edge_ref.source(), edge_ref.weight()))
212 }
213
214 fn dot(&self) -> String {
215 let mut output = String::from("digraph ComponentGraph {\n");
216 output.push_str(" rankdir=BT;\n");
217 output.push_str(" node [fontname=\"Arial\", fontsize=10];\n");
218 output.push_str(" edge [fontname=\"Arial\", fontsize=9];\n");
219
220 for node_index in self.graph.node_indices() {
221 let node = &self.graph[node_index];
222 let node_attrs = match node {
223 Node::Component(def) => {
224 let is_internal = def.name.starts_with('_');
225 let color = if is_internal { "yellow" } else { "lightblue" };
226 format!(
227 "[label=\"{}\", shape=box, fillcolor={color}, style=\"rounded,filled\"]",
228 def.name
229 )
230 }
231 Node::Capability(def) => {
232 format!(
233 "[label=\"{}\", shape=ellipse, fillcolor=orange, style=\"rounded,filled\"]",
234 def.name
235 )
236 }
237 };
238 output.push_str(&format!(" {} {};\n", node_index.index(), node_attrs));
239 }
240
241 for edge_ref in self.graph.edge_references() {
242 let edge_attrs = match edge_ref.weight() {
243 Edge::Dependency => "[color=blue, style=solid]".to_string(),
244 Edge::Interceptor(position) => {
245 format!("[color=red, style=dashed, label=\"interceptor: {position}\"]")
246 }
247 };
248 output.push_str(&format!(
249 " {} -> {} {};\n",
250 edge_ref.source().index(),
251 edge_ref.target().index(),
252 edge_attrs
253 ));
254 }
255
256 output.push_str("}\n");
257 output
258 }
259}
260
261impl std::fmt::Debug for ComponentGraph {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 struct FlatNode<'a>(&'a Node);
264 impl<'a> std::fmt::Debug for FlatNode<'a> {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 match self.0 {
267 Node::Component(def) => std::fmt::Debug::fmt(def, f),
268 Node::Capability(def) => std::fmt::Debug::fmt(def, f),
269 }
270 }
271 }
272
273 let mut debug_struct = f.debug_struct("ComponentGraph");
274
275 let nodes: Vec<_> = self
276 .graph
277 .raw_nodes()
278 .iter()
279 .map(|n| FlatNode(&n.weight))
280 .collect();
281 debug_struct.field("nodes", &nodes);
282
283 let edges: Vec<String> = self
284 .graph
285 .edge_references()
286 .map(|edge| {
287 let source_node = &self.graph[edge.source()];
288 let target_node = &self.graph[edge.target()];
289 let source_name = match source_node {
290 Node::Component(def) => &def.name,
291 Node::Capability(def) => &def.name,
292 };
293 let target_name = match target_node {
294 Node::Component(def) => &def.name,
295 Node::Capability(def) => &def.name,
296 };
297 format!("{} -> {} ({:?})", source_name, target_name, edge.weight())
298 })
299 .collect();
300 debug_struct.field("edges", &edges);
301 debug_struct.finish()
302 }
303}
304
305impl Index<NodeIndex> for ComponentGraph {
306 type Output = Node;
307
308 fn index(&self, index: NodeIndex) -> &Self::Output {
309 &self.graph[index]
310 }
311}
312
313impl IndexMut<NodeIndex> for ComponentGraph {
314 fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output {
315 &mut self.graph[index]
316 }
317}
318
319#[derive(Debug, Clone)]
320pub enum Node {
321 Component(ComponentDefinition),
322 Capability(CapabilityDefinition),
323}
324
325#[derive(Debug, Clone)]
326#[allow(dead_code)]
327pub enum Edge {
328 Dependency,
329 Interceptor(i32), }
331
332pub struct GraphBuilder {
334 paths: Vec<PathBuf>,
335 loaders: Vec<Box<dyn crate::config::types::DefinitionLoader>>,
336 handlers: Vec<Box<dyn crate::config::types::ConfigHandler>>,
337 use_default_loaders: bool,
338}
339
340impl GraphBuilder {
341 fn new() -> Self {
342 Self {
343 paths: Vec::new(),
344 loaders: Vec::new(),
345 handlers: Vec::new(),
346 use_default_loaders: true,
347 }
348 }
349
350 pub fn from_path(mut self, path: impl Into<PathBuf>) -> Self {
352 self.paths.push(path.into());
353 self
354 }
355
356 pub fn from_paths(mut self, paths: &[PathBuf]) -> Self {
358 self.paths.extend_from_slice(paths);
359 self
360 }
361
362 pub fn add_loader(mut self, loader: Box<dyn crate::config::types::DefinitionLoader>) -> Self {
364 self.loaders.push(loader);
365 self
366 }
367
368 pub fn add_handler(mut self, handler: Box<dyn crate::config::types::ConfigHandler>) -> Self {
370 self.handlers.push(handler);
371 self
372 }
373
374 pub fn no_default_loaders(mut self) -> Self {
376 self.use_default_loaders = false;
377 self
378 }
379
380 pub fn build(self) -> Result<ComponentGraph> {
382 let mut processor = ConfigProcessor::new();
383
384 if self.use_default_loaders {
385 processor.add_loader(Box::new(TomlLoader::new()));
386 processor.add_loader(Box::new(WasmLoader::new()));
387 }
388 for loader in self.loaders {
389 processor.add_loader(loader);
390 }
391 for handler in self.handlers {
392 processor.add_handler(handler);
393 }
394
395 let (component_definitions, capability_definitions) = processor.process(&self.paths)?;
396 ComponentGraph::build(&component_definitions, &capability_definitions)
397 }
398}