Skip to main content

made_api/
ceremony_summary.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{CeremonyParticipant, InterventionView};
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    pub created_at_millis: i64,
32    pub updated_at_millis: i64,
33    pub completed_at_millis: Option<i64>,
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn a_summary_survives_the_wire() {
42        let summary = CeremonySummary {
43            ceremony_id: "c-1".to_owned(),
44            definition_name: "scope_discovery".to_owned(),
45            definition_version: "1.0".to_owned(),
46            definition_digest: Some("abc123".to_owned()),
47            current_state: "STARTED".to_owned(),
48            participants: vec![CeremonyParticipant {
49                role_id: "FACILITATOR".to_owned(),
50                specialty: "coordination".to_owned(),
51                bound_at_millis: 1_700_000_000_000,
52            }],
53            interventions: Vec::new(),
54            context: BTreeMap::from([(
55                "requested_by".to_owned(),
56                serde_json::Value::String("consumer-1".to_owned()),
57            )]),
58            created_at_millis: 1_700_000_000_000,
59            updated_at_millis: 1_700_000_000_000,
60            completed_at_millis: None,
61        };
62        let bytes = serde_json::to_vec(&summary).expect("serializes");
63        assert_eq!(
64            serde_json::from_slice::<CeremonySummary>(&bytes).expect("deserializes"),
65            summary
66        );
67    }
68
69    #[test]
70    fn an_unbound_instance_has_no_digest_rather_than_a_placeholder() {
71        let summary = CeremonySummary {
72            ceremony_id: "c-1".to_owned(),
73            definition_name: "draft".to_owned(),
74            definition_version: "1.0".to_owned(),
75            definition_digest: None,
76            current_state: "STARTED".to_owned(),
77            participants: Vec::new(),
78            interventions: Vec::new(),
79            context: BTreeMap::new(),
80            created_at_millis: 1,
81            updated_at_millis: 1,
82            completed_at_millis: None,
83        };
84        assert!(
85            summary.definition_digest.is_none(),
86            "a placeholder digest would let an unpublished draft read as provable"
87        );
88    }
89}