Skip to main content

made_api/
ceremony_summary.rs

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