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