1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ModalElement {
17 pub callback_id: String,
19 pub title: String,
21 pub submit_label: Option<String>,
23 pub children: Vec<ModalChild>,
25 pub private_metadata: Option<String>,
27 pub notify_on_close: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum ModalChild {
34 TextInput(TextInputElement),
36 Select(SelectElement),
38 RadioSelect(RadioSelectElement),
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct TextInputElement {
45 pub id: String,
47 pub label: String,
49 pub placeholder: Option<String>,
51 pub initial_value: Option<String>,
53 pub multiline: bool,
55 pub optional: bool,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SelectElement {
62 pub id: String,
64 pub label: String,
66 pub placeholder: Option<String>,
68 pub options: Vec<SelectOption>,
70 pub initial_option: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct SelectOption {
77 pub label: String,
79 pub value: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct RadioSelectElement {
86 pub id: String,
88 pub label: String,
90 pub options: Vec<SelectOption>,
92 pub initial_option: Option<String>,
94}
95
96impl ModalElement {
101 #[must_use]
103 pub fn new(callback_id: impl Into<String>, title: impl Into<String>) -> Self {
104 Self {
105 callback_id: callback_id.into(),
106 title: title.into(),
107 submit_label: None,
108 children: Vec::new(),
109 private_metadata: None,
110 notify_on_close: false,
111 }
112 }
113
114 #[must_use]
116 pub fn submit_label(mut self, label: impl Into<String>) -> Self {
117 self.submit_label = Some(label.into());
118 self
119 }
120
121 #[must_use]
123 pub fn text_input(mut self, input: TextInputElement) -> Self {
124 self.children.push(ModalChild::TextInput(input));
125 self
126 }
127
128 #[must_use]
130 pub fn select(mut self, select: SelectElement) -> Self {
131 self.children.push(ModalChild::Select(select));
132 self
133 }
134
135 #[must_use]
137 pub fn radio_select(mut self, radio: RadioSelectElement) -> Self {
138 self.children.push(ModalChild::RadioSelect(radio));
139 self
140 }
141
142 #[must_use]
144 pub fn private_metadata(mut self, metadata: impl Into<String>) -> Self {
145 self.private_metadata = Some(metadata.into());
146 self
147 }
148
149 #[must_use]
151 pub fn notify_on_close(mut self, notify: bool) -> Self {
152 self.notify_on_close = notify;
153 self
154 }
155}
156
157impl TextInputElement {
162 #[must_use]
164 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
165 Self {
166 id: id.into(),
167 label: label.into(),
168 placeholder: None,
169 initial_value: None,
170 multiline: false,
171 optional: false,
172 }
173 }
174
175 #[must_use]
177 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
178 self.placeholder = Some(placeholder.into());
179 self
180 }
181
182 #[must_use]
184 pub fn initial_value(mut self, value: impl Into<String>) -> Self {
185 self.initial_value = Some(value.into());
186 self
187 }
188
189 #[must_use]
191 pub fn multiline(mut self, multiline: bool) -> Self {
192 self.multiline = multiline;
193 self
194 }
195
196 #[must_use]
198 pub fn optional(mut self, optional: bool) -> Self {
199 self.optional = optional;
200 self
201 }
202}
203
204impl SelectElement {
209 #[must_use]
211 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
212 Self {
213 id: id.into(),
214 label: label.into(),
215 placeholder: None,
216 options: Vec::new(),
217 initial_option: None,
218 }
219 }
220
221 #[must_use]
223 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
224 self.placeholder = Some(placeholder.into());
225 self
226 }
227
228 #[must_use]
230 pub fn option(mut self, option: SelectOption) -> Self {
231 self.options.push(option);
232 self
233 }
234
235 #[must_use]
237 pub fn initial_option(mut self, value: impl Into<String>) -> Self {
238 self.initial_option = Some(value.into());
239 self
240 }
241}
242
243impl SelectOption {
248 #[must_use]
250 pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
251 Self { label: label.into(), value: value.into() }
252 }
253}
254
255impl RadioSelectElement {
260 #[must_use]
262 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
263 Self { id: id.into(), label: label.into(), options: Vec::new(), initial_option: None }
264 }
265
266 #[must_use]
268 pub fn option(mut self, option: SelectOption) -> Self {
269 self.options.push(option);
270 self
271 }
272
273 #[must_use]
275 pub fn initial_option(mut self, value: impl Into<String>) -> Self {
276 self.initial_option = Some(value.into());
277 self
278 }
279}
280
281#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn empty_modal_construction() {
291 let modal = ModalElement::new("cb_1", "My Modal");
292 assert_eq!(modal.callback_id, "cb_1");
293 assert_eq!(modal.title, "My Modal");
294 assert!(modal.submit_label.is_none());
295 assert!(modal.children.is_empty());
296 assert!(modal.private_metadata.is_none());
297 assert!(!modal.notify_on_close);
298 }
299
300 #[test]
301 fn builder_chaining() {
302 let modal = ModalElement::new("cb_settings", "Settings")
303 .submit_label("Save")
304 .private_metadata("{\"v\":1}")
305 .notify_on_close(true)
306 .text_input(
307 TextInputElement::new("name", "Your Name")
308 .placeholder("Enter name")
309 .initial_value("Alice")
310 .multiline(false)
311 .optional(false),
312 )
313 .select(
314 SelectElement::new("color", "Favourite Colour")
315 .placeholder("Pick one")
316 .option(SelectOption::new("Red", "red"))
317 .option(SelectOption::new("Blue", "blue"))
318 .initial_option("blue"),
319 )
320 .radio_select(
321 RadioSelectElement::new("size", "T-Shirt Size")
322 .option(SelectOption::new("Small", "s"))
323 .option(SelectOption::new("Medium", "m"))
324 .option(SelectOption::new("Large", "l"))
325 .initial_option("m"),
326 );
327
328 assert_eq!(modal.submit_label.as_deref(), Some("Save"));
329 assert_eq!(modal.private_metadata.as_deref(), Some("{\"v\":1}"));
330 assert!(modal.notify_on_close);
331 assert_eq!(modal.children.len(), 3);
332
333 assert!(matches!(modal.children[0], ModalChild::TextInput(_)));
335 assert!(matches!(modal.children[1], ModalChild::Select(_)));
336 assert!(matches!(modal.children[2], ModalChild::RadioSelect(_)));
337
338 if let ModalChild::TextInput(ref input) = modal.children[0] {
340 assert_eq!(input.id, "name");
341 assert_eq!(input.label, "Your Name");
342 assert_eq!(input.placeholder.as_deref(), Some("Enter name"));
343 assert_eq!(input.initial_value.as_deref(), Some("Alice"));
344 assert!(!input.multiline);
345 assert!(!input.optional);
346 }
347
348 if let ModalChild::Select(ref select) = modal.children[1] {
350 assert_eq!(select.id, "color");
351 assert_eq!(select.options.len(), 2);
352 assert_eq!(select.options[0].label, "Red");
353 assert_eq!(select.options[0].value, "red");
354 assert_eq!(select.initial_option.as_deref(), Some("blue"));
355 }
356
357 if let ModalChild::RadioSelect(ref radio) = modal.children[2] {
359 assert_eq!(radio.id, "size");
360 assert_eq!(radio.options.len(), 3);
361 assert_eq!(radio.initial_option.as_deref(), Some("m"));
362 }
363 }
364
365 #[test]
366 fn serde_roundtrip() {
367 let original = ModalElement::new("cb_rt", "Roundtrip")
368 .submit_label("Go")
369 .notify_on_close(true)
370 .text_input(
371 TextInputElement::new("field1", "Field 1")
372 .placeholder("type here")
373 .multiline(true)
374 .optional(true),
375 )
376 .select(
377 SelectElement::new("sel1", "Select 1")
378 .option(SelectOption::new("A", "a"))
379 .initial_option("a"),
380 )
381 .radio_select(
382 RadioSelectElement::new("rad1", "Radio 1")
383 .option(SelectOption::new("X", "x"))
384 .option(SelectOption::new("Y", "y")),
385 );
386
387 let json = serde_json::to_string(&original).expect("serialize");
388 let restored: ModalElement = serde_json::from_str(&json).expect("deserialize");
389
390 assert_eq!(restored.callback_id, original.callback_id);
391 assert_eq!(restored.title, original.title);
392 assert_eq!(restored.submit_label, original.submit_label);
393 assert_eq!(restored.notify_on_close, original.notify_on_close);
394 assert_eq!(restored.children.len(), original.children.len());
395
396 if let (ModalChild::TextInput(orig), ModalChild::TextInput(rest)) =
398 (&original.children[0], &restored.children[0])
399 {
400 assert_eq!(orig.id, rest.id);
401 assert_eq!(orig.label, rest.label);
402 assert_eq!(orig.placeholder, rest.placeholder);
403 assert_eq!(orig.initial_value, rest.initial_value);
404 assert_eq!(orig.multiline, rest.multiline);
405 assert_eq!(orig.optional, rest.optional);
406 } else {
407 panic!("expected TextInput children");
408 }
409 }
410
411 #[test]
412 fn serde_roundtrip_empty_modal() {
413 let original = ModalElement::new("cb_empty", "Empty");
414 let json = serde_json::to_string(&original).expect("serialize");
415 let restored: ModalElement = serde_json::from_str(&json).expect("deserialize");
416
417 assert_eq!(restored.callback_id, "cb_empty");
418 assert_eq!(restored.title, "Empty");
419 assert!(restored.children.is_empty());
420 assert!(!restored.notify_on_close);
421 }
422}