1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
use crate::domain::error::Result;
use crate::domain::resource::Resource;
use petgraph::algo::toposort;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, warn};
/// A representation of the system configuration as a directed acyclic graph (DAG) of resources.
///
/// `Catalog` manages the relationships and dependency order between different system components
/// (files, directories, packages, executions) to ensure safe and predictable application.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Catalog {
/// The target hostname/node name this catalog is intended for.
pub node_name: String,
/// The deployment environment (e.g., `"production"`, `"staging"`).
pub environment: String,
/// The directed graph containing the resources.
pub graph: DiGraph<Resource, ()>,
/// UNIX timestamp when the catalog was compiled.
pub timestamp: i64,
/// Helper map to find node index by resource ID.
/// Excluded from serialization and rebuilt dynamically on load.
#[serde(skip)]
id_map: HashMap<String, NodeIndex>,
}
impl Catalog {
/// Creates a new, empty `Catalog` for the given node and environment.
pub fn new(node_name: String, environment: String) -> Self {
Self {
node_name,
environment,
graph: DiGraph::new(),
timestamp: chrono::Utc::now().timestamp(),
id_map: HashMap::new(),
}
}
/// Adds a new resource to the catalog graph.
pub fn add_resource(&mut self, resource: Resource) {
let id = resource.id().to_string();
let idx = self.graph.add_node(resource);
self.id_map.insert(id, idx);
}
/// Explicitly declares a dependency between two resources.
///
/// The resource with ID `to_id` will depend on the resource with ID `from_id`
/// (i.e. `from_id` must execute BEFORE `to_id`).
pub fn add_dependency(&mut self, from_id: &str, to_id: &str) -> Result<()> {
let from_idx = *self
.id_map
.get(from_id)
.ok_or_else(|| anyhow::anyhow!("Resource not found: {}", from_id))?;
let to_idx = *self
.id_map
.get(to_id)
.ok_or_else(|| anyhow::anyhow!("Resource not found: {}", to_id))?;
self.graph.add_edge(from_idx, to_idx, ());
// Sync with resource struct to ensure persistence/rebuild works
if let Some(resource) = self.graph.node_weight_mut(to_idx) {
resource.add_dependency(from_id.to_string());
}
Ok(())
}
/// Rebuilds the internal lookup map after deserializing the catalog.
pub fn rebuild_id_map(&mut self) {
self.id_map.clear();
for idx in self.graph.node_indices() {
let id = self.graph[idx].id().to_string();
self.id_map.insert(id, idx);
}
}
/// Retrieves a reference to a resource by its unique identifier.
pub fn get_resource(&self, id: &str) -> Option<&Resource> {
self.id_map.get(id).map(|&idx| &self.graph[idx])
}
/// Populates directed graph edges based on embedded resource dependencies.
///
/// Iterates over all resources in the catalog and establishes graph connections
/// based on the IDs returned by `dependencies()`. Missing dependencies result in warnings
/// but do not halt the process.
pub fn build_edges(&mut self) {
// Clear existing edges to avoid duplicates.
// This makes the operation idempotent and safe to call multiple times,
// e.g. after adding more resources.
self.graph.clear_edges();
let mut edges_to_add = Vec::new();
let mut missing_deps_count = 0;
debug!("Building graph edges from resource dependencies...");
for idx in self.graph.node_indices() {
let resource = &self.graph[idx];
let resource_id = resource.id().to_string();
for dep_id in resource.dependencies() {
if let Some(&dep_idx) = self.id_map.get(dep_id) {
// Dependency: resource -> dep (resource depends on dep)
// Edge direction: dep -> resource
// topological sort usually gives order where dep comes BEFORE resource.
// If edge is dep -> resource, then dep comes first.
edges_to_add.push((dep_idx, idx));
} else {
// Log warning for missing dependency.
// This often happens if dependencies are conditional or external.
warn!(
resource_id = %resource_id,
missing_dependency = %dep_id,
"Skipping missing dependency for resource"
);
missing_deps_count += 1;
}
}
}
let edge_count = edges_to_add.len();
for (from, to) in edges_to_add {
self.graph.add_edge(from, to, ());
}
debug!(
edges_added = edge_count,
missing_dependencies = missing_deps_count,
"Graph edges built successfully"
);
}
/// Returns a flat vector of all resources defined in this catalog.
pub fn resources(&self) -> Vec<Resource> {
self.graph
.node_indices()
.map(|idx| self.graph[idx].clone())
.collect()
}
/// Sorts the resources topologically, producing a valid linear execution order.
///
/// Returns an error if a circular dependency is detected.
pub fn topological_sort(&self) -> Result<Vec<Resource>> {
match toposort(&self.graph, None) {
Ok(indices) => Ok(indices
.into_iter()
.map(|idx| self.graph[idx].clone())
.collect()),
Err(cycle) => {
let resource_id = self.graph[cycle.node_id()].id();
Err(anyhow::anyhow!(
"Circular dependency detected involving: {}",
resource_id
))
}
}
}
/// Returns a sub-catalog (subgraph branch) containing the specified root resource and all its dependencies.
pub fn get_branch(&self, root_id: &str) -> Result<Catalog> {
let root_idx = *self
.id_map
.get(root_id)
.ok_or_else(|| anyhow::anyhow!("Resource not found: {}", root_id))?;
let mut branch = Catalog::new(self.node_name.clone(), self.environment.clone());
let mut visited = HashMap::new();
self.copy_recursive(root_idx, &mut branch, &mut visited)?;
Ok(branch)
}
fn copy_recursive(
&self,
idx: NodeIndex,
target: &mut Catalog,
visited: &mut HashMap<NodeIndex, NodeIndex>,
) -> Result<NodeIndex> {
if let Some(&new_idx) = visited.get(&idx) {
return Ok(new_idx);
}
let resource = &self.graph[idx];
target.add_resource(resource.clone());
let new_idx = *target.id_map.get(resource.id()).ok_or_else(|| {
anyhow::anyhow!(
"Invariant failed: resource {} not found after addition",
resource.id()
)
})?;
visited.insert(idx, new_idx);
// Copy edges (dependencies)
for edge in self.graph.edges(idx) {
let target_node_idx = edge.target();
let new_target_idx = self.copy_recursive(target_node_idx, target, visited)?;
target.graph.add_edge(new_idx, new_target_idx, ());
}
Ok(new_idx)
}
}