Skip to main content

made_core/value_objects/execution/
execution_intent.rs

1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4use super::{ExecutionConnectorId, ExecutionOperation, ExecutionRecoveryCapability};
5use crate::error::DomainError;
6use crate::value_objects::artifact::ArtifactSourceKind;
7use crate::value_objects::audit::AuditActorKind;
8use crate::value_objects::ceremony::StepClaimFence;
9
10/// Durable declaration written immediately before one claim calls a connector.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ExecutionIntent {
13    operation: ExecutionOperation,
14    claim_fence: StepClaimFence,
15    connector_id: ExecutionConnectorId,
16    recovery_capability: ExecutionRecoveryCapability,
17    source_kind: ArtifactSourceKind,
18    actor_kind: AuditActorKind,
19    #[serde(with = "time::serde::rfc3339")]
20    recorded_at: OffsetDateTime,
21}
22
23impl ExecutionIntent {
24    pub fn new(
25        operation: ExecutionOperation,
26        claim_fence: StepClaimFence,
27        connector_id: ExecutionConnectorId,
28        recovery_capability: ExecutionRecoveryCapability,
29        source_kind: ArtifactSourceKind,
30        actor_kind: AuditActorKind,
31        recorded_at: OffsetDateTime,
32    ) -> Result<Self, crate::error::DomainError> {
33        if !source_kind.is_execution_source() {
34            return Err(crate::error::DomainError::InvariantViolated {
35                reason: "execution intent requires an execution source kind",
36            });
37        }
38        Ok(Self {
39            operation,
40            claim_fence,
41            connector_id,
42            recovery_capability,
43            source_kind,
44            actor_kind,
45            recorded_at,
46        })
47    }
48
49    #[must_use]
50    pub const fn operation(&self) -> &ExecutionOperation {
51        &self.operation
52    }
53
54    #[must_use]
55    pub const fn claim_fence(&self) -> &StepClaimFence {
56        &self.claim_fence
57    }
58
59    #[must_use]
60    pub const fn connector_id(&self) -> &ExecutionConnectorId {
61        &self.connector_id
62    }
63
64    #[must_use]
65    pub const fn recovery_capability(&self) -> ExecutionRecoveryCapability {
66        self.recovery_capability
67    }
68
69    #[must_use]
70    pub const fn source_kind(&self) -> ArtifactSourceKind {
71        self.source_kind
72    }
73
74    #[must_use]
75    pub const fn actor_kind(&self) -> AuditActorKind {
76        self.actor_kind
77    }
78
79    #[must_use]
80    pub const fn recorded_at(&self) -> OffsetDateTime {
81        self.recorded_at
82    }
83
84    /// Re-check nested and cross-field invariants after deserialization.
85    pub fn validate(&self) -> Result<(), DomainError> {
86        self.operation.validate()?;
87        if !self.source_kind.is_execution_source() {
88            return Err(DomainError::InvariantViolated {
89                reason: "execution intent requires an execution source kind",
90            });
91        }
92        Ok(())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use serde_json::json;
99
100    use super::*;
101    use crate::value_objects::{CeremonyId, StateIteration, StateVisit, StepId, StepIteration};
102
103    #[test]
104    fn deserialized_intent_rejects_a_non_execution_source() {
105        let operation = ExecutionOperation::new(
106            CeremonyId::new("ceremony").unwrap(),
107            StepId::new("work").unwrap(),
108            StateVisit::FIRST,
109            StateIteration::FIRST,
110            StepIteration::FIRST,
111            super::super::ExecutionRequestBytes::new(b"request".to_vec()).unwrap(),
112        );
113        let mut raw = serde_json::to_value(
114            ExecutionIntent::new(
115                operation,
116                StepClaimFence::new("1".repeat(64)).unwrap(),
117                ExecutionConnectorId::new("test").unwrap(),
118                ExecutionRecoveryCapability::IdempotentByOperationId,
119                ArtifactSourceKind::NoOp,
120                AuditActorKind::Engine,
121                OffsetDateTime::UNIX_EPOCH,
122            )
123            .unwrap(),
124        )
125        .unwrap();
126        raw["source_kind"] = json!("generated_report");
127        let corrupted: ExecutionIntent = serde_json::from_value(raw).unwrap();
128        assert!(corrupted.validate().is_err());
129    }
130}