Skip to main content

presolve_compiler/
resource.rs

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/// A source Resource declaration's compiler-owned attempt to select an
11/// integrity-checked semantic-package endpoint.
12///
13/// This is intentionally not a ResourceDeclaration. It records package
14/// resolution before activation, artifact, cancellation, and resume lowering
15/// exist, so source syntax remains non-executable until the full N6 contract
16/// can be emitted.
17#[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/// The exact endpoint metadata selected by a resolved Resource source fact.
43#[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/// Compiler-owned declaration for a Resource operation.
57///
58/// This is intentionally a semantic product, not an async runtime. A later
59/// N6 source/lowering slice must supply a registered endpoint or capability,
60/// activation artifact, cancellation transport, and resume codec before a
61/// Resource becomes executable.
62#[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}