1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::messaging_card::tier::Tier;
7use crate::messaging_card::types::{Action, MessageCard};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct MessageCardIr {
11 pub tier: Tier,
12 pub head: Head,
13 #[serde(default, skip_serializing_if = "Vec::is_empty")]
14 pub elements: Vec<Element>,
15 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub actions: Vec<IrAction>,
17 #[serde(default)]
18 pub meta: Meta,
19}
20
21impl Default for MessageCardIr {
22 fn default() -> Self {
23 Self {
24 tier: Tier::Basic,
25 head: Head::default(),
26 elements: Vec::new(),
27 actions: Vec::new(),
28 meta: Meta::default(),
29 }
30 }
31}
32
33impl MessageCardIr {
34 pub fn from_plain(card: &MessageCard) -> Self {
35 let mut builder = MessageCardIrBuilder::default().tier(Tier::Basic);
36
37 if let Some(title) = &card.title {
38 builder = builder.title(title);
39 }
40 if let Some(text) = &card.text {
41 builder = builder.primary_text(text, card.allow_markdown);
42 }
43 if let Some(footer) = &card.footer {
44 builder = builder.footer(footer);
45 }
46 for image in &card.images {
47 builder = builder.image(image.url.clone(), image.alt.clone());
48 }
49 for action in &card.actions {
50 builder = match action {
51 Action::OpenUrl { title, url } => builder.open_url(title, url),
52 Action::PostBack { title, data } => builder.postback(title, data.clone()),
53 };
54 }
55
56 let mut built = builder.build();
57 built.auto_tier();
58 built
59 }
60
61 pub fn auto_tier(&mut self) {
62 self.tier = self.derive_tier();
63 }
64
65 fn derive_tier(&self) -> Tier {
66 let premium = self
67 .elements
68 .iter()
69 .any(|element| matches!(element, Element::Input { .. }))
70 || self
71 .meta
72 .capabilities
73 .iter()
74 .any(|cap| matches!(cap.as_str(), "inputs" | "execute" | "showcard"));
75 if premium {
76 return Tier::Premium;
77 }
78
79 let advanced = self
80 .elements
81 .iter()
82 .any(|element| matches!(element, Element::Image { .. } | Element::FactSet { .. }))
83 || self
84 .actions
85 .iter()
86 .any(|action| matches!(action, IrAction::Postback { .. }));
87
88 if advanced {
89 Tier::Advanced
90 } else {
91 Tier::Basic
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
97pub struct Head {
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub title: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub text: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub footer: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(tag = "type", rename_all = "snake_case")]
108pub enum Element {
109 Text {
110 text: String,
111 markdown: bool,
112 },
113 Image {
114 url: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 alt: Option<String>,
117 },
118 FactSet {
119 facts: Vec<Fact>,
120 },
121 Input {
122 #[serde(skip_serializing_if = "Option::is_none")]
123 label: Option<String>,
124 kind: InputKind,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 id: Option<String>,
127 required: bool,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 choices: Vec<InputChoice>,
130 },
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct Fact {
135 pub label: String,
136 pub value: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(rename_all = "snake_case")]
141pub enum InputKind {
142 Text,
143 Choice,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct InputChoice {
148 pub title: String,
149 pub value: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153#[serde(tag = "type", rename_all = "snake_case")]
154pub enum IrAction {
155 OpenUrl { title: String, url: String },
156 Postback { title: String, data: Value },
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
160pub struct Meta {
161 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
162 pub capabilities: BTreeSet<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub source: Option<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub warnings: Vec<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub adaptive_payload: Option<Value>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub app_link: Option<AppLink>,
171}
172
173impl Meta {
174 pub fn add_capability(&mut self, cap: impl Into<String>) {
175 self.capabilities.insert(cap.into());
176 }
177
178 pub fn warn(&mut self, message: impl Into<String>) {
179 self.warnings.push(message.into());
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct AppLink {
185 pub base_url: String,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub secret: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub tenant: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub scope: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub state: Option<Value>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub jwt: Option<AppLinkJwt>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199pub struct AppLinkJwt {
200 pub secret: String,
201 #[serde(default = "default_app_link_jwt_algorithm")]
202 pub algorithm: String,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub audience: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub issuer: Option<String>,
207 #[serde(default = "default_app_link_jwt_ttl")]
208 pub ttl_seconds: u64,
209}
210
211fn default_app_link_jwt_algorithm() -> String {
212 "HS256".into()
213}
214
215fn default_app_link_jwt_ttl() -> u64 {
216 900
217}
218
219#[derive(Debug, Default)]
220pub struct MessageCardIrBuilder {
221 inner: MessageCardIr,
222}
223
224impl MessageCardIrBuilder {
225 pub fn tier(mut self, tier: Tier) -> Self {
226 self.inner.tier = tier;
227 self
228 }
229
230 pub fn title(mut self, title: &str) -> Self {
231 self.inner.head.title = Some(title.to_string());
232 self
233 }
234
235 pub fn primary_text(mut self, text: &str, markdown: bool) -> Self {
236 self.inner.head.text = Some(text.to_string());
237 self.inner.elements.push(Element::Text {
238 text: text.into(),
239 markdown,
240 });
241 self
242 }
243
244 pub fn footer(mut self, footer: &str) -> Self {
245 self.inner.head.footer = Some(footer.to_string());
246 self
247 }
248
249 pub fn image(mut self, url: String, alt: Option<String>) -> Self {
250 self.inner.elements.push(Element::Image { url, alt });
251 self
252 }
253
254 pub fn fact(mut self, label: &str, value: &str) -> Self {
255 self.inner.elements.push(Element::FactSet {
256 facts: vec![Fact {
257 label: label.into(),
258 value: value.into(),
259 }],
260 });
261 self
262 }
263
264 pub fn input(
265 mut self,
266 label: Option<String>,
267 kind: InputKind,
268 id: Option<String>,
269 choices: Vec<InputChoice>,
270 ) -> Self {
271 self.inner.elements.push(Element::Input {
272 label,
273 kind,
274 id,
275 required: false,
276 choices,
277 });
278 self
279 }
280
281 pub fn open_url(mut self, title: &str, url: &str) -> Self {
282 self.inner.actions.push(IrAction::OpenUrl {
283 title: title.into(),
284 url: url.into(),
285 });
286 self
287 }
288
289 pub fn postback(mut self, title: &str, data: Value) -> Self {
290 self.inner.actions.push(IrAction::Postback {
291 title: title.into(),
292 data,
293 });
294 self
295 }
296
297 pub fn build(self) -> MessageCardIr {
298 self.inner
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::messaging_card::types::{Action, ImageRef, MessageCard};
306 use serde_json::json;
307
308 #[test]
309 fn builder_translates_plain_card() {
310 let card = MessageCard {
311 title: Some("IR".into()),
312 text: Some("hello".into()),
313 footer: Some("footer".into()),
314 images: vec![ImageRef {
315 url: "https://example.com/img.png".into(),
316 alt: Some("img".into()),
317 }],
318 actions: vec![
319 Action::OpenUrl {
320 title: "view".into(),
321 url: "https://example.com".into(),
322 },
323 Action::PostBack {
324 title: "ack".into(),
325 data: json!({"ok": true}),
326 },
327 ],
328 ..Default::default()
329 };
330
331 let ir = MessageCardIr::from_plain(&card);
332 assert_eq!(ir.head.title, Some("IR".into()));
333 assert_eq!(ir.elements.len(), 2);
334 assert_eq!(ir.actions.len(), 2);
335 assert_eq!(ir.tier, Tier::Advanced);
336 }
337}