Skip to main content

trellis_core/
graph.rs

1use crate::{
2    AuditState, DependencyList, DerivedNode, GraphError, GraphResult, InputNode, NodeHandle,
3    NodeId, NodeKind, NodeMeta, OutputKey, OutputMeta, Revision, ScopeId, ScopeMeta, Transaction,
4    TransactionId, TransactionOptions,
5    collection::{CollectionSpec, StoredCollection, StoredDiff},
6    derive::DerivedSpec,
7    input::{StoredInput, value_type},
8    output::OutputSpec,
9    resource::{ResourceKey, ResourcePlanner},
10};
11use std::collections::{BTreeMap, BTreeSet};
12
13/// Trellis graph skeleton with transactional input mutation.
14pub struct Graph<C = (), O = ()> {
15    pub(crate) next_node_id: u64,
16    pub(crate) next_scope_id: u64,
17    pub(crate) next_output_key: u64,
18    pub(crate) next_transaction_id: TransactionId,
19    pub(crate) revision: Revision,
20    pub(crate) nodes: BTreeMap<NodeId, NodeMeta>,
21    pub(crate) scopes: BTreeMap<ScopeId, ScopeMeta>,
22    pub(crate) input_values: BTreeMap<NodeId, Box<dyn StoredInput>>,
23    pub(crate) derived_specs: BTreeMap<NodeId, DerivedSpec<C, O>>,
24    pub(crate) derived_values: BTreeMap<NodeId, Box<dyn StoredInput>>,
25    pub(crate) collection_specs: BTreeMap<NodeId, CollectionSpec<C, O>>,
26    pub(crate) collection_values: BTreeMap<NodeId, Box<dyn StoredCollection>>,
27    pub(crate) previous_collection_values: BTreeMap<NodeId, Box<dyn StoredCollection>>,
28    pub(crate) collection_diffs: BTreeMap<NodeId, Box<dyn StoredDiff>>,
29    pub(crate) resource_planners: Vec<ResourcePlanner<C, O>>,
30    pub(crate) resource_owners: BTreeMap<ResourceKey, BTreeSet<ScopeId>>,
31    pub(crate) output_specs: BTreeMap<OutputKey, OutputSpec<C, O>>,
32    pub(crate) output_values: BTreeMap<OutputKey, O>,
33    pub(crate) outputs: BTreeMap<OutputKey, OutputMeta>,
34    pub(crate) audit: AuditState,
35    pub(crate) transaction_open: bool,
36}
37
38impl<C, O> Graph<C, O> {
39    /// Creates an empty graph.
40    pub fn new_with_command_type() -> Self {
41        Self {
42            next_node_id: 1,
43            next_scope_id: 1,
44            next_output_key: 1,
45            next_transaction_id: TransactionId::default(),
46            revision: Revision::default(),
47            nodes: BTreeMap::new(),
48            scopes: BTreeMap::new(),
49            input_values: BTreeMap::new(),
50            derived_specs: BTreeMap::new(),
51            derived_values: BTreeMap::new(),
52            collection_specs: BTreeMap::new(),
53            collection_values: BTreeMap::new(),
54            previous_collection_values: BTreeMap::new(),
55            collection_diffs: BTreeMap::new(),
56            resource_planners: Vec::new(),
57            resource_owners: BTreeMap::new(),
58            output_specs: BTreeMap::new(),
59            output_values: BTreeMap::new(),
60            outputs: BTreeMap::new(),
61            audit: AuditState::default(),
62            transaction_open: false,
63        }
64    }
65
66    /// Returns the graph revision.
67    pub fn revision(&self) -> Revision {
68        self.revision
69    }
70
71    /// Begins an input transaction with default options.
72    pub fn begin_transaction(&mut self) -> GraphResult<Transaction<'_, C, O>>
73    where
74        O: Clone + PartialEq,
75    {
76        self.begin_transaction_with_options(TransactionOptions::default())
77    }
78
79    /// Begins an input transaction with explicit options.
80    pub fn begin_transaction_with_options(
81        &mut self,
82        options: TransactionOptions,
83    ) -> GraphResult<Transaction<'_, C, O>>
84    where
85        O: Clone + PartialEq,
86    {
87        if self.transaction_open {
88            return Err(GraphError::NestedTransaction);
89        }
90
91        self.transaction_open = true;
92        let id = self.allocate_transaction_id();
93        Ok(Transaction::new(self, id, options))
94    }
95
96    pub(crate) fn create_scope_with_parent_direct(
97        &mut self,
98        id: ScopeId,
99        debug_name: impl Into<String>,
100        parent: Option<ScopeId>,
101    ) -> GraphResult<ScopeId> {
102        if let Some(parent) = parent {
103            let parent_meta = self.require_scope(parent)?;
104            if parent_meta.is_closed() {
105                return Err(GraphError::ScopeAlreadyClosed(parent));
106            }
107        }
108
109        self.scopes
110            .insert(id, ScopeMeta::new(id, debug_name, parent));
111        Ok(id)
112    }
113
114    pub(crate) fn input_direct<T>(
115        &mut self,
116        id: NodeId,
117        debug_name: impl Into<String>,
118    ) -> GraphResult<InputNode<T>>
119    where
120        T: Clone + PartialEq + Send + Sync + 'static,
121    {
122        let meta = NodeMeta::new(
123            id,
124            NodeKind::Input,
125            debug_name,
126            DependencyList::empty(),
127            self.revision,
128            Some(value_type::<T>()),
129        );
130        self.nodes.insert(id, meta);
131        Ok(InputNode::new(id))
132    }
133
134    pub(crate) fn derived_direct<T>(
135        &mut self,
136        id: NodeId,
137        debug_name: impl Into<String>,
138        dependencies: DependencyList,
139        derive: impl for<'ctx> Fn(&crate::DeriveContext<'ctx, C, O>) -> Result<T, crate::DeriveError>
140        + Send
141        + Sync
142        + 'static,
143    ) -> GraphResult<DerivedNode<T>>
144    where
145        T: Clone + PartialEq + Send + Sync + 'static,
146    {
147        self.validate_dependencies(id, &dependencies)?;
148        self.reject_collection_dependencies(&dependencies)?;
149        let meta = NodeMeta::new(
150            id,
151            NodeKind::Derived,
152            debug_name,
153            dependencies,
154            self.revision,
155            Some(value_type::<T>()),
156        );
157        self.nodes.insert(id, meta);
158        self.derived_specs
159            .insert(id, DerivedSpec::<C, O>::new(derive));
160        Ok(DerivedNode::new(id))
161    }
162
163    pub(crate) fn attach_node_to_scope_direct(
164        &mut self,
165        node_id: NodeId,
166        scope: ScopeId,
167    ) -> GraphResult<()> {
168        let scope_meta = self.require_scope(scope)?;
169        if scope_meta.is_closed() {
170            return Err(GraphError::ScopeAlreadyClosed(scope));
171        }
172
173        let node_meta = self
174            .nodes
175            .get_mut(&node_id)
176            .ok_or(GraphError::UnknownNode(node_id))?;
177
178        if node_meta.owning_scope().is_some() {
179            return Err(GraphError::NodeAlreadyAttached(node_id));
180        }
181
182        node_meta.attach_scope(scope);
183        Ok(())
184    }
185
186    /// Returns metadata for a node.
187    pub fn node_meta<H: NodeHandle>(&self, node: H) -> Option<&NodeMeta> {
188        self.nodes.get(&node.id())
189    }
190
191    /// Returns metadata for a node id.
192    pub fn node_meta_by_id(&self, id: NodeId) -> Option<&NodeMeta> {
193        self.nodes.get(&id)
194    }
195
196    /// Returns metadata for a scope.
197    pub fn scope_meta(&self, id: ScopeId) -> Option<&ScopeMeta> {
198        self.scopes.get(&id)
199    }
200
201    /// Returns metadata for a materialized output.
202    pub fn output_meta(&self, key: OutputKey) -> Option<&OutputMeta> {
203        self.outputs.get(&key)
204    }
205
206    /// Returns declared dependencies for a node.
207    pub fn dependencies<H: NodeHandle>(&self, node: H) -> Option<&DependencyList> {
208        self.node_meta(node).map(NodeMeta::dependencies)
209    }
210
211    /// Returns all node metadata in stable id order.
212    pub fn nodes(&self) -> impl Iterator<Item = &NodeMeta> {
213        self.nodes.values()
214    }
215
216    /// Returns all scope metadata in stable id order.
217    pub fn scopes(&self) -> impl Iterator<Item = &ScopeMeta> {
218        self.scopes.values()
219    }
220
221    pub(crate) fn allocate_node_id(&mut self) -> NodeId {
222        let id = NodeId::from_index(self.next_node_id);
223        self.next_node_id += 1;
224        id
225    }
226
227    pub(crate) fn allocate_scope_id(&mut self) -> ScopeId {
228        let id = ScopeId::from_index(self.next_scope_id);
229        self.next_scope_id += 1;
230        id
231    }
232
233    pub(crate) fn allocate_output_key(&mut self) -> OutputKey {
234        let key = OutputKey::from_index(self.next_output_key);
235        self.next_output_key += 1;
236        key
237    }
238
239    fn allocate_transaction_id(&mut self) -> TransactionId {
240        self.next_transaction_id = self.next_transaction_id.next();
241        self.next_transaction_id
242    }
243
244    pub(crate) fn require_scope(&self, id: ScopeId) -> GraphResult<&ScopeMeta> {
245        self.scopes.get(&id).ok_or(GraphError::UnknownScope(id))
246    }
247
248    pub(crate) fn validate_dependencies(
249        &self,
250        node_id: NodeId,
251        dependencies: &DependencyList,
252    ) -> GraphResult<()> {
253        for dependency in dependencies.as_slice() {
254            if *dependency == node_id {
255                return Err(GraphError::SelfDependency(node_id));
256            }
257            if !self.nodes.contains_key(dependency) {
258                return Err(GraphError::UnknownNode(*dependency));
259            }
260            if self.depends_on(*dependency, node_id) {
261                return Err(GraphError::CycleDetected(node_id));
262            }
263        }
264        Ok(())
265    }
266
267    pub(crate) fn validate_output_dependencies(
268        &self,
269        dependencies: &DependencyList,
270    ) -> GraphResult<()> {
271        for dependency in dependencies.as_slice() {
272            if !self.nodes.contains_key(dependency) {
273                return Err(GraphError::UnknownNode(*dependency));
274            }
275        }
276        Ok(())
277    }
278
279    fn reject_collection_dependencies(&self, dependencies: &DependencyList) -> GraphResult<()> {
280        for dependency in dependencies.as_slice() {
281            if self
282                .nodes
283                .get(dependency)
284                .is_some_and(|meta| meta.kind() == NodeKind::Collection)
285            {
286                return Err(GraphError::CollectionDependencyNotAllowed(*dependency));
287            }
288        }
289        Ok(())
290    }
291
292    fn depends_on(&self, start: NodeId, target: NodeId) -> bool {
293        let Some(meta) = self.nodes.get(&start) else {
294            return false;
295        };
296        meta.dependencies()
297            .as_slice()
298            .iter()
299            .any(|dependency| *dependency == target || self.depends_on(*dependency, target))
300    }
301}