Skip to main content

fraiseql_core/federation/
dependency_graph.rs

1//! Dependency graph construction and cycle detection for federation directives
2//!
3//! Builds a directed graph from @requires directives and provides cycle
4//! detection and topological sort capabilities.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7
8use crate::federation::types::{FederationMetadata, FieldPathSelection};
9
10/// Dependency graph node representing a field with @requires directives
11#[derive(Debug, Clone)]
12struct DependencyNode {
13    /// Node ID: "TypeName.fieldName"
14    id:       String,
15    /// Fields this node requires
16    requires: Vec<FieldPathSelection>,
17}
18
19/// Directed edge in the dependency graph
20#[derive(Debug, Clone)]
21struct DependencyEdge {
22    /// Source node (from)
23    from: String,
24    /// Target node (to)
25    to:   String,
26}
27
28/// Dependency graph for federation @requires directives
29///
30/// Represents dependencies between fields based on @requires directives.
31/// Used to detect cycles and determine resolution order.
32pub struct DependencyGraph {
33    /// All nodes in the graph
34    nodes: HashMap<String, DependencyNode>,
35    /// All edges in the graph
36    edges: Vec<DependencyEdge>,
37}
38
39impl DependencyGraph {
40    /// Build a dependency graph from federation metadata
41    ///
42    /// Scans all types and fields for @requires directives and constructs
43    /// a directed graph where edges represent dependencies.
44    ///
45    /// # Errors
46    ///
47    /// Returns error if graph construction fails (should not happen in normal operation).
48    pub fn build(metadata: &FederationMetadata) -> Result<Self, String> {
49        let mut nodes: HashMap<String, DependencyNode> = HashMap::new();
50        let mut edges = Vec::new();
51
52        // Step 1: Build nodes for all fields with @requires directives
53        for federated_type in &metadata.types {
54            for (field_name, directives) in &federated_type.field_directives {
55                // Only create nodes for fields that have @requires directives
56                if !directives.requires.is_empty() {
57                    let node_id = format!("{}.{}", federated_type.name, field_name);
58
59                    nodes.insert(
60                        node_id.clone(),
61                        DependencyNode {
62                            id:       node_id,
63                            requires: directives.requires.clone(),
64                        },
65                    );
66                }
67            }
68        }
69
70        // Step 2: Build edges based on @requires dependencies
71        // For each node, create edges to the fields it requires
72        for node in nodes.values() {
73            for required in &node.requires {
74                // The target is "TypeName.fieldName" for the required field
75                let target_id = format!("{}.{}", required.typename, required.path.join("."));
76
77                edges.push(DependencyEdge {
78                    from: node.id.clone(),
79                    to:   target_id,
80                });
81            }
82        }
83
84        Ok(DependencyGraph { nodes, edges })
85    }
86
87    /// Get the number of nodes in the graph
88    pub fn node_count(&self) -> usize {
89        self.nodes.len()
90    }
91
92    /// Check if a node exists in the graph
93    pub fn has_node(&self, node_id: &str) -> bool {
94        self.nodes.contains_key(node_id)
95    }
96
97    /// Get all edges from a given node
98    fn edges_from(&self, node_id: &str) -> Vec<&DependencyEdge> {
99        self.edges.iter().filter(|e| e.from == node_id).collect()
100    }
101
102    /// Detect cycles in the dependency graph using DFS
103    ///
104    /// Returns a vector of cycles, where each cycle is a vector of node IDs
105    /// that form a circular dependency.
106    pub fn detect_cycles(&self) -> Vec<Vec<String>> {
107        let mut visited = HashSet::new();
108        let mut rec_stack = HashSet::new();
109        let mut cycles = Vec::new();
110
111        for node_id in self.nodes.keys() {
112            if !visited.contains(node_id) {
113                self.dfs_cycle_detection(
114                    node_id,
115                    &mut visited,
116                    &mut rec_stack,
117                    &mut cycles,
118                    &mut vec![],
119                );
120            }
121        }
122
123        cycles
124    }
125
126    /// DFS helper for cycle detection using recursion stack tracking
127    fn dfs_cycle_detection(
128        &self,
129        node_id: &str,
130        visited: &mut HashSet<String>,
131        rec_stack: &mut HashSet<String>,
132        cycles: &mut Vec<Vec<String>>,
133        path: &mut Vec<String>,
134    ) {
135        visited.insert(node_id.to_string());
136        rec_stack.insert(node_id.to_string());
137        path.push(node_id.to_string());
138
139        // Visit all neighbors (dependencies)
140        for edge in self.edges_from(node_id) {
141            let next = &edge.to;
142
143            if !visited.contains(next) {
144                self.dfs_cycle_detection(next, visited, rec_stack, cycles, path);
145            } else if rec_stack.contains(next) {
146                // Found a back edge - extract cycle from path
147                if let Some(cycle_start) = path.iter().position(|x| x == next) {
148                    let cycle: Vec<String> = path[cycle_start..].to_vec();
149                    cycles.push(cycle);
150                }
151            }
152        }
153
154        rec_stack.remove(node_id);
155        path.pop();
156    }
157
158    /// Perform topological sort using Kahn's algorithm
159    ///
160    /// # Errors
161    ///
162    /// Returns error if cycles are detected in the graph.
163    pub fn topological_sort(&self) -> Result<Vec<String>, String> {
164        // First check for cycles
165        let cycles = self.detect_cycles();
166        if !cycles.is_empty() {
167            return Err(format!("Circular dependencies detected: {:?}", cycles));
168        }
169
170        // Compute in-degree for each node
171        let mut in_degree: HashMap<String, usize> = HashMap::new();
172
173        // Initialize in-degree to 0
174        for node_id in self.nodes.keys() {
175            in_degree.insert(node_id.clone(), 0);
176        }
177
178        // Count incoming edges (only for nodes that exist in the graph)
179        for edge in &self.edges {
180            if self.nodes.contains_key(&edge.to) {
181                *in_degree.get_mut(&edge.to).unwrap() += 1;
182            }
183        }
184
185        // Find nodes with no incoming edges
186        let mut queue: VecDeque<String> = in_degree
187            .iter()
188            .filter(|(_, degree)| **degree == 0)
189            .map(|(id, _)| id.clone())
190            .collect();
191
192        let mut result = Vec::new();
193
194        // Process nodes in topological order
195        while let Some(node_id) = queue.pop_front() {
196            result.push(node_id.clone());
197
198            // For each edge from this node
199            for edge in self.edges_from(&node_id) {
200                // Only process if target is in the graph
201                if let Some(target_degree) = in_degree.get_mut(&edge.to) {
202                    *target_degree -= 1;
203
204                    // If target now has in-degree 0, add to queue
205                    if *target_degree == 0 {
206                        queue.push_back(edge.to.clone());
207                    }
208                }
209            }
210        }
211
212        Ok(result)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_dependency_node_creation() {
222        let node = DependencyNode {
223            id:       "User.orders".to_string(),
224            requires: vec![FieldPathSelection {
225                path:     vec!["email".to_string()],
226                typename: "User".to_string(),
227            }],
228        };
229
230        assert_eq!(node.id, "User.orders");
231        assert_eq!(node.requires.len(), 1);
232    }
233
234    #[test]
235    fn test_empty_graph() {
236        let metadata = FederationMetadata {
237            enabled: true,
238            version: "v2".to_string(),
239            types:   vec![],
240        };
241
242        let graph = DependencyGraph::build(&metadata).unwrap();
243        assert_eq!(graph.node_count(), 0);
244        assert!(graph.detect_cycles().is_empty());
245    }
246}