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