mzrs-sdk 0.1.21

High-level Rust SDK for Mezon platform
Documentation
//! Interactive component builders (buttons, selects, action rows).
//!
//! Components are rendered below message text and support user interaction
//! (button clicks, dropdown selections).

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

// ── Enums ───────────────────────────────────────────────────────────

/// Button appearance style.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
#[serde(into = "u8")]
pub enum ButtonStyle {
    /// Blue button, primary action.
    Primary = 1,
    /// Grey button, secondary action.
    Secondary = 2,
    /// Green button, success/confirm.
    Success = 3,
    /// Red button, destructive action.
    Danger = 4,
    /// Renders as a hyperlink; pass `url` when creating.
    Link = 5,
}

impl From<ButtonStyle> for u8 {
    fn from(s: ButtonStyle) -> u8 {
        s as u8
    }
}

/// Interactive component type tag.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
#[serde(into = "u8")]
pub enum ComponentType {
    /// Button component.
    Button = 1,
    /// Select / dropdown component.
    Select = 2,
    /// Text input component.
    Input = 3,
    /// Date picker component.
    DatePicker = 4,
    /// Radio button group.
    Radio = 5,
    /// Animation component.
    Animation = 6,
    /// Grid component.
    Grid = 7,
}

impl From<ComponentType> for u8 {
    fn from(t: ComponentType) -> u8 {
        t as u8
    }
}

/// A select / dropdown option.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SelectOption {
    /// Display text.
    pub label: String,
    /// Value submitted on selection.
    pub value: String,
}

impl SelectOption {
    /// Create a new select option.
    pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            value: value.into(),
        }
    }
}

// ── Internal row component ──────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
struct RowComponent {
    id: String,
    #[serde(rename = "type")]
    kind: ComponentType,
    component: Value,
}

// ── ActionRow ───────────────────────────────────────────────────────

/// A row of interactive components (buttons, selects) rendered below
/// the message text.
///
/// # Example
///
/// ```rust
/// use mzrs_sdk::{ActionRow, ButtonStyle};
///
/// let row = ActionRow::new()
///     .button("ok", "OK", ButtonStyle::Primary)
///     .button("cancel", "Cancel", ButtonStyle::Secondary);
///
/// let json = row.build();
/// assert!(json.is_object());
/// ```
#[derive(Debug, Clone, Default)]
pub struct ActionRow {
    items: Vec<RowComponent>,
}

impl ActionRow {
    /// Create an empty action row.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a button component.
    pub fn button(
        mut self,
        id: impl Into<String>,
        label: impl Into<String>,
        style: ButtonStyle,
    ) -> Self {
        let comp = json!({ "label": label.into(), "style": style as u8 });
        self.items.push(RowComponent {
            id: id.into(),
            kind: ComponentType::Button,
            component: comp,
        });
        self
    }

    /// Add a link button (opens a URL).
    pub fn link_button(
        mut self,
        id: impl Into<String>,
        label: impl Into<String>,
        url: impl Into<String>,
    ) -> Self {
        let comp = json!({
            "label": label.into(),
            "style": ButtonStyle::Link as u8,
            "url": url.into()
        });
        self.items.push(RowComponent {
            id: id.into(),
            kind: ComponentType::Button,
            component: comp,
        });
        self
    }

    /// Add a select / dropdown component.
    pub fn select(mut self, id: impl Into<String>, options: Vec<SelectOption>) -> Self {
        self.items.push(RowComponent {
            id: id.into(),
            kind: ComponentType::Select,
            component: json!({ "options": options }),
        });
        self
    }

    /// Serialise the action row to a JSON [`Value`].
    pub fn build(self) -> Value {
        serde_json::to_value(&ActionRowSer {
            components: self.items,
        })
        .unwrap_or(Value::Null)
    }
}

/// Internal serialization wrapper.
#[derive(Serialize)]
struct ActionRowSer {
    components: Vec<RowComponent>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn action_row_with_buttons() {
        let row = ActionRow::new()
            .button("ok", "OK", ButtonStyle::Primary)
            .button("no", "Cancel", ButtonStyle::Danger);

        let v = row.build();
        let components = v["components"].as_array().unwrap();
        assert_eq!(components.len(), 2);
        assert_eq!(components[0]["component"]["label"], "OK");
    }

    #[test]
    fn action_row_with_select() {
        let row = ActionRow::new().select(
            "color",
            vec![
                SelectOption::new("Red", "red"),
                SelectOption::new("Blue", "blue"),
            ],
        );

        let v = row.build();
        let components = v["components"].as_array().unwrap();
        assert_eq!(components.len(), 1);
        let opts = components[0]["component"]["options"].as_array().unwrap();
        assert_eq!(opts.len(), 2);
    }
}