Skip to main content

goud_engine/assets/dependency/
graph.rs

1//! Dependency graph for tracking relationships between assets.
2//!
3//! The graph maintains both forward edges (what an asset depends on) and
4//! reverse edges (what depends on an asset) to support cascade reloading.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::fmt;
8
9// =============================================================================
10// CycleError
11// =============================================================================
12
13/// Error returned when adding a dependency would create a cycle.
14#[derive(Debug, Clone)]
15pub struct CycleError {
16    /// The asset that would create the cycle.
17    pub from: String,
18    /// The dependency target that would close the cycle.
19    pub to: String,
20}
21
22impl fmt::Display for CycleError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(
25            f,
26            "adding dependency '{}' -> '{}' would create a cycle",
27            self.from, self.to
28        )
29    }
30}
31
32impl std::error::Error for CycleError {}
33
34// =============================================================================
35// DependencyGraph
36// =============================================================================
37
38/// Tracks dependency relationships between assets.
39///
40/// Maintains two maps:
41/// - `dependencies`: forward edges (asset -> what it depends on)
42/// - `dependents`: reverse edges (asset -> who depends on it)
43///
44/// Supports cycle detection when adding edges and BFS cascade ordering
45/// for reload propagation.
46///
47/// # Example
48///
49/// ```
50/// use goud_engine::assets::dependency::DependencyGraph;
51///
52/// let mut graph = DependencyGraph::new();
53/// graph.add_dependency("shader.glsl", "common.glsl").unwrap();
54/// graph.add_dependency("material.json", "shader.glsl").unwrap();
55///
56/// // When common.glsl changes, reload shader.glsl then material.json
57/// let order = graph.get_cascade_order("common.glsl");
58/// assert_eq!(order, vec!["shader.glsl", "material.json"]);
59/// ```
60pub struct DependencyGraph {
61    /// Forward edges: asset -> set of assets it depends on.
62    dependencies: HashMap<String, HashSet<String>>,
63    /// Reverse edges: asset -> set of assets that depend on it.
64    dependents: HashMap<String, HashSet<String>>,
65}
66
67impl DependencyGraph {
68    /// Creates a new empty dependency graph.
69    pub fn new() -> Self {
70        Self {
71            dependencies: HashMap::new(),
72            dependents: HashMap::new(),
73        }
74    }
75
76    /// Adds a dependency: `asset` depends on `depends_on`.
77    ///
78    /// Returns `Err(CycleError)` if adding this edge would create a cycle.
79    ///
80    /// # Arguments
81    ///
82    /// * `asset` - The asset that has the dependency
83    /// * `depends_on` - The asset being depended upon
84    pub fn add_dependency(&mut self, asset: &str, depends_on: &str) -> Result<(), CycleError> {
85        // Self-dependency is a trivial cycle
86        if asset == depends_on {
87            return Err(CycleError {
88                from: asset.to_string(),
89                to: depends_on.to_string(),
90            });
91        }
92
93        // Check if adding this edge would create a cycle:
94        // A cycle exists if `depends_on` can already reach `asset` through
95        // the existing forward edges.
96        if self.can_reach(depends_on, asset) {
97            return Err(CycleError {
98                from: asset.to_string(),
99                to: depends_on.to_string(),
100            });
101        }
102
103        // Add forward edge
104        self.dependencies
105            .entry(asset.to_string())
106            .or_default()
107            .insert(depends_on.to_string());
108
109        // Add reverse edge
110        self.dependents
111            .entry(depends_on.to_string())
112            .or_default()
113            .insert(asset.to_string());
114
115        Ok(())
116    }
117
118    /// Returns true if `from` can reach `to` via forward dependency edges.
119    ///
120    /// Uses BFS to traverse the dependency graph.
121    fn can_reach(&self, from: &str, to: &str) -> bool {
122        let mut visited = HashSet::new();
123        let mut queue = VecDeque::new();
124        queue.push_back(from.to_string());
125
126        while let Some(current) = queue.pop_front() {
127            if current == to {
128                return true;
129            }
130            if !visited.insert(current.clone()) {
131                continue;
132            }
133            if let Some(deps) = self.dependencies.get(&current) {
134                for dep in deps {
135                    if !visited.contains(dep) {
136                        queue.push_back(dep.clone());
137                    }
138                }
139            }
140        }
141
142        false
143    }
144
145    /// Returns the cascade reload order for a changed asset.
146    ///
147    /// Performs a BFS over reverse edges (dependents) to produce a
148    /// topological ordering: assets closer to the changed asset are
149    /// reloaded first.
150    ///
151    /// The changed asset itself is NOT included in the output.
152    ///
153    /// # Arguments
154    ///
155    /// * `changed_path` - The path of the asset that changed
156    pub fn get_cascade_order(&self, changed_path: &str) -> Vec<String> {
157        let mut result = Vec::new();
158        let mut visited = HashSet::new();
159        let mut queue = VecDeque::new();
160
161        visited.insert(changed_path.to_string());
162
163        // Seed with direct dependents
164        if let Some(deps) = self.dependents.get(changed_path) {
165            for dep in deps {
166                if visited.insert(dep.clone()) {
167                    queue.push_back(dep.clone());
168                }
169            }
170        }
171
172        // BFS through dependents
173        while let Some(current) = queue.pop_front() {
174            result.push(current.clone());
175            if let Some(deps) = self.dependents.get(&current) {
176                for dep in deps {
177                    if visited.insert(dep.clone()) {
178                        queue.push_back(dep.clone());
179                    }
180                }
181            }
182        }
183
184        result
185    }
186
187    /// Removes an asset and all its edges from the graph.
188    ///
189    /// Cleans up both forward and reverse edges so no dangling
190    /// references remain.
191    pub fn remove_asset(&mut self, path: &str) {
192        // Remove forward edges: for each dependency of this asset,
193        // remove this asset from their dependents set.
194        if let Some(deps) = self.dependencies.remove(path) {
195            for dep in &deps {
196                if let Some(rev) = self.dependents.get_mut(dep) {
197                    rev.remove(path);
198                    if rev.is_empty() {
199                        self.dependents.remove(dep);
200                    }
201                }
202            }
203        }
204
205        // Remove reverse edges: for each dependent of this asset,
206        // remove this asset from their dependencies set.
207        if let Some(rev_deps) = self.dependents.remove(path) {
208            for rev in &rev_deps {
209                if let Some(fwd) = self.dependencies.get_mut(rev) {
210                    fwd.remove(path);
211                    if fwd.is_empty() {
212                        self.dependencies.remove(rev);
213                    }
214                }
215            }
216        }
217    }
218
219    /// Returns the set of assets that `asset` directly depends on.
220    pub fn get_dependencies(&self, asset: &str) -> Option<&HashSet<String>> {
221        self.dependencies.get(asset)
222    }
223
224    /// Returns the set of assets that directly depend on `asset`.
225    pub fn get_dependents(&self, asset: &str) -> Option<&HashSet<String>> {
226        self.dependents.get(asset)
227    }
228
229    /// Returns true if the graph contains any edges for the given asset.
230    pub fn contains(&self, asset: &str) -> bool {
231        self.dependencies.contains_key(asset) || self.dependents.contains_key(asset)
232    }
233
234    /// Returns the total number of assets tracked in the graph.
235    pub fn asset_count(&self) -> usize {
236        let mut all: HashSet<&str> = HashSet::new();
237        for key in self.dependencies.keys() {
238            all.insert(key.as_str());
239        }
240        for key in self.dependents.keys() {
241            all.insert(key.as_str());
242        }
243        all.len()
244    }
245
246    /// Clears all dependency relationships.
247    pub fn clear(&mut self) {
248        self.dependencies.clear();
249        self.dependents.clear();
250    }
251}
252
253impl Default for DependencyGraph {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259impl fmt::Debug for DependencyGraph {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.debug_struct("DependencyGraph")
262            .field("assets", &self.asset_count())
263            .field("forward_edges", &self.dependencies.len())
264            .field("reverse_edges", &self.dependents.len())
265            .finish()
266    }
267}