Skip to main content

made_api/
intervention_views.rs

1use serde::{Deserialize, Serialize};
2
3/// One answer given at the table.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub struct InterventionResponseView {
6    pub role_id: String,
7    pub content: String,
8    pub responded_at_millis: i64,
9}
10
11/// One intervention — a question, investigation or proposed action put to the
12/// table — as a consumer sees it.
13///
14/// The table's conversational unit. A projection, never the entity: responses
15/// come with who answered and when, the request with who asked, and `open`
16/// with whether the table still owes an answer.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct InterventionView {
19    pub intervention_id: String,
20    /// `opinion`, `investigation` or `action`.
21    pub kind: String,
22    pub requested_by: String,
23    /// The seats asked. Empty means the whole table.
24    pub target_role_ids: Vec<String>,
25    pub request: String,
26    pub open: bool,
27    pub responses: Vec<InterventionResponseView>,
28    pub created_at_millis: i64,
29    pub closed_at_millis: Option<i64>,
30}
31
32/// Put a question, investigation or proposed action to the table.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct RaiseInterventionRequest {
35    pub ceremony_id: String,
36    /// Chosen by the caller so a retried request raises the same intervention,
37    /// not a second one.
38    pub intervention_id: String,
39    /// The seat speaking. A seat, not a person: the engine sees roles and
40    /// cannot see what fills them.
41    pub role_id: String,
42    /// One of `human`, `agent`, `service`, `engine`.
43    pub role_kind: String,
44    /// One of `opinion`, `investigation`, `action`.
45    pub kind: String,
46    /// The seats asked. Empty asks the whole table.
47    pub target_role_ids: Vec<String>,
48    pub request: String,
49}
50
51/// Answer an open intervention.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct RespondToInterventionRequest {
54    pub ceremony_id: String,
55    pub intervention_id: String,
56    pub role_id: String,
57    pub role_kind: String,
58    pub content: String,
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn an_intervention_survives_the_wire() {
67        let view = InterventionView {
68            intervention_id: "iv-1".to_owned(),
69            kind: "opinion".to_owned(),
70            requested_by: "FACILITATOR".to_owned(),
71            target_role_ids: vec!["REVIEWER".to_owned()],
72            request: "Recommend the safest cleanup.".to_owned(),
73            open: true,
74            responses: vec![InterventionResponseView {
75                role_id: "REVIEWER".to_owned(),
76                content: "Rotate the certificate first.".to_owned(),
77                responded_at_millis: 1_700_000_000_000,
78            }],
79            created_at_millis: 1_700_000_000_000,
80            closed_at_millis: None,
81        };
82        let bytes = serde_json::to_vec(&view).expect("serializes");
83        assert_eq!(
84            serde_json::from_slice::<InterventionView>(&bytes).expect("deserializes"),
85            view
86        );
87    }
88
89    #[test]
90    fn an_empty_target_means_the_whole_table() {
91        let request = RaiseInterventionRequest {
92            ceremony_id: "c-1".to_owned(),
93            intervention_id: "iv-1".to_owned(),
94            role_id: "FACILITATOR".to_owned(),
95            role_kind: "human".to_owned(),
96            kind: "opinion".to_owned(),
97            target_role_ids: Vec::new(),
98            request: "Thoughts?".to_owned(),
99        };
100        assert!(
101            request.target_role_ids.is_empty(),
102            "no listed seat narrows the question; everyone at the table is asked"
103        );
104    }
105}