use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModalElement {
pub callback_id: String,
pub title: String,
pub submit_label: Option<String>,
pub children: Vec<ModalChild>,
pub private_metadata: Option<String>,
pub notify_on_close: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModalChild {
TextInput(TextInputElement),
Select(SelectElement),
RadioSelect(RadioSelectElement),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextInputElement {
pub id: String,
pub label: String,
pub placeholder: Option<String>,
pub initial_value: Option<String>,
pub multiline: bool,
pub optional: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectElement {
pub id: String,
pub label: String,
pub placeholder: Option<String>,
pub options: Vec<SelectOption>,
pub initial_option: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectOption {
pub label: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RadioSelectElement {
pub id: String,
pub label: String,
pub options: Vec<SelectOption>,
pub initial_option: Option<String>,
}
impl ModalElement {
#[must_use]
pub fn new(callback_id: impl Into<String>, title: impl Into<String>) -> Self {
Self {
callback_id: callback_id.into(),
title: title.into(),
submit_label: None,
children: Vec::new(),
private_metadata: None,
notify_on_close: false,
}
}
#[must_use]
pub fn submit_label(mut self, label: impl Into<String>) -> Self {
self.submit_label = Some(label.into());
self
}
#[must_use]
pub fn text_input(mut self, input: TextInputElement) -> Self {
self.children.push(ModalChild::TextInput(input));
self
}
#[must_use]
pub fn select(mut self, select: SelectElement) -> Self {
self.children.push(ModalChild::Select(select));
self
}
#[must_use]
pub fn radio_select(mut self, radio: RadioSelectElement) -> Self {
self.children.push(ModalChild::RadioSelect(radio));
self
}
#[must_use]
pub fn private_metadata(mut self, metadata: impl Into<String>) -> Self {
self.private_metadata = Some(metadata.into());
self
}
#[must_use]
pub fn notify_on_close(mut self, notify: bool) -> Self {
self.notify_on_close = notify;
self
}
}
impl TextInputElement {
#[must_use]
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
placeholder: None,
initial_value: None,
multiline: false,
optional: false,
}
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn initial_value(mut self, value: impl Into<String>) -> Self {
self.initial_value = Some(value.into());
self
}
#[must_use]
pub fn multiline(mut self, multiline: bool) -> Self {
self.multiline = multiline;
self
}
#[must_use]
pub fn optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
}
impl SelectElement {
#[must_use]
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
placeholder: None,
options: Vec::new(),
initial_option: None,
}
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn option(mut self, option: SelectOption) -> Self {
self.options.push(option);
self
}
#[must_use]
pub fn initial_option(mut self, value: impl Into<String>) -> Self {
self.initial_option = Some(value.into());
self
}
}
impl SelectOption {
#[must_use]
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self { label: label.into(), value: value.into() }
}
}
impl RadioSelectElement {
#[must_use]
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self { id: id.into(), label: label.into(), options: Vec::new(), initial_option: None }
}
#[must_use]
pub fn option(mut self, option: SelectOption) -> Self {
self.options.push(option);
self
}
#[must_use]
pub fn initial_option(mut self, value: impl Into<String>) -> Self {
self.initial_option = Some(value.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_modal_construction() {
let modal = ModalElement::new("cb_1", "My Modal");
assert_eq!(modal.callback_id, "cb_1");
assert_eq!(modal.title, "My Modal");
assert!(modal.submit_label.is_none());
assert!(modal.children.is_empty());
assert!(modal.private_metadata.is_none());
assert!(!modal.notify_on_close);
}
#[test]
fn builder_chaining() {
let modal = ModalElement::new("cb_settings", "Settings")
.submit_label("Save")
.private_metadata("{\"v\":1}")
.notify_on_close(true)
.text_input(
TextInputElement::new("name", "Your Name")
.placeholder("Enter name")
.initial_value("Alice")
.multiline(false)
.optional(false),
)
.select(
SelectElement::new("color", "Favourite Colour")
.placeholder("Pick one")
.option(SelectOption::new("Red", "red"))
.option(SelectOption::new("Blue", "blue"))
.initial_option("blue"),
)
.radio_select(
RadioSelectElement::new("size", "T-Shirt Size")
.option(SelectOption::new("Small", "s"))
.option(SelectOption::new("Medium", "m"))
.option(SelectOption::new("Large", "l"))
.initial_option("m"),
);
assert_eq!(modal.submit_label.as_deref(), Some("Save"));
assert_eq!(modal.private_metadata.as_deref(), Some("{\"v\":1}"));
assert!(modal.notify_on_close);
assert_eq!(modal.children.len(), 3);
assert!(matches!(modal.children[0], ModalChild::TextInput(_)));
assert!(matches!(modal.children[1], ModalChild::Select(_)));
assert!(matches!(modal.children[2], ModalChild::RadioSelect(_)));
if let ModalChild::TextInput(ref input) = modal.children[0] {
assert_eq!(input.id, "name");
assert_eq!(input.label, "Your Name");
assert_eq!(input.placeholder.as_deref(), Some("Enter name"));
assert_eq!(input.initial_value.as_deref(), Some("Alice"));
assert!(!input.multiline);
assert!(!input.optional);
}
if let ModalChild::Select(ref select) = modal.children[1] {
assert_eq!(select.id, "color");
assert_eq!(select.options.len(), 2);
assert_eq!(select.options[0].label, "Red");
assert_eq!(select.options[0].value, "red");
assert_eq!(select.initial_option.as_deref(), Some("blue"));
}
if let ModalChild::RadioSelect(ref radio) = modal.children[2] {
assert_eq!(radio.id, "size");
assert_eq!(radio.options.len(), 3);
assert_eq!(radio.initial_option.as_deref(), Some("m"));
}
}
#[test]
fn serde_roundtrip() {
let original = ModalElement::new("cb_rt", "Roundtrip")
.submit_label("Go")
.notify_on_close(true)
.text_input(
TextInputElement::new("field1", "Field 1")
.placeholder("type here")
.multiline(true)
.optional(true),
)
.select(
SelectElement::new("sel1", "Select 1")
.option(SelectOption::new("A", "a"))
.initial_option("a"),
)
.radio_select(
RadioSelectElement::new("rad1", "Radio 1")
.option(SelectOption::new("X", "x"))
.option(SelectOption::new("Y", "y")),
);
let json = serde_json::to_string(&original).expect("serialize");
let restored: ModalElement = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.callback_id, original.callback_id);
assert_eq!(restored.title, original.title);
assert_eq!(restored.submit_label, original.submit_label);
assert_eq!(restored.notify_on_close, original.notify_on_close);
assert_eq!(restored.children.len(), original.children.len());
if let (ModalChild::TextInput(orig), ModalChild::TextInput(rest)) =
(&original.children[0], &restored.children[0])
{
assert_eq!(orig.id, rest.id);
assert_eq!(orig.label, rest.label);
assert_eq!(orig.placeholder, rest.placeholder);
assert_eq!(orig.initial_value, rest.initial_value);
assert_eq!(orig.multiline, rest.multiline);
assert_eq!(orig.optional, rest.optional);
} else {
panic!("expected TextInput children");
}
}
#[test]
fn serde_roundtrip_empty_modal() {
let original = ModalElement::new("cb_empty", "Empty");
let json = serde_json::to_string(&original).expect("serialize");
let restored: ModalElement = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.callback_id, "cb_empty");
assert_eq!(restored.title, "Empty");
assert!(restored.children.is_empty());
assert!(!restored.notify_on_close);
}
}