made_core/value_objects/artifact/
artifact_provenance.rs1use 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#[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 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}