Skip to main content

conversation_api/
interaction.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4#[serde(tag = "type", rename_all = "snake_case")]
5pub enum PreviewValue {
6    Text { text: String },
7    TextList { items: Vec<String> },
8    Number { value: f64 },
9    Bool { value: bool },
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct PreviewDetail {
15    pub label: String,
16    pub value: PreviewValue,
17}
18
19#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum InteractionTone {
22    #[default]
23    Normal,
24    Danger,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(
29    tag = "type",
30    rename_all = "snake_case",
31    rename_all_fields = "camelCase"
32)]
33pub enum ClientTask {
34    InstallIntegrations { integration_ids: Vec<String> },
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38#[serde(rename_all = "camelCase")]
39pub struct InteractionPreview {
40    pub action_code: String,
41    pub title: String,
42    pub message: String,
43    #[serde(default)]
44    pub tone: InteractionTone,
45    #[serde(default)]
46    pub details: Vec<PreviewDetail>,
47    pub proceed_label: String,
48    pub reject_label: String,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub client_task: Option<ClientTask>,
51}
52
53#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum InteractionKind {
56    #[default]
57    Approval,
58    UserAction,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62#[serde(rename_all = "camelCase")]
63pub struct PendingInteractionItem {
64    pub action_id: String,
65    pub kind: InteractionKind,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub expires_at: Option<String>,
68    pub preview: InteractionPreview,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "camelCase")]
73pub struct PendingInteraction {
74    pub request_id: String,
75    pub batch_id: String,
76    pub title: String,
77    pub message: String,
78    pub items: Vec<PendingInteractionItem>,
79}
80
81#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "snake_case")]
83pub enum InteractionDecisionValue {
84    Approve,
85    Reject,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90pub struct InteractionDecision {
91    pub action_id: String,
92    pub decision: InteractionDecisionValue,
93}
94
95impl Default for InteractionPreview {
96    fn default() -> Self {
97        Self::confirmation(
98            "interaction",
99            "Review action",
100            "Review this action before continuing.",
101        )
102    }
103}
104
105impl InteractionPreview {
106    #[must_use]
107    pub fn confirmation(
108        action_code: impl Into<String>,
109        title: impl Into<String>,
110        message: impl Into<String>,
111    ) -> Self {
112        Self {
113            action_code: action_code.into(),
114            title: title.into(),
115            message: message.into(),
116            tone: InteractionTone::Normal,
117            details: Vec::new(),
118            proceed_label: "Continue".to_owned(),
119            reject_label: "Cancel".to_owned(),
120            client_task: None,
121        }
122    }
123
124    /// Returns whether the facade supplied a complete display-only interaction contract.
125    #[must_use]
126    pub fn is_valid(&self) -> bool {
127        !self.action_code.trim().is_empty()
128            && !self.title.trim().is_empty()
129            && !self.proceed_label.trim().is_empty()
130            && !self.reject_label.trim().is_empty()
131            && self.details.iter().all(|detail| {
132                !detail.label.trim().is_empty()
133                    && match &detail.value {
134                        PreviewValue::Number { value } => value.is_finite(),
135                        PreviewValue::TextList { items } => {
136                            items.iter().all(|item| !item.trim().is_empty())
137                        }
138                        PreviewValue::Text { .. } | PreviewValue::Bool { .. } => true,
139                    }
140            })
141            && match &self.client_task {
142                Some(ClientTask::InstallIntegrations { integration_ids }) => {
143                    !integration_ids.is_empty()
144                        && integration_ids
145                            .iter()
146                            .all(|integration_id| !integration_id.trim().is_empty())
147                }
148                None => true,
149            }
150    }
151}