mdmodels_core/tree.rs
1/*
2 * Copyright (c) 2025 Jan Range
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 *
22 */
23
24use std::collections::{HashMap, HashSet};
25
26use crate::{datamodel::DataModel, object::Object};
27use petgraph::{
28 graph::{DiGraph, NodeIndex},
29 Direction,
30};
31
32/// Creates a directed graph representing dependencies between objects in a data model.
33///
34/// This function builds a directed graph where nodes represent objects and edges represent
35/// dependencies between them. A dependency exists when one object's attributes reference
36/// another object as its data type. This function includes all objects in the model.
37///
38/// # Arguments
39///
40/// * `model` - The data model containing objects and their relationships
41///
42/// # Returns
43///
44/// A directed graph (DiGraph) where:
45/// - Nodes contain the full Object structs
46/// - Edges represent dependencies between objects (empty unit type)
47/// - All objects in the model are included in the graph
48pub fn dependency_graph(model: &DataModel) -> DiGraph<Object, ()> {
49 let mut graph = DiGraph::new();
50 let adjacency_map = extract_adjacency_map(&model.objects);
51
52 // Create nodes for each object
53 let nodes: HashMap<String, NodeIndex> = model
54 .objects
55 .iter()
56 .map(|o| (o.name.clone(), graph.add_node(o.clone())))
57 .collect();
58
59 // Add edges based on adjacency map
60 for (object_name, dependencies) in adjacency_map {
61 for dependency in dependencies {
62 graph.add_edge(nodes[&dependency], nodes[&object_name], ());
63 }
64 }
65
66 graph
67}
68
69/// Creates a directed graph representing dependencies between objects in a data model.
70///
71/// This function builds a directed graph where nodes represent objects and edges represent
72/// dependencies between them. A dependency exists when one object's attributes reference
73/// another object as its data type. The graph is built recursively starting from the root
74/// object and includes only objects reachable from the root.
75///
76/// # Arguments
77///
78/// * `model` - The data model containing objects and their relationships
79/// * `root` - The name of the root object to start building the graph from
80///
81/// # Returns
82///
83/// A directed graph (DiGraph) where:
84/// - Nodes contain the full Object structs
85/// - Edges represent dependencies between objects (empty unit type)
86/// - Only objects reachable from the root are included in the graph
87///
88/// # Errors
89///
90/// Returns an error if the root object is not found in the model or if there are
91/// issues traversing the object dependencies.
92pub fn object_graph(model: &DataModel, root: &str) -> Result<DiGraph<Object, ()>, String> {
93 let mut graph = DiGraph::new();
94 let adjacency_map = extract_adjacency_map(&model.objects);
95
96 // Create nodes for each object
97 let nodes: HashMap<String, NodeIndex> = model
98 .objects
99 .iter()
100 .map(|o| (o.name.clone(), graph.add_node(o.clone())))
101 .collect();
102
103 // Verify root exists
104 if !nodes.contains_key(root) {
105 return Err(format!("Object '{}' not found in model", root));
106 }
107
108 // Track visited nodes to avoid infinite recursion
109 let mut visited = HashSet::new();
110
111 // Build the graph recursively starting from root
112 object_graph_helper(&nodes, &adjacency_map, &mut graph, root, &mut visited)?;
113
114 Ok(graph)
115}
116
117/// Recursive helper function to build the object dependency graph.
118///
119/// This function traverses the object dependency tree starting from the given root,
120/// adding edges to the graph for each dependency relationship. It uses a visited set
121/// to prevent infinite recursion in case of circular dependencies.
122///
123/// # Arguments
124///
125/// * `nodes` - Mapping of object names to their node indices in the graph
126/// * `adjacency_map` - Map of object names to their list of dependencies
127/// * `graph` - The graph being built (modified in place)
128/// * `root` - The current object being processed
129/// * `visited` - Set of object names that have already been processed
130fn object_graph_helper(
131 nodes: &HashMap<String, NodeIndex>,
132 adjacency_map: &HashMap<String, Vec<String>>,
133 graph: &mut DiGraph<Object, ()>,
134 root: &str,
135 visited: &mut HashSet<String>,
136) -> Result<(), String> {
137 // If we've already processed this node, return early
138 if visited.contains(root) {
139 return Ok(());
140 }
141
142 // Mark this node as visited
143 visited.insert(root.to_string());
144
145 // Get the dependencies for this object
146 let dependencies = adjacency_map.get(root).map(|v| v.as_slice()).unwrap_or(&[]);
147
148 // Process each dependency
149 for dependency in dependencies {
150 // Verify the dependency exists
151 if !nodes.contains_key(dependency) {
152 return Err(format!(
153 "Dependency '{}' referenced by '{}' not found in model",
154 dependency, root
155 ));
156 }
157
158 // Recursively process the dependency first
159 object_graph_helper(nodes, adjacency_map, graph, dependency, visited)?;
160
161 // Add edge from root to dependency
162 graph.add_edge(nodes[root], nodes[dependency], ());
163 }
164
165 Ok(())
166}
167
168/// Returns a topologically sorted list of node names from the dependency graph.
169///
170/// This function performs a depth-first traversal of the graph to generate a topological
171/// ordering of the nodes. The ordering ensures that for any directed edge u->v in the graph,
172/// node u comes before node v in the ordering.
173///
174/// # Arguments
175///
176/// * `graph` - The directed graph to traverse, with Object node values
177///
178/// # Returns
179///
180/// A Vec<String> containing the object names in topological order
181pub fn get_topological_order(graph: &DiGraph<Object, ()>) -> Vec<String> {
182 let mut visited = HashSet::new();
183 let mut stack = Vec::new();
184
185 for node in graph.node_indices() {
186 visit(graph, node, &mut visited, &mut stack);
187 }
188
189 stack
190}
191
192/// Helper function for depth-first post-order traversal during topological sorting.
193///
194/// This function performs a recursive depth-first traversal starting from the given node,
195/// marking visited nodes and building up the topologically sorted stack. It tracks visited
196/// nodes by their names to ensure each object is processed only once.
197///
198/// # Arguments
199///
200/// * `graph` - The directed graph being traversed
201/// * `node` - The current node being visited
202/// * `visited` - Set of already visited object names
203/// * `stack` - Vector storing the topologically sorted object names
204fn visit(
205 graph: &DiGraph<Object, ()>,
206 node: NodeIndex,
207 visited: &mut HashSet<String>,
208 stack: &mut Vec<String>,
209) {
210 let object_name = &graph[node].name;
211
212 if visited.contains(object_name) {
213 return;
214 }
215 visited.insert(object_name.clone());
216
217 // Visit all dependencies first
218 for neighbor in graph.neighbors_directed(node, Direction::Outgoing) {
219 visit(graph, neighbor, visited, stack);
220 }
221
222 // Add to stack only after visiting all dependencies
223 stack.push(object_name.clone());
224}
225
226/// Extracts an adjacency map from a list of objects, representing their type dependencies.
227///
228/// This function analyzes the data types of attributes in each object and creates a mapping
229/// of attribute names to their dependent object types. It only includes attributes that have
230/// data types referencing other objects in the model.
231///
232/// # Arguments
233///
234/// * `objects` - A slice of Object structs to analyze
235///
236/// # Returns
237///
238/// A HashMap where:
239/// - Keys are attribute names (String)
240/// - Values are vectors of object names (Vec<String>) that the attribute depends on
241fn extract_adjacency_map(objects: &[Object]) -> HashMap<String, Vec<String>> {
242 // Create a HashSet of object names for faster lookups
243 let object_names: HashSet<_> = objects.iter().map(|o| &o.name).collect();
244
245 // Pre-allocate with estimated capacity
246 let mut dtype_map = HashMap::with_capacity(objects.len());
247
248 for object in objects {
249 let mut deps = Vec::new();
250 for attribute in &object.attributes {
251 deps.extend(
252 attribute
253 .dtypes
254 .iter()
255 .filter(|d| object_names.contains(d))
256 .cloned(),
257 );
258 }
259
260 dtype_map.insert(object.name.clone(), deps);
261 }
262 dtype_map
263}