Skip to main content

made_core/entities/
ceremony_evidence_pack.rs

1//! Immutable evidence returned by one host-provided ceremony source.
2
3use 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}