made_core/entities/
ceremony_evidence_pack.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use time::OffsetDateTime;
7
8use crate::error::DomainError;
9use crate::value_objects::{Attributes, CeremonyEvidenceSourceId, CeremonyInterventionContent};
10
11use super::ExternalContextBundle;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct CeremonyEvidencePack {
15 source_id: CeremonyEvidenceSourceId,
16 bundle: ExternalContextBundle,
17 #[serde(with = "time::serde::rfc3339")]
18 collected_at: OffsetDateTime,
19}
20
21impl CeremonyEvidencePack {
22 pub fn new(
23 source_id: CeremonyEvidenceSourceId,
24 bundle: ExternalContextBundle,
25 collected_at: OffsetDateTime,
26 ) -> Result<Self, DomainError> {
27 if bundle.summary().is_none() {
28 return Err(DomainError::EmptyField {
29 field: "ceremony_evidence_pack.summary",
30 });
31 }
32 if bundle.items().is_empty() && bundle.references().is_empty() {
33 return Err(DomainError::EmptyCollection {
34 field: "ceremony_evidence_pack.evidence",
35 });
36 }
37 Ok(Self {
38 source_id,
39 bundle,
40 collected_at,
41 })
42 }
43
44 #[must_use]
45 pub fn source_id(&self) -> &CeremonyEvidenceSourceId {
46 &self.source_id
47 }
48
49 #[must_use]
50 pub fn bundle(&self) -> &ExternalContextBundle {
51 &self.bundle
52 }
53
54 #[must_use]
55 pub fn collected_at(&self) -> OffsetDateTime {
56 self.collected_at
57 }
58
59 pub fn intervention_content(&self) -> Result<CeremonyInterventionContent, DomainError> {
60 let summary = self
61 .bundle
62 .summary()
63 .ok_or(DomainError::InvariantViolated {
64 reason: "ceremony evidence pack must retain its summary",
65 })?;
66 let serialized =
67 serde_json::to_value(self).map_err(|_| DomainError::InvariantViolated {
68 reason: "ceremony evidence pack could not be represented as attributes",
69 })?;
70 let details = Attributes::new(BTreeMap::from([("evidence_pack".to_owned(), serialized)]))?;
71 CeremonyInterventionContent::new(summary.text(), details)
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::entities::{ContextItem, ContextSummary};
79 use time::macros::datetime;
80
81 fn bundle(items: Vec<ContextItem>) -> ExternalContextBundle {
82 ExternalContextBundle::new(
83 "observability-1",
84 "1.0",
85 Some(ContextSummary::new("Error rate increased.", Attributes::empty()).unwrap()),
86 items,
87 Vec::new(),
88 Attributes::empty(),
89 )
90 .unwrap()
91 }
92
93 #[test]
94 fn rejects_an_empty_pack_even_when_it_has_a_summary() {
95 let error = CeremonyEvidencePack::new(
96 CeremonyEvidenceSourceId::new("observability").unwrap(),
97 bundle(Vec::new()),
98 datetime!(2026-07-21 18:00:00 UTC),
99 )
100 .unwrap_err();
101
102 assert!(matches!(error, DomainError::EmptyCollection { .. }));
103 }
104
105 #[test]
106 fn builds_intervention_content_with_the_typed_pack() {
107 let item = ContextItem::new(
108 "error-rate",
109 "metric",
110 "Checkout error rate",
111 Some("Error rate is 18%.".to_owned()),
112 Attributes::empty(),
113 Vec::new(),
114 )
115 .unwrap();
116 let pack = CeremonyEvidencePack::new(
117 CeremonyEvidenceSourceId::new("observability").unwrap(),
118 bundle(vec![item]),
119 datetime!(2026-07-21 18:00:00 UTC),
120 )
121 .unwrap();
122
123 let content = pack.intervention_content().unwrap();
124
125 assert_eq!(content.message(), "Error rate increased.");
126 assert_eq!(
127 content.details().as_map()["evidence_pack"]["source_id"],
128 "observability"
129 );
130 }
131}