Skip to main content

made_core/value_objects/artifact/
artifact_provenance.rs

1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4use super::{ArtifactImportRef, ArtifactSourceKind};
5use crate::error::DomainError;
6use crate::value_objects::ceremony::StepClaimFence;
7use crate::value_objects::execution::{ExecutionOperationId, ExecutionReceiptId};
8
9/// Typed origin of artifact metadata. Authentication joins in C5.7.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ArtifactProvenance {
12    source_kind: ArtifactSourceKind,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    execution_receipt_id: Option<ExecutionReceiptId>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    operation_id: Option<ExecutionOperationId>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    accepted_claim_fence: Option<StepClaimFence>,
19    #[serde(with = "time::serde::rfc3339")]
20    observed_at: OffsetDateTime,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    import_ref: Option<ArtifactImportRef>,
23}
24
25impl ArtifactProvenance {
26    pub fn execution(
27        source_kind: ArtifactSourceKind,
28        execution_receipt_id: ExecutionReceiptId,
29        operation_id: ExecutionOperationId,
30        accepted_claim_fence: StepClaimFence,
31        observed_at: OffsetDateTime,
32    ) -> Result<Self, DomainError> {
33        if !source_kind.is_execution_source() {
34            return Err(DomainError::InvariantViolated {
35                reason: "execution artifact provenance requires an execution source kind",
36            });
37        }
38        Ok(Self {
39            source_kind,
40            execution_receipt_id: Some(execution_receipt_id),
41            operation_id: Some(operation_id),
42            accepted_claim_fence: Some(accepted_claim_fence),
43            observed_at,
44            import_ref: None,
45        })
46    }
47
48    #[must_use]
49    pub fn generated_report(observed_at: OffsetDateTime) -> Self {
50        Self {
51            source_kind: ArtifactSourceKind::GeneratedReport,
52            execution_receipt_id: None,
53            operation_id: None,
54            accepted_claim_fence: None,
55            observed_at,
56            import_ref: None,
57        }
58    }
59
60    #[must_use]
61    pub fn imported(import_ref: ArtifactImportRef, observed_at: OffsetDateTime) -> Self {
62        Self {
63            source_kind: ArtifactSourceKind::Imported,
64            execution_receipt_id: None,
65            operation_id: None,
66            accepted_claim_fence: None,
67            observed_at,
68            import_ref: Some(import_ref),
69        }
70    }
71
72    /// Re-check invariants after a persistence or wire deserialization.
73    pub fn validate(&self) -> Result<(), DomainError> {
74        let has_execution = self.execution_receipt_id.is_some()
75            && self.operation_id.is_some()
76            && self.accepted_claim_fence.is_some();
77        let has_partial_execution = self.execution_receipt_id.is_some()
78            || self.operation_id.is_some()
79            || self.accepted_claim_fence.is_some();
80        match self.source_kind {
81            ArtifactSourceKind::ExternalExecution
82            | ArtifactSourceKind::Fixture
83            | ArtifactSourceKind::NoOp
84                if has_execution && self.import_ref.is_none() =>
85            {
86                Ok(())
87            }
88            ArtifactSourceKind::GeneratedReport
89                if !has_partial_execution && self.import_ref.is_none() =>
90            {
91                Ok(())
92            }
93            ArtifactSourceKind::Imported if !has_partial_execution && self.import_ref.is_some() => {
94                Ok(())
95            }
96            _ => Err(DomainError::InvariantViolated {
97                reason: "artifact provenance fields do not match its source kind",
98            }),
99        }
100    }
101
102    #[must_use]
103    pub const fn source_kind(&self) -> ArtifactSourceKind {
104        self.source_kind
105    }
106
107    #[must_use]
108    pub fn execution_receipt_id(&self) -> Option<&ExecutionReceiptId> {
109        self.execution_receipt_id.as_ref()
110    }
111
112    #[must_use]
113    pub fn operation_id(&self) -> Option<&ExecutionOperationId> {
114        self.operation_id.as_ref()
115    }
116
117    #[must_use]
118    pub fn accepted_claim_fence(&self) -> Option<&StepClaimFence> {
119        self.accepted_claim_fence.as_ref()
120    }
121
122    #[must_use]
123    pub const fn observed_at(&self) -> OffsetDateTime {
124        self.observed_at
125    }
126
127    #[must_use]
128    pub fn import_ref(&self) -> Option<&ArtifactImportRef> {
129        self.import_ref.as_ref()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use serde_json::json;
136
137    use super::*;
138
139    #[test]
140    fn deserialized_provenance_must_match_its_source_kind() {
141        let raw = json!({
142            "source_kind": "external_execution",
143            "observed_at": "1970-01-01T00:00:00Z"
144        });
145        let provenance: ArtifactProvenance = serde_json::from_value(raw).unwrap();
146        assert!(provenance.validate().is_err());
147    }
148}