Skip to main content

made_api/
ceremony_summary.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{CeremonyParticipant, InterventionView, RecollectionView};
6
7/// One ceremony instance, as a consumer sees it.
8///
9/// A projection, never the aggregate. The instance inside the engine gains
10/// fields as the domain needs them; a consumer that read it directly would
11/// inherit each one as a contract. Everything here is plain data a consumer can
12/// hold, log or map into its own vocabulary without importing the domain.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct CeremonySummary {
15    pub ceremony_id: String,
16    pub definition_name: String,
17    pub definition_version: String,
18    /// The digest of the published definition this instance was bound to, hex
19    /// encoded — or absent for an instance started from an unpublished draft.
20    /// Present, it makes "this exact procedure ran" provable rather than a
21    /// promise about a name.
22    pub definition_digest: Option<String>,
23    pub current_state: String,
24    pub participants: Vec<CeremonyParticipant>,
25    /// The table's conversation: every intervention raised, with its answers.
26    pub interventions: Vec<InterventionView>,
27    /// The context the instance was started with. This is where a consuming
28    /// product keeps its own reference to its own aggregate — the engine
29    /// carries the keys without knowing what they mean.
30    pub context: BTreeMap<String, serde_json::Value>,
31    /// What earlier sessions in this ceremony's memory scope decided,
32    /// as this one was told when it opened. Absent when it was told
33    /// nothing.
34    #[serde(default)]
35    pub recollection: Option<RecollectionView>,
36    pub created_at_millis: i64,
37    pub updated_at_millis: i64,
38    pub completed_at_millis: Option<i64>,
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn a_summary_survives_the_wire() {
47        let summary = CeremonySummary {
48            ceremony_id: "c-1".to_owned(),
49            definition_name: "scope_discovery".to_owned(),
50            definition_version: "1.0".to_owned(),
51            definition_digest: Some("abc123".to_owned()),
52            current_state: "STARTED".to_owned(),
53            participants: vec![CeremonyParticipant {
54                role_id: "FACILITATOR".to_owned(),
55                specialty: "coordination".to_owned(),
56                bound_at_millis: 1_700_000_000_000,
57            }],
58            interventions: Vec::new(),
59            recollection: None,
60            context: BTreeMap::from([(
61                "requested_by".to_owned(),
62                serde_json::Value::String("consumer-1".to_owned()),
63            )]),
64            created_at_millis: 1_700_000_000_000,
65            updated_at_millis: 1_700_000_000_000,
66            completed_at_millis: None,
67        };
68        let bytes = serde_json::to_vec(&summary).expect("serializes");
69        assert_eq!(
70            serde_json::from_slice::<CeremonySummary>(&bytes).expect("deserializes"),
71            summary
72        );
73    }
74
75    #[test]
76    fn an_unbound_instance_has_no_digest_rather_than_a_placeholder() {
77        let summary = CeremonySummary {
78            ceremony_id: "c-1".to_owned(),
79            definition_name: "draft".to_owned(),
80            definition_version: "1.0".to_owned(),
81            definition_digest: None,
82            current_state: "STARTED".to_owned(),
83            participants: Vec::new(),
84            interventions: Vec::new(),
85            recollection: None,
86            context: BTreeMap::new(),
87            created_at_millis: 1,
88            updated_at_millis: 1,
89            completed_at_millis: None,
90        };
91        assert!(
92            summary.definition_digest.is_none(),
93            "a placeholder digest would let an unpublished draft read as provable"
94        );
95    }
96}