1use std::collections::BTreeSet;
2
3use crate::{
4 serialization_compatibility, ComponentInstanceId, ResourceActivationId,
5 ResourceExecutionBoundary, ResourceId, SemanticId, SemanticType, SerializationCompatibility,
6 SourceProvenance,
7};
8use crate::{SemanticPackageKind, SemanticPackageResourceEndpoint};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ResourceEndpointResolution {
19 pub owner_component: SemanticId,
20 pub field: String,
21 pub endpoint_designator: Option<String>,
22 pub outcome: ResourceEndpointResolutionOutcome,
23 pub provenance: SourceProvenance,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ResourceEndpointResolutionOutcome {
28 MissingDesignator,
29 UnboundDesignator {
30 designator: String,
31 },
32 NonSemanticPackageBinding {
33 designator: String,
34 },
35 NonResourceBinding {
36 designator: String,
37 kind: SemanticPackageKind,
38 },
39 Resolved(ResourceEndpointBinding),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ResourceEndpointBinding {
45 pub local_name: String,
46 pub package: String,
47 pub version: String,
48 pub integrity: String,
49 pub export: String,
50 pub type_signature: String,
51 pub runtime_module: String,
52 pub resume_policy: String,
53 pub endpoint: SemanticPackageResourceEndpoint,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ResourceDeclaration {
64 pub id: ResourceId,
65 pub owner_component: SemanticId,
66 pub name: String,
67 pub key: String,
68 pub data_type: SemanticType,
69 pub error_type: SemanticType,
70 pub execution_boundary: ResourceExecutionBoundary,
71 pub input_dependencies: BTreeSet<SemanticId>,
72 pub retry_policy: ResourceRetryPolicy,
73 pub invalidation_policy: ResourceInvalidationPolicy,
74 pub provenance: SourceProvenance,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ResourceRetryPolicy {
79 Never,
80 ExplicitOnly,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ResourceInvalidationPolicy {
85 OnInputChange,
86 ExplicitOnly,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ResourceLifecycleState {
91 Idle,
92 Pending { generation: u64 },
93 Ready { generation: u64 },
94 Failed { generation: u64 },
95 Cancelled { generation: u64 },
96}
97
98impl ResourceLifecycleState {
99 #[must_use]
100 pub const fn generation(self) -> Option<u64> {
101 match self {
102 Self::Idle => None,
103 Self::Pending { generation }
104 | Self::Ready { generation }
105 | Self::Failed { generation }
106 | Self::Cancelled { generation } => Some(generation),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum ResourceLifecycleEvent {
113 Activate,
114 Resolve,
115 Reject,
116 Cancel,
117 Invalidate,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ResourceActivation {
122 pub id: ResourceActivationId,
123 pub declaration: ResourceId,
124 pub component_instance: ComponentInstanceId,
125 pub state: ResourceLifecycleState,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum ResourceDeclarationError {
130 EmptyName,
131 EmptyKey,
132 NonSerializableData,
133 NonSerializableError,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum ResourceLifecycleError {
138 InvalidTransition {
139 state: ResourceLifecycleState,
140 event: ResourceLifecycleEvent,
141 },
142}
143
144impl ResourceDeclaration {
145 #[allow(clippy::too_many_arguments)]
146 pub fn new(
147 owner_component: SemanticId,
148 name: String,
149 key: String,
150 data_type: SemanticType,
151 error_type: SemanticType,
152 execution_boundary: ResourceExecutionBoundary,
153 input_dependencies: BTreeSet<SemanticId>,
154 retry_policy: ResourceRetryPolicy,
155 invalidation_policy: ResourceInvalidationPolicy,
156 provenance: SourceProvenance,
157 ) -> Result<Self, ResourceDeclarationError> {
158 if name.is_empty() {
159 return Err(ResourceDeclarationError::EmptyName);
160 }
161 if key.is_empty() {
162 return Err(ResourceDeclarationError::EmptyKey);
163 }
164 if serialization_compatibility(&data_type) != SerializationCompatibility::Serializable {
165 return Err(ResourceDeclarationError::NonSerializableData);
166 }
167 if serialization_compatibility(&error_type) != SerializationCompatibility::Serializable {
168 return Err(ResourceDeclarationError::NonSerializableError);
169 }
170 let id = ResourceId::for_owner(&owner_component, &name);
171 Ok(Self {
172 id,
173 owner_component,
174 name,
175 key,
176 data_type,
177 error_type,
178 execution_boundary,
179 input_dependencies,
180 retry_policy,
181 invalidation_policy,
182 provenance,
183 })
184 }
185
186 #[must_use]
187 pub fn activation_for(&self, component_instance: ComponentInstanceId) -> ResourceActivation {
188 ResourceActivation {
189 id: ResourceActivationId::for_component_instance(&component_instance, &self.id),
190 declaration: self.id.clone(),
191 component_instance,
192 state: ResourceLifecycleState::Idle,
193 }
194 }
195}
196
197impl ResourceActivation {
198 pub fn transition(
199 &mut self,
200 event: ResourceLifecycleEvent,
201 ) -> Result<(), ResourceLifecycleError> {
202 let next = match (self.state, event) {
203 (ResourceLifecycleState::Idle, ResourceLifecycleEvent::Activate)
204 | (ResourceLifecycleState::Ready { .. }, ResourceLifecycleEvent::Invalidate)
205 | (ResourceLifecycleState::Failed { .. }, ResourceLifecycleEvent::Invalidate)
206 | (ResourceLifecycleState::Cancelled { .. }, ResourceLifecycleEvent::Activate)
207 | (ResourceLifecycleState::Cancelled { .. }, ResourceLifecycleEvent::Invalidate) => {
208 ResourceLifecycleState::Pending {
209 generation: self.state.generation().unwrap_or(0) + 1,
210 }
211 }
212 (ResourceLifecycleState::Pending { generation }, ResourceLifecycleEvent::Resolve) => {
213 ResourceLifecycleState::Ready { generation }
214 }
215 (ResourceLifecycleState::Pending { generation }, ResourceLifecycleEvent::Reject) => {
216 ResourceLifecycleState::Failed { generation }
217 }
218 (ResourceLifecycleState::Pending { generation }, ResourceLifecycleEvent::Cancel) => {
219 ResourceLifecycleState::Cancelled { generation }
220 }
221 (state, event) => {
222 return Err(ResourceLifecycleError::InvalidTransition { state, event })
223 }
224 };
225 self.state = next;
226 Ok(())
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::collections::BTreeSet;
233 use std::path::Path;
234
235 use presolve_parser::SourceSpan;
236
237 use super::{
238 ResourceDeclaration, ResourceDeclarationError, ResourceInvalidationPolicy,
239 ResourceLifecycleError, ResourceLifecycleEvent, ResourceLifecycleState,
240 ResourceRetryPolicy,
241 };
242 use crate::{
243 ComponentInstanceId, ComponentRootId, ResourceExecutionBoundary, SemanticId, SemanticType,
244 SourceProvenance,
245 };
246
247 fn declaration() -> ResourceDeclaration {
248 ResourceDeclaration::new(
249 SemanticId::component(Some("x-profile"), "Profile"),
250 "profile".to_owned(),
251 "profile-by-user".to_owned(),
252 SemanticType::String,
253 SemanticType::String,
254 ResourceExecutionBoundary::Shared,
255 BTreeSet::from([
256 SemanticId::component(Some("x-profile"), "Profile").state_field("userId")
257 ]),
258 ResourceRetryPolicy::ExplicitOnly,
259 ResourceInvalidationPolicy::OnInputChange,
260 SourceProvenance::new(
261 Path::new("src/Profile.tsx"),
262 SourceSpan {
263 start: 0,
264 end: 1,
265 line: 1,
266 column: 1,
267 },
268 ),
269 )
270 .expect("valid resource declaration")
271 }
272
273 #[test]
274 fn declaration_and_instance_activation_have_separate_stable_identities() {
275 let declaration = declaration();
276 let root =
277 ComponentRootId::for_component(&SemanticId::component(Some("x-profile"), "Profile"));
278 let activation = declaration.activation_for(ComponentInstanceId::for_root(&root));
279
280 assert_eq!(
281 declaration.id.as_str(),
282 "component:x-profile/resource:profile"
283 );
284 assert_eq!(
285 activation.id.as_str(),
286 "root:component:x-profile/resource-activation:component:x-profile/resource:profile"
287 );
288 assert_eq!(activation.state, ResourceLifecycleState::Idle);
289 }
290
291 #[test]
292 fn lifecycle_is_generation_scoped_and_rejects_impossible_transitions() {
293 let declaration = declaration();
294 let root =
295 ComponentRootId::for_component(&SemanticId::component(Some("x-profile"), "Profile"));
296 let mut activation = declaration.activation_for(ComponentInstanceId::for_root(&root));
297
298 activation
299 .transition(ResourceLifecycleEvent::Activate)
300 .expect("activate");
301 assert_eq!(
302 activation.state,
303 ResourceLifecycleState::Pending { generation: 1 }
304 );
305 activation
306 .transition(ResourceLifecycleEvent::Resolve)
307 .expect("resolve");
308 assert_eq!(
309 activation.state,
310 ResourceLifecycleState::Ready { generation: 1 }
311 );
312 activation
313 .transition(ResourceLifecycleEvent::Invalidate)
314 .expect("invalidate");
315 assert_eq!(
316 activation.state,
317 ResourceLifecycleState::Pending { generation: 2 }
318 );
319 activation
320 .transition(ResourceLifecycleEvent::Cancel)
321 .expect("cancel");
322 assert_eq!(
323 activation.state,
324 ResourceLifecycleState::Cancelled { generation: 2 }
325 );
326 assert_eq!(
327 activation.transition(ResourceLifecycleEvent::Resolve),
328 Err(ResourceLifecycleError::InvalidTransition {
329 state: ResourceLifecycleState::Cancelled { generation: 2 },
330 event: ResourceLifecycleEvent::Resolve,
331 })
332 );
333 }
334
335 #[test]
336 fn declaration_rejects_nonserializable_cross_boundary_data() {
337 let result = ResourceDeclaration::new(
338 SemanticId::component(Some("x-profile"), "Profile"),
339 "profile".to_owned(),
340 "profile-by-user".to_owned(),
341 SemanticType::Unknown,
342 SemanticType::String,
343 ResourceExecutionBoundary::Shared,
344 BTreeSet::new(),
345 ResourceRetryPolicy::Never,
346 ResourceInvalidationPolicy::ExplicitOnly,
347 SourceProvenance::new(
348 Path::new("src/Profile.tsx"),
349 SourceSpan {
350 start: 0,
351 end: 1,
352 line: 1,
353 column: 1,
354 },
355 ),
356 );
357
358 assert_eq!(result, Err(ResourceDeclarationError::NonSerializableData));
359 }
360}