Skip to main content

trellis_core/
resource.rs

1use crate::{Graph, GraphResult, NodeId, ScopeId};
2use core::fmt;
3use std::sync::Arc;
4
5/// Stable identity for a desired external resource.
6#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct ResourceKey(Box<str>);
9
10impl ResourceKey {
11    /// Creates a resource key from deterministic host-chosen identity.
12    pub fn new(key: impl Into<Box<str>>) -> Self {
13        Self(key.into())
14    }
15
16    /// Creates an explicit broad-resource key.
17    ///
18    /// Core treats this as an opaque identity; tests and applications decide
19    /// whether the key represents a forbidden fallback or wildcard resource.
20    pub fn wildcard(key: impl AsRef<str>) -> Self {
21        Self::new(format!("wildcard:{}", key.as_ref()))
22    }
23
24    /// Returns this key as a string slice.
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28}
29
30impl fmt::Debug for ResourceKey {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.debug_tuple("ResourceKey").field(&self.0).finish()
33    }
34}
35
36/// Data-only command describing an external resource lifecycle change.
37#[derive(Clone, Debug, Eq, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub enum ResourceCommand<C> {
40    /// Open a resource with an application-defined command payload.
41    Open {
42        /// Resource identity understood by the graph.
43        key: ResourceKey,
44        /// Scope requesting ownership.
45        scope: ScopeId,
46        /// Host-defined command payload.
47        command: C,
48    },
49    /// Close a resource after its final graph-visible owner is removed.
50    Close {
51        /// Resource identity understood by the graph.
52        key: ResourceKey,
53        /// Scope whose ownership was removed.
54        scope: ScopeId,
55    },
56    /// Replace a live resource with an application-defined command payload.
57    Replace {
58        /// Resource identity understood by the graph.
59        key: ResourceKey,
60        /// Scope requesting replacement.
61        scope: ScopeId,
62        /// Host-defined command payload.
63        command: C,
64    },
65    /// Refresh a live resource with an application-defined command payload.
66    Refresh {
67        /// Resource identity understood by the graph.
68        key: ResourceKey,
69        /// Scope requesting refresh.
70        scope: ScopeId,
71        /// Host-defined command payload.
72        command: C,
73    },
74}
75
76impl<C> ResourceCommand<C> {
77    /// Returns the resource key for this command.
78    pub fn key(&self) -> &ResourceKey {
79        match self {
80            Self::Open { key, .. }
81            | Self::Close { key, .. }
82            | Self::Replace { key, .. }
83            | Self::Refresh { key, .. } => key,
84        }
85    }
86
87    /// Returns the scope associated with this command.
88    pub fn scope(&self) -> ScopeId {
89        match self {
90            Self::Open { scope, .. }
91            | Self::Close { scope, .. }
92            | Self::Replace { scope, .. }
93            | Self::Refresh { scope, .. } => *scope,
94        }
95    }
96}
97
98/// Ordered data-only resource plan returned from graph propagation.
99#[derive(Clone, Debug, Eq, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct ResourcePlan<C> {
102    commands: Vec<ResourceCommand<C>>,
103}
104
105impl<C> ResourcePlan<C> {
106    /// Creates an empty resource plan.
107    pub fn new() -> Self {
108        Self {
109            commands: Vec::new(),
110        }
111    }
112
113    /// Adds an open command.
114    pub fn open(&mut self, key: ResourceKey, scope: ScopeId, command: C) {
115        self.commands.push(ResourceCommand::Open {
116            key,
117            scope,
118            command,
119        });
120    }
121
122    /// Adds a close command.
123    pub fn close(&mut self, key: ResourceKey, scope: ScopeId) {
124        self.commands.push(ResourceCommand::Close { key, scope });
125    }
126
127    /// Adds a replace command.
128    pub fn replace(&mut self, key: ResourceKey, scope: ScopeId, command: C) {
129        self.commands.push(ResourceCommand::Replace {
130            key,
131            scope,
132            command,
133        });
134    }
135
136    /// Adds a refresh command.
137    pub fn refresh(&mut self, key: ResourceKey, scope: ScopeId, command: C) {
138        self.commands.push(ResourceCommand::Refresh {
139            key,
140            scope,
141            command,
142        });
143    }
144
145    /// Returns ordered commands in this plan.
146    pub fn commands(&self) -> &[ResourceCommand<C>] {
147        &self.commands
148    }
149
150    /// Consumes the plan into ordered commands.
151    pub fn into_commands(self) -> Vec<ResourceCommand<C>> {
152        self.commands
153    }
154
155    pub(crate) fn append(&mut self, other: ResourcePlan<C>) {
156        self.commands.extend(other.commands);
157    }
158}
159
160impl<C> Default for ResourcePlan<C> {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166/// Read-only context passed to resource planners.
167pub struct PlanContext<'graph, D> {
168    scope: ScopeId,
169    diff: &'graph D,
170}
171
172impl<'graph, D> PlanContext<'graph, D> {
173    pub(crate) fn new(scope: ScopeId, diff: &'graph D) -> Self {
174        Self { scope, diff }
175    }
176
177    /// Scope that owns resource demand produced by this planner.
178    pub fn scope(&self) -> ScopeId {
179        self.scope
180    }
181
182    /// Structural diff consumed by this planner.
183    pub fn diff(&self) -> &'graph D {
184        self.diff
185    }
186}
187
188type PlannerFn<C, O> = dyn Fn(&Graph<C, O>) -> GraphResult<ResourcePlan<C>>;
189
190pub(crate) struct ResourcePlanner<C, O> {
191    pub(crate) collection: NodeId,
192    pub(crate) scope: ScopeId,
193    run: Arc<PlannerFn<C, O>>,
194}
195
196impl<C, O> Clone for ResourcePlanner<C, O> {
197    fn clone(&self) -> Self {
198        Self {
199            collection: self.collection,
200            scope: self.scope,
201            run: Arc::clone(&self.run),
202        }
203    }
204}
205
206impl<C, O> ResourcePlanner<C, O> {
207    pub(crate) fn new(
208        collection: NodeId,
209        scope: ScopeId,
210        run: impl Fn(&Graph<C, O>) -> GraphResult<ResourcePlan<C>> + 'static,
211    ) -> Self {
212        Self {
213            collection,
214            scope,
215            run: Arc::new(run),
216        }
217    }
218
219    pub(crate) fn run(&self, graph: &Graph<C, O>) -> GraphResult<ResourcePlan<C>> {
220        (self.run)(graph)
221    }
222}