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(¤t) {
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(¤t) {
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}