use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ButtonStyle {
Primary = 1,
Secondary = 2,
Success = 3,
Danger = 4,
Link = 5,
}
#[derive(Debug, Clone)]
pub struct CreateButton {
style: ButtonStyle,
custom_id: Option<String>,
url: Option<String>,
label: Option<String>,
emoji: Option<Value>,
disabled: bool,
}
impl CreateButton {
pub fn new(custom_id: impl Into<String>, style: ButtonStyle) -> Self {
Self { style, custom_id: Some(custom_id.into()), url: None, label: None, emoji: None, disabled: false }
}
pub fn link(url: impl Into<String>) -> Self {
Self { style: ButtonStyle::Link, custom_id: None, url: Some(url.into()), label: None, emoji: None, disabled: false }
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn emoji(mut self, emoji: Value) -> Self {
self.emoji = Some(emoji);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn to_json(&self) -> Value {
let mut obj = json!({
"type": 2,
"style": self.style as u8,
"disabled": self.disabled,
});
if let Some(ref id) = self.custom_id {
obj["custom_id"] = json!(id);
}
if let Some(ref url) = self.url {
obj["url"] = json!(url);
}
if let Some(ref label) = self.label {
obj["label"] = json!(label);
}
if let Some(ref emoji) = self.emoji {
obj["emoji"] = emoji.clone();
}
obj
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateSelectMenuOption {
pub label: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub emoji: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl CreateSelectMenuOption {
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self { label: label.into(), value: value.into(), description: None, emoji: None, default: None }
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn default_selection(mut self, is_default: bool) -> Self {
self.default = Some(is_default);
self
}
}
#[derive(Debug, Clone)]
pub struct CreateSelectMenu {
custom_id: String,
placeholder: Option<String>,
min_values: Option<u8>,
max_values: Option<u8>,
disabled: bool,
options: Vec<CreateSelectMenuOption>,
}
impl CreateSelectMenu {
pub fn new(custom_id: impl Into<String>) -> Self {
Self {
custom_id: custom_id.into(),
placeholder: None,
min_values: None,
max_values: None,
disabled: false,
options: Vec::new(),
}
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn min_values(mut self, min: u8) -> Self {
self.min_values = Some(min);
self
}
pub fn max_values(mut self, max: u8) -> Self {
self.max_values = Some(max);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn add_option(mut self, option: CreateSelectMenuOption) -> Self {
self.options.push(option);
self
}
pub fn options(mut self, options: Vec<CreateSelectMenuOption>) -> Self {
self.options = options;
self
}
pub fn to_json(&self) -> Value {
let mut obj = json!({
"type": 3, "custom_id": self.custom_id,
"options": self.options,
"disabled": self.disabled,
});
if let Some(ref ph) = self.placeholder {
obj["placeholder"] = json!(ph);
}
if let Some(min) = self.min_values {
obj["min_values"] = json!(min);
}
if let Some(max) = self.max_values {
obj["max_values"] = json!(max);
}
obj
}
}
#[derive(Debug, Clone)]
pub enum CreateActionRow {
Buttons(Vec<CreateButton>),
SelectMenu(CreateSelectMenu),
}
impl CreateActionRow {
pub fn buttons(buttons: Vec<CreateButton>) -> Self {
Self::Buttons(buttons)
}
pub fn select_menu(menu: CreateSelectMenu) -> Self {
Self::SelectMenu(menu)
}
pub fn to_json(&self) -> Value {
match self {
Self::Buttons(buttons) => json!({
"type": 1,
"components": buttons.iter().map(|b| b.to_json()).collect::<Vec<_>>(),
}),
Self::SelectMenu(menu) => json!({
"type": 1,
"components": [menu.to_json()],
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum ComponentType {
ActionRow = 1,
Button = 2,
StringSelect = 3,
TextInput = 4,
UserSelect = 5,
RoleSelect = 6,
MentionableSelect = 7,
ChannelSelect = 8,
Section = 9,
TextDisplay = 10,
Thumbnail = 11,
MediaGallery = 12,
File = 13,
Separator = 14,
Container = 17,
Label = 18,
FileUpload = 19,
RadioGroup = 21,
CheckboxGroup = 22,
Checkbox = 23,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DefaultValue {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
}
impl DefaultValue {
pub fn user(id: impl Into<String>) -> Self {
Self { id: id.into(), type_: "user".into() }
}
pub fn role(id: impl Into<String>) -> Self {
Self { id: id.into(), type_: "role".into() }
}
pub fn channel(id: impl Into<String>) -> Self {
Self { id: id.into(), type_: "channel".into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UnfurledMediaItem {
pub url: String,
}
impl UnfurledMediaItem {
pub fn new(url: impl Into<String>) -> Self {
Self { url: url.into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MediaGalleryItem {
pub media: UnfurledMediaItem,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spoiler: Option<bool>,
}
impl MediaGalleryItem {
pub fn new(url: impl Into<String>) -> Self {
Self { media: UnfurledMediaItem::new(url), description: None, spoiler: None }
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn spoiler(mut self, spoiler: bool) -> Self {
self.spoiler = Some(spoiler);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RadioOption {
pub label: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl RadioOption {
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self { label: label.into(), value: value.into(), description: None, default: None }
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn default_selection(mut self, is_default: bool) -> Self {
self.default = Some(is_default);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Component {
ActionRow {
components: Vec<Component>,
id: Option<u32>,
},
UserSelect {
custom_id: String,
placeholder: Option<String>,
default_values: Option<Vec<DefaultValue>>,
min_values: Option<u8>,
max_values: Option<u8>,
disabled: Option<bool>,
},
RoleSelect {
custom_id: String,
placeholder: Option<String>,
default_values: Option<Vec<DefaultValue>>,
min_values: Option<u8>,
max_values: Option<u8>,
disabled: Option<bool>,
},
MentionableSelect {
custom_id: String,
placeholder: Option<String>,
default_values: Option<Vec<DefaultValue>>,
min_values: Option<u8>,
max_values: Option<u8>,
disabled: Option<bool>,
},
ChannelSelect {
custom_id: String,
placeholder: Option<String>,
channel_types: Option<Vec<u8>>,
default_values: Option<Vec<DefaultValue>>,
min_values: Option<u8>,
max_values: Option<u8>,
disabled: Option<bool>,
},
Section {
components: Vec<Component>,
accessory: Option<Box<Component>>,
id: Option<u32>,
},
TextDisplay {
content: String,
id: Option<u32>,
},
Thumbnail {
media: UnfurledMediaItem,
description: Option<String>,
spoiler: Option<bool>,
id: Option<u32>,
},
MediaGallery {
items: Vec<MediaGalleryItem>,
id: Option<u32>,
},
File {
file: UnfurledMediaItem,
spoiler: Option<bool>,
id: Option<u32>,
},
Separator {
divider: Option<bool>,
spacing: Option<u8>,
id: Option<u32>,
},
Container {
components: Vec<Component>,
accent_color: Option<u32>,
spoiler: Option<bool>,
id: Option<u32>,
},
Label {
label: String,
description: Option<String>,
component: Box<Component>,
id: Option<u32>,
},
FileUpload {
custom_id: String,
min_values: Option<u8>,
max_values: Option<u8>,
required: Option<bool>,
id: Option<u32>,
},
RadioGroup {
custom_id: String,
options: Vec<RadioOption>,
required: Option<bool>,
id: Option<u32>,
},
CheckboxGroup {
custom_id: String,
options: Vec<RadioOption>,
min_values: Option<u8>,
max_values: Option<u8>,
id: Option<u32>,
},
Checkbox {
custom_id: String,
label: String,
description: Option<String>,
default: Option<bool>,
id: Option<u32>,
},
}
impl Serialize for Component {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut obj = serde_json::Map::new();
let insert_opt = |obj: &mut serde_json::Map<String, Value>, k: &str, v: Option<Value>| {
if let Some(v) = v {
obj.insert(k.to_string(), v);
}
};
match self {
Component::ActionRow { components, id } => {
obj.insert("type".into(), json!(ComponentType::ActionRow as u8));
obj.insert("components".into(), serde_json::to_value(components).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::UserSelect { custom_id, placeholder, default_values, min_values, max_values, disabled } => {
obj.insert("type".into(), json!(ComponentType::UserSelect as u8));
obj.insert("custom_id".into(), json!(custom_id));
insert_opt(&mut obj, "placeholder", placeholder.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "default_values", default_values.as_ref().map(|x| serde_json::to_value(x).unwrap()));
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "disabled", disabled.as_ref().map(|x| json!(x)));
}
Component::RoleSelect { custom_id, placeholder, default_values, min_values, max_values, disabled } => {
obj.insert("type".into(), json!(ComponentType::RoleSelect as u8));
obj.insert("custom_id".into(), json!(custom_id));
insert_opt(&mut obj, "placeholder", placeholder.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "default_values", default_values.as_ref().map(|x| serde_json::to_value(x).unwrap()));
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "disabled", disabled.as_ref().map(|x| json!(x)));
}
Component::MentionableSelect { custom_id, placeholder, default_values, min_values, max_values, disabled } => {
obj.insert("type".into(), json!(ComponentType::MentionableSelect as u8));
obj.insert("custom_id".into(), json!(custom_id));
insert_opt(&mut obj, "placeholder", placeholder.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "default_values", default_values.as_ref().map(|x| serde_json::to_value(x).unwrap()));
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "disabled", disabled.as_ref().map(|x| json!(x)));
}
Component::ChannelSelect { custom_id, placeholder, channel_types, default_values, min_values, max_values, disabled } => {
obj.insert("type".into(), json!(ComponentType::ChannelSelect as u8));
obj.insert("custom_id".into(), json!(custom_id));
insert_opt(&mut obj, "placeholder", placeholder.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "channel_types", channel_types.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "default_values", default_values.as_ref().map(|x| serde_json::to_value(x).unwrap()));
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "disabled", disabled.as_ref().map(|x| json!(x)));
}
Component::Section { components, accessory, id } => {
obj.insert("type".into(), json!(ComponentType::Section as u8));
obj.insert("components".into(), serde_json::to_value(components).map_err(serde::ser::Error::custom)?);
if let Some(a) = accessory {
obj.insert("accessory".into(), serde_json::to_value(a.as_ref()).map_err(serde::ser::Error::custom)?);
}
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::TextDisplay { content, id } => {
obj.insert("type".into(), json!(ComponentType::TextDisplay as u8));
obj.insert("content".into(), json!(content));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::Thumbnail { media, description, spoiler, id } => {
obj.insert("type".into(), json!(ComponentType::Thumbnail as u8));
obj.insert("media".into(), serde_json::to_value(media).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "description", description.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "spoiler", spoiler.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::MediaGallery { items, id } => {
obj.insert("type".into(), json!(ComponentType::MediaGallery as u8));
obj.insert("items".into(), serde_json::to_value(items).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::File { file, spoiler, id } => {
obj.insert("type".into(), json!(ComponentType::File as u8));
obj.insert("file".into(), serde_json::to_value(file).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "spoiler", spoiler.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::Separator { divider, spacing, id } => {
obj.insert("type".into(), json!(ComponentType::Separator as u8));
insert_opt(&mut obj, "divider", divider.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "spacing", spacing.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::Container { components, accent_color, spoiler, id } => {
obj.insert("type".into(), json!(ComponentType::Container as u8));
obj.insert("components".into(), serde_json::to_value(components).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "accent_color", accent_color.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "spoiler", spoiler.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::Label { label, description, component, id } => {
obj.insert("type".into(), json!(ComponentType::Label as u8));
obj.insert("label".into(), json!(label));
insert_opt(&mut obj, "description", description.as_ref().map(|x| json!(x)));
obj.insert("component".into(), serde_json::to_value(component.as_ref()).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::FileUpload { custom_id, min_values, max_values, required, id } => {
obj.insert("type".into(), json!(ComponentType::FileUpload as u8));
obj.insert("custom_id".into(), json!(custom_id));
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "required", required.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::RadioGroup { custom_id, options, required, id } => {
obj.insert("type".into(), json!(ComponentType::RadioGroup as u8));
obj.insert("custom_id".into(), json!(custom_id));
obj.insert("options".into(), serde_json::to_value(options).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "required", required.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::CheckboxGroup { custom_id, options, min_values, max_values, id } => {
obj.insert("type".into(), json!(ComponentType::CheckboxGroup as u8));
obj.insert("custom_id".into(), json!(custom_id));
obj.insert("options".into(), serde_json::to_value(options).map_err(serde::ser::Error::custom)?);
insert_opt(&mut obj, "min_values", min_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "max_values", max_values.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
Component::Checkbox { custom_id, label, description, default, id } => {
obj.insert("type".into(), json!(ComponentType::Checkbox as u8));
obj.insert("custom_id".into(), json!(custom_id));
obj.insert("label".into(), json!(label));
insert_opt(&mut obj, "description", description.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "default", default.as_ref().map(|x| json!(x)));
insert_opt(&mut obj, "id", id.as_ref().map(|x| json!(x)));
}
}
Value::Object(obj).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Component {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = Value::deserialize(deserializer)?;
let obj = v.as_object().ok_or_else(|| serde::de::Error::custom("expected component object"))?;
let ty = obj.get("type")
.and_then(|v| v.as_u64())
.ok_or_else(|| serde::de::Error::custom("missing/invalid integer `type`"))? as u8;
let take_str = |k: &str| -> Result<String, D::Error> {
obj.get(k).and_then(|v| v.as_str()).map(String::from)
.ok_or_else(|| serde::de::Error::custom(format!("missing string field `{k}`")))
};
let opt_str = |k: &str| obj.get(k).and_then(|v| v.as_str()).map(String::from);
let opt_u8 = |k: &str| obj.get(k).and_then(|v| v.as_u64()).map(|x| x as u8);
let opt_u32 = |k: &str| obj.get(k).and_then(|v| v.as_u64()).map(|x| x as u32);
let opt_bool = |k: &str| obj.get(k).and_then(|v| v.as_bool());
let opt_default_values = |k: &str| -> Result<Option<Vec<DefaultValue>>, D::Error> {
obj.get(k).map(|v| serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)).transpose()
};
let opt_components = |k: &str| -> Result<Vec<Component>, D::Error> {
obj.get(k)
.map(|v| serde_json::from_value::<Vec<Component>>(v.clone()).map_err(serde::de::Error::custom))
.transpose()?
.ok_or_else(|| serde::de::Error::custom(format!("missing `{k}` array")))
};
let take_media = |k: &str| -> Result<UnfurledMediaItem, D::Error> {
obj.get(k)
.map(|v| serde_json::from_value::<UnfurledMediaItem>(v.clone()).map_err(serde::de::Error::custom))
.ok_or_else(|| serde::de::Error::custom(format!("missing `{k}` object")))?
};
let opt_channel_types = |k: &str| -> Option<Vec<u8>> {
obj.get(k).and_then(|v| v.as_array()).map(|arr| {
arr.iter().filter_map(|x| x.as_u64().map(|y| y as u8)).collect()
})
};
let comp = match ty {
1 => Component::ActionRow { components: opt_components("components")?, id: opt_u32("id") },
5 => Component::UserSelect {
custom_id: take_str("custom_id")?,
placeholder: opt_str("placeholder"),
default_values: opt_default_values("default_values")?,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
disabled: opt_bool("disabled"),
},
6 => Component::RoleSelect {
custom_id: take_str("custom_id")?,
placeholder: opt_str("placeholder"),
default_values: opt_default_values("default_values")?,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
disabled: opt_bool("disabled"),
},
7 => Component::MentionableSelect {
custom_id: take_str("custom_id")?,
placeholder: opt_str("placeholder"),
default_values: opt_default_values("default_values")?,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
disabled: opt_bool("disabled"),
},
8 => Component::ChannelSelect {
custom_id: take_str("custom_id")?,
placeholder: opt_str("placeholder"),
channel_types: opt_channel_types("channel_types"),
default_values: opt_default_values("default_values")?,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
disabled: opt_bool("disabled"),
},
9 => {
let accessory = obj.get("accessory")
.map(|v| serde_json::from_value::<Component>(v.clone()).map_err(serde::de::Error::custom))
.transpose()?
.map(Box::new);
Component::Section {
components: opt_components("components")?,
accessory,
id: opt_u32("id"),
}
}
10 => Component::TextDisplay { content: take_str("content")?, id: opt_u32("id") },
11 => Component::Thumbnail {
media: take_media("media")?,
description: opt_str("description"),
spoiler: opt_bool("spoiler"),
id: opt_u32("id"),
},
12 => {
let items = obj.get("items")
.map(|v| serde_json::from_value::<Vec<MediaGalleryItem>>(v.clone()).map_err(serde::de::Error::custom))
.transpose()?
.ok_or_else(|| serde::de::Error::custom("missing `items` array"))?;
Component::MediaGallery { items, id: opt_u32("id") }
}
13 => Component::File { file: take_media("file")?, spoiler: opt_bool("spoiler"), id: opt_u32("id") },
14 => Component::Separator { divider: opt_bool("divider"), spacing: opt_u8("spacing"), id: opt_u32("id") },
17 => Component::Container {
components: opt_components("components")?,
accent_color: opt_u32("accent_color"),
spoiler: opt_bool("spoiler"),
id: opt_u32("id"),
},
18 => {
let component = obj.get("component")
.map(|v| serde_json::from_value::<Component>(v.clone()).map_err(serde::de::Error::custom))
.ok_or_else(|| serde::de::Error::custom("missing `component` object"))??;
Component::Label {
label: take_str("label")?,
description: opt_str("description"),
component: Box::new(component),
id: opt_u32("id"),
}
}
19 => Component::FileUpload {
custom_id: take_str("custom_id")?,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
required: opt_bool("required"),
id: opt_u32("id"),
},
21 => {
let options = obj.get("options")
.map(|v| serde_json::from_value::<Vec<RadioOption>>(v.clone()).map_err(serde::de::Error::custom))
.transpose()?
.ok_or_else(|| serde::de::Error::custom("missing `options` array"))?;
Component::RadioGroup {
custom_id: take_str("custom_id")?,
options,
required: opt_bool("required"),
id: opt_u32("id"),
}
}
22 => {
let options = obj.get("options")
.map(|v| serde_json::from_value::<Vec<RadioOption>>(v.clone()).map_err(serde::de::Error::custom))
.transpose()?
.ok_or_else(|| serde::de::Error::custom("missing `options` array"))?;
Component::CheckboxGroup {
custom_id: take_str("custom_id")?,
options,
min_values: opt_u8("min_values"),
max_values: opt_u8("max_values"),
id: opt_u32("id"),
}
}
23 => Component::Checkbox {
custom_id: take_str("custom_id")?,
label: take_str("label")?,
description: opt_str("description"),
default: opt_bool("default"),
id: opt_u32("id"),
},
other => return Err(serde::de::Error::custom(format!("unsupported component type tag: {other}"))),
};
Ok(comp)
}
}
impl Component {
pub fn text_display(content: impl Into<String>) -> Self {
Self::TextDisplay { content: content.into(), id: None }
}
pub fn section(components: Vec<Component>) -> Self {
Self::Section { components, accessory: None, id: None }
}
pub fn section_with_accessory(components: Vec<Component>, accessory: Component) -> Self {
Self::Section { components, accessory: Some(Box::new(accessory)), id: None }
}
pub fn thumbnail(url: impl Into<String>) -> Self {
Self::Thumbnail {
media: UnfurledMediaItem::new(url),
description: None,
spoiler: None,
id: None,
}
}
pub fn media_gallery(items: Vec<MediaGalleryItem>) -> Self {
Self::MediaGallery { items, id: None }
}
pub fn file(url: impl Into<String>) -> Self {
Self::File { file: UnfurledMediaItem::new(url), spoiler: None, id: None }
}
pub fn separator(divider: Option<bool>, spacing: Option<u8>) -> Self {
Self::Separator { divider, spacing, id: None }
}
pub fn container(components: Vec<Component>) -> Self {
Self::Container { components, accent_color: None, spoiler: None, id: None }
}
pub fn label(label: impl Into<String>, component: Component) -> Self {
Self::Label {
label: label.into(),
description: None,
component: Box::new(component),
id: None,
}
}
pub fn file_upload(custom_id: impl Into<String>) -> Self {
Self::FileUpload {
custom_id: custom_id.into(),
min_values: None,
max_values: None,
required: None,
id: None,
}
}
pub fn radio_group(custom_id: impl Into<String>, options: Vec<RadioOption>) -> Self {
Self::RadioGroup {
custom_id: custom_id.into(),
options,
required: None,
id: None,
}
}
pub fn checkbox_group(custom_id: impl Into<String>, options: Vec<RadioOption>) -> Self {
Self::CheckboxGroup {
custom_id: custom_id.into(),
options,
min_values: None,
max_values: None,
id: None,
}
}
pub fn checkbox(custom_id: impl Into<String>, label: impl Into<String>) -> Self {
Self::Checkbox {
custom_id: custom_id.into(),
label: label.into(),
description: None,
default: None,
id: None,
}
}
}
#[cfg(test)]
mod component_tests {
use super::*;
use serde_json::json;
#[test]
fn text_display_serializes_with_type_tag() {
let c = Component::text_display("hello **world**");
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v["type"], 10);
assert_eq!(v["content"], "hello **world**");
}
#[test]
fn user_select_round_trips() {
let c = Component::UserSelect {
custom_id: "pick_user".into(),
placeholder: Some("Pick someone".into()),
default_values: Some(vec![DefaultValue::user("123")]),
min_values: Some(1),
max_values: Some(3),
disabled: Some(false),
};
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v["type"], 5);
assert_eq!(v["custom_id"], "pick_user");
assert_eq!(v["default_values"][0]["type"], "user");
let back: Component = serde_json::from_value(v).unwrap();
assert_eq!(back, c);
}
#[test]
fn channel_select_carries_channel_types() {
let c = Component::ChannelSelect {
custom_id: "chan".into(),
placeholder: None,
channel_types: Some(vec![0, 5]),
default_values: None,
min_values: None,
max_values: None,
disabled: None,
};
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v["type"], 8);
assert_eq!(v["channel_types"], json!([0, 5]));
}
#[test]
fn section_with_accessory_thumbnail() {
let s = Component::section_with_accessory(
vec![Component::text_display("Hello there")],
Component::thumbnail("https://example.com/x.png"),
);
let v = serde_json::to_value(&s).unwrap();
assert_eq!(v["type"], 9);
assert_eq!(v["components"][0]["type"], 10);
assert_eq!(v["accessory"]["type"], 11);
assert_eq!(v["accessory"]["media"]["url"], "https://example.com/x.png");
}
#[test]
fn container_with_separator_and_gallery() {
let c = Component::container(vec![
Component::text_display("Header"),
Component::separator(Some(true), Some(2)),
Component::media_gallery(vec![
MediaGalleryItem::new("https://example.com/1.png").description("first"),
MediaGalleryItem::new("https://example.com/2.png").spoiler(true),
]),
]);
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v["type"], 17);
assert_eq!(v["components"][1]["type"], 14);
assert_eq!(v["components"][1]["divider"], true);
assert_eq!(v["components"][2]["type"], 12);
assert_eq!(v["components"][2]["items"][1]["spoiler"], true);
}
#[test]
fn component_type_repr_values() {
assert_eq!(ComponentType::ActionRow as u8, 1);
assert_eq!(ComponentType::UserSelect as u8, 5);
assert_eq!(ComponentType::ChannelSelect as u8, 8);
assert_eq!(ComponentType::Section as u8, 9);
assert_eq!(ComponentType::Container as u8, 17);
}
}