use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
use serde_json::value::RawValue;
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::{ChannelType, PartialEmoji, Snowflake, TypeField};
pub type StringSelect = SelectMenu<3>;
pub type UserSelect = SelectMenu<5>;
pub type RoleSelect = SelectMenu<6>;
pub type MentionableSelect = SelectMenu<7>;
pub type ChannelSelect = SelectMenu<8>;
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum Component {
ActionRow(ActionRow),
Button(ButtonComponent),
StringSelect(StringSelect),
TextInput(TextInput),
UserSelect(UserSelect),
RoleSelect(RoleSelect),
MentionableSelect(MentionableSelect),
ChannelSelect(ChannelSelect),
Section(Section),
TextDisplay(TextDisplay),
Thumbnail(Thumbnail),
MediaGallery(MediaGallery),
File(FileComponent),
Separator(Separator),
Container(Container),
Label(Label),
FileUpload(FileUpload),
RadioGroup(RadioGroup),
CheckboxGroup(CheckboxGroup),
Checkbox(Checkbox),
Unknown(Box<RawValue>),
}
impl Component {
pub fn row(components: Vec<Component>) -> Component {
Component::ActionRow(ActionRow::new(components))
}
pub fn container(components: Vec<Component>) -> Component {
Component::Container(Container::new(components))
}
pub fn section(components: Vec<Component>, accessory: Component) -> Component {
Component::Section(Section::new(components, accessory))
}
pub fn thumbnail(url: impl Into<String>) -> Component {
Component::Thumbnail(Thumbnail::new(url))
}
pub fn gallery(items: Vec<MediaGalleryItem>) -> Component {
Component::MediaGallery(MediaGallery::new(items))
}
pub fn file(url: impl Into<String>) -> Component {
Component::File(FileComponent::new(url))
}
pub fn separator() -> Component {
Component::Separator(Separator::new())
}
pub fn id(&self) -> Option<i32> {
match self {
Component::ActionRow(c) => c.id,
Component::Button(c) => c.id,
Component::StringSelect(c) => c.id,
Component::TextInput(c) => c.id,
Component::UserSelect(c) => c.id,
Component::RoleSelect(c) => c.id,
Component::MentionableSelect(c) => c.id,
Component::ChannelSelect(c) => c.id,
Component::Section(c) => c.id,
Component::TextDisplay(c) => c.id,
Component::Thumbnail(c) => c.id,
Component::MediaGallery(c) => c.id,
Component::File(c) => c.id,
Component::Separator(c) => c.id,
Component::Container(c) => c.id,
Component::Label(c) => c.id,
Component::FileUpload(c) => c.id,
Component::RadioGroup(c) => c.id,
Component::CheckboxGroup(c) => c.id,
Component::Checkbox(c) => c.id,
Component::Unknown(raw) => {
#[derive(Deserialize)]
struct Id {
id: Option<i64>,
}
serde_json::from_slice::<Id>(raw.get().as_bytes())
.ok()?
.id
.and_then(|id| i32::try_from(id).ok())
}
}
}
pub fn with_id(mut self, id: i32) -> Component {
let slot = match &mut self {
Component::ActionRow(c) => &mut c.id,
Component::Button(c) => &mut c.id,
Component::StringSelect(c) => &mut c.id,
Component::TextInput(c) => &mut c.id,
Component::UserSelect(c) => &mut c.id,
Component::RoleSelect(c) => &mut c.id,
Component::MentionableSelect(c) => &mut c.id,
Component::ChannelSelect(c) => &mut c.id,
Component::Section(c) => &mut c.id,
Component::TextDisplay(c) => &mut c.id,
Component::Thumbnail(c) => &mut c.id,
Component::MediaGallery(c) => &mut c.id,
Component::File(c) => &mut c.id,
Component::Separator(c) => &mut c.id,
Component::Container(c) => &mut c.id,
Component::Label(c) => &mut c.id,
Component::FileUpload(c) => &mut c.id,
Component::RadioGroup(c) => &mut c.id,
Component::CheckboxGroup(c) => &mut c.id,
Component::Checkbox(c) => &mut c.id,
Component::Unknown(_) => return self,
};
*slot = Some(id);
self
}
pub fn label(label: impl Into<String>, component: Component) -> Component {
Component::Label(Label::new(label, component))
}
pub fn text(content: impl Into<String>) -> Component {
Component::TextDisplay(TextDisplay::new(content))
}
pub fn new_button(
style: ButtonStyle,
label: Option<String>,
emoji: Option<PartialEmoji>,
custom_id: Option<String>,
url: Option<String>,
disabled: Option<bool>,
) -> Component {
Self::Button(ButtonComponent::new(
style, label, emoji, custom_id, url, disabled,
))
}
pub fn new_string_select(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Component {
Self::StringSelect(SelectMenu::new(
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
))
}
#[allow(clippy::too_many_arguments)]
pub fn new_text_input(
custom_id: String,
style: TextInputStyle,
label: String,
min_length: Option<i32>,
max_length: Option<i32>,
required: Option<bool>,
value: Option<String>,
placeholder: Option<String>,
) -> Component {
Self::TextInput(TextInput::new(
custom_id,
style,
label,
min_length,
max_length,
required,
value,
placeholder,
))
}
pub fn new_user_select(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Component {
Self::UserSelect(SelectMenu::new(
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
))
}
pub fn new_role_select(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Component {
Self::RoleSelect(SelectMenu::new(
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
))
}
pub fn new_mentionable_select(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Component {
Self::MentionableSelect(SelectMenu::new(
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
))
}
pub fn new_channel_select(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Component {
Self::ChannelSelect(SelectMenu::new(
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
))
}
}
impl<'de> Deserialize<'de> for Component {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = Box::<RawValue>::deserialize(deserializer)?;
Component::from_raw(raw).map_err(serde::de::Error::custom)
}
}
#[derive(Deserialize)]
struct ComponentType {
#[serde(rename = "type")]
t: u64,
}
impl Component {
fn from_raw(raw: Box<RawValue>) -> serde_json::Result<Self> {
fn parse<T: DeserializeOwned>(raw: &RawValue) -> serde_json::Result<T> {
serde_json::from_slice(raw.get().as_bytes())
}
let ComponentType { t } = parse(&raw)?;
Ok(match t {
1 => Component::ActionRow(parse(&raw)?),
2 => Component::Button(parse(&raw)?),
3 => Component::StringSelect(parse(&raw)?),
4 => Component::TextInput(parse(&raw)?),
5 => Component::UserSelect(parse(&raw)?),
6 => Component::RoleSelect(parse(&raw)?),
7 => Component::MentionableSelect(parse(&raw)?),
8 => Component::ChannelSelect(parse(&raw)?),
9 => Component::Section(parse(&raw)?),
10 => Component::TextDisplay(parse(&raw)?),
11 => Component::Thumbnail(parse(&raw)?),
12 => Component::MediaGallery(parse(&raw)?),
13 => Component::File(parse(&raw)?),
14 => Component::Separator(parse(&raw)?),
17 => Component::Container(parse(&raw)?),
18 => Component::Label(parse(&raw)?),
19 => Component::FileUpload(parse(&raw)?),
21 => Component::RadioGroup(parse(&raw)?),
22 => Component::CheckboxGroup(parse(&raw)?),
23 => Component::Checkbox(parse(&raw)?),
_ => Component::Unknown(raw),
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ActionRow {
#[serde(rename = "type")]
pub t: TypeField<1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub components: Vec<Component>,
}
impl ActionRow {
pub fn new(components: Vec<Component>) -> Self {
Self {
t: TypeField,
id: None,
components,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ButtonComponent {
#[serde(rename = "type")]
pub t: TypeField<2>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub style: ButtonStyle,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emoji: Option<PartialEmoji>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sku_id: Option<Snowflake>,
}
impl ButtonComponent {
pub fn new(
style: ButtonStyle,
label: Option<String>,
emoji: Option<PartialEmoji>,
custom_id: Option<String>,
url: Option<String>,
disabled: Option<bool>,
) -> Self {
Self {
t: TypeField,
id: None,
style,
label,
emoji,
custom_id,
url,
disabled,
sku_id: None,
}
}
pub fn premium(sku_id: Snowflake) -> Self {
Self {
sku_id: Some(sku_id),
..Self::new(ButtonStyle::Premium, None, None, None, None, None)
}
}
}
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u8)]
pub enum ButtonStyle {
Primary = 1,
Secondary = 2,
Success = 3,
Danger = 4,
Link = 5,
Premium = 6,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SelectMenu<const T: u8> {
#[serde(rename = "type")]
pub t: TypeField<T>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<SelectOption>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_types: Option<Vec<ChannelType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_values: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_values: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_values: Option<Vec<SelectDefaultValue>>,
}
impl<const T: u8> SelectMenu<T> {
pub fn new(
custom_id: String,
options: Option<Vec<SelectOption>>,
channel_types: Option<Vec<ChannelType>>,
placeholder: Option<String>,
min_values: Option<i32>,
max_values: Option<i32>,
disabled: Option<bool>,
) -> Self {
Self {
t: TypeField,
id: None,
custom_id,
options,
channel_types,
placeholder,
min_values,
max_values,
disabled,
required: None,
default_values: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SelectDefaultValue {
pub id: Snowflake,
#[serde(rename = "type")]
pub kind: String,
}
impl SelectDefaultValue {
pub fn user(id: Snowflake) -> Self {
Self::of(id, "user")
}
pub fn role(id: Snowflake) -> Self {
Self::of(id, "role")
}
pub fn channel(id: Snowflake) -> Self {
Self::of(id, "channel")
}
fn of(id: Snowflake, kind: &str) -> Self {
Self {
id,
kind: kind.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SelectOption {
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<PartialEmoji>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl SelectOption {
pub fn new(
label: String,
value: String,
description: Option<String>,
emoji: Option<PartialEmoji>,
default: Option<bool>,
) -> Self {
Self {
label,
value,
description,
emoji,
default,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TextInput {
#[serde(rename = "type")]
pub t: TypeField<4>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
pub style: TextInputStyle,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
}
impl TextInput {
#[allow(clippy::too_many_arguments)]
pub fn new(
custom_id: String,
style: TextInputStyle,
label: String,
min_length: Option<i32>,
max_length: Option<i32>,
required: Option<bool>,
value: Option<String>,
placeholder: Option<String>,
) -> Self {
Self {
t: TypeField,
id: None,
custom_id,
style,
label: Some(label),
min_length,
max_length,
required,
value,
placeholder,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TextDisplay {
#[serde(rename = "type")]
pub t: TypeField<10>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub content: String,
}
impl TextDisplay {
pub fn new(content: impl Into<String>) -> Self {
Self {
t: TypeField,
id: None,
content: content.into(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Container {
#[serde(rename = "type")]
pub t: TypeField<17>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub components: Vec<Component>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accent_color: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spoiler: Option<bool>,
}
impl Container {
pub fn new(components: Vec<Component>) -> Self {
Self {
t: TypeField,
id: None,
components,
accent_color: None,
spoiler: None,
}
}
pub fn with_accent_color(mut self, color: u32) -> Self {
self.accent_color = Some(color);
self
}
pub fn spoiler(mut self) -> Self {
self.spoiler = Some(true);
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Section {
#[serde(rename = "type")]
pub t: TypeField<9>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub components: Vec<Component>,
pub accessory: Box<Component>,
}
impl Section {
pub fn new(components: Vec<Component>, accessory: Component) -> Self {
Self {
t: TypeField,
id: None,
components,
accessory: Box::new(accessory),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UnfurledMedia {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder_version: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flags: Option<UnfurledMediaFlags>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attachment_id: Option<Snowflake>,
}
impl UnfurledMedia {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
proxy_url: None,
height: None,
width: None,
placeholder: None,
placeholder_version: None,
content_type: None,
flags: None,
attachment_id: None,
}
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UnfurledMediaFlags: u32 {
const IsAnimated = 1 << 0;
}
}
impl Serialize for UnfurledMediaFlags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u32(self.bits())
}
}
impl<'de> Deserialize<'de> for UnfurledMediaFlags {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self::from_bits_retain(u32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Thumbnail {
#[serde(rename = "type")]
pub t: TypeField<11>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub media: UnfurledMedia,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spoiler: Option<bool>,
}
impl Thumbnail {
pub fn new(url: impl Into<String>) -> Self {
Self {
t: TypeField,
id: None,
media: UnfurledMedia::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) -> Self {
self.spoiler = Some(true);
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MediaGallery {
#[serde(rename = "type")]
pub t: TypeField<12>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub items: Vec<MediaGalleryItem>,
}
impl MediaGallery {
pub fn new(items: Vec<MediaGalleryItem>) -> Self {
Self {
t: TypeField,
id: None,
items,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MediaGalleryItem {
pub media: UnfurledMedia,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spoiler: Option<bool>,
}
impl MediaGalleryItem {
pub fn new(url: impl Into<String>) -> Self {
Self {
media: UnfurledMedia::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) -> Self {
self.spoiler = Some(true);
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FileComponent {
#[serde(rename = "type")]
pub t: TypeField<13>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub file: UnfurledMedia,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spoiler: Option<bool>,
#[serde(default, skip_serializing)]
pub name: Option<String>,
#[serde(default, skip_serializing)]
pub size: Option<u64>,
}
impl FileComponent {
pub fn new(url: impl Into<String>) -> Self {
Self {
t: TypeField,
id: None,
file: UnfurledMedia::new(url),
spoiler: None,
name: None,
size: None,
}
}
pub fn spoiler(mut self) -> Self {
self.spoiler = Some(true);
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Separator {
#[serde(rename = "type")]
pub t: TypeField<14>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub divider: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spacing: Option<SeparatorSpacing>,
}
impl Separator {
pub fn new() -> Self {
Self {
t: TypeField,
id: None,
divider: None,
spacing: None,
}
}
pub fn without_divider(mut self) -> Self {
self.divider = Some(false);
self
}
pub fn with_spacing(mut self, spacing: SeparatorSpacing) -> Self {
self.spacing = Some(spacing);
self
}
}
impl Default for Separator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize_repr, Serialize_repr)]
#[repr(u8)]
pub enum SeparatorSpacing {
Small = 1,
Large = 2,
}
macro_rules! into_component {
($($variant:ident($ty:ty)),* $(,)?) => {
$(impl From<$ty> for Component {
fn from(component: $ty) -> Self {
Component::$variant(component)
}
})*
};
}
into_component! {
ActionRow(ActionRow),
Button(ButtonComponent),
StringSelect(StringSelect),
TextInput(TextInput),
UserSelect(UserSelect),
RoleSelect(RoleSelect),
MentionableSelect(MentionableSelect),
ChannelSelect(ChannelSelect),
Section(Section),
TextDisplay(TextDisplay),
Thumbnail(Thumbnail),
MediaGallery(MediaGallery),
File(FileComponent),
Separator(Separator),
Container(Container),
Label(Label),
FileUpload(FileUpload),
RadioGroup(RadioGroup),
CheckboxGroup(CheckboxGroup),
Checkbox(Checkbox),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Label {
#[serde(rename = "type")]
pub t: TypeField<18>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub component: Box<Component>,
}
impl Label {
pub fn new(label: impl Into<String>, component: Component) -> Self {
Self {
t: TypeField,
id: None,
label: label.into(),
description: None,
component: Box::new(component),
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FileUpload {
#[serde(rename = "type")]
pub t: TypeField<19>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_values: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_values: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_types: Option<Vec<String>>,
}
impl FileUpload {
pub fn new(custom_id: impl Into<String>) -> Self {
Self {
t: TypeField,
id: None,
custom_id: custom_id.into(),
min_values: None,
max_values: None,
required: None,
file_types: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GroupOption {
pub value: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl GroupOption {
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
description: None,
default: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RadioGroup {
#[serde(rename = "type")]
pub t: TypeField<21>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
pub options: Vec<GroupOption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
impl RadioGroup {
pub fn new(custom_id: impl Into<String>, options: Vec<GroupOption>) -> Self {
Self {
t: TypeField,
id: None,
custom_id: custom_id.into(),
options,
required: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CheckboxGroup {
#[serde(rename = "type")]
pub t: TypeField<22>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
pub options: Vec<GroupOption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_values: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_values: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
impl CheckboxGroup {
pub fn new(custom_id: impl Into<String>, options: Vec<GroupOption>) -> Self {
Self {
t: TypeField,
id: None,
custom_id: custom_id.into(),
options,
min_values: None,
max_values: None,
required: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Checkbox {
#[serde(rename = "type")]
pub t: TypeField<23>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<i32>,
pub custom_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl Checkbox {
pub fn new(custom_id: impl Into<String>) -> Self {
Self {
t: TypeField,
id: None,
custom_id: custom_id.into(),
default: None,
}
}
}
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u8)]
pub enum TextInputStyle {
Short = 1,
Paragraph = 2,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn layout_builders_serialize_like_the_docs() {
let message = vec![
Component::container(vec![
Component::section(
vec![Component::text("# Title"), Component::text("body")],
Thumbnail::new("https://example.com/a.png")
.description("alt")
.into(),
),
Separator::new()
.without_divider()
.with_spacing(SeparatorSpacing::Large)
.into(),
Component::gallery(vec![MediaGalleryItem::new("attachment://b.png").spoiler()]),
Component::file("attachment://c.txt"),
Component::row(vec![Component::new_button(
ButtonStyle::Primary,
Some("Go".into()),
None,
Some("go".into()),
None,
None,
)]),
])
.with_id(7),
Component::separator(),
];
assert_eq!(
serde_json::to_value(&message).unwrap(),
json!([
{ "type": 17, "id": 7, "components": [
{ "type": 9,
"components": [
{ "type": 10, "content": "# Title" },
{ "type": 10, "content": "body" },
],
"accessory": {
"type": 11,
"media": { "url": "https://example.com/a.png" },
"description": "alt",
} },
{ "type": 14, "divider": false, "spacing": 2 },
{ "type": 12, "items": [
{ "media": { "url": "attachment://b.png" }, "spoiler": true },
] },
{ "type": 13, "file": { "url": "attachment://c.txt" } },
{ "type": 1, "components": [
{ "type": 2, "style": 1, "label": "Go", "custom_id": "go" },
] },
] },
{ "type": 14 },
])
);
}
#[test]
fn received_components_v2_message_deserializes() {
let media = json!({
"id": "1", "url": "https://cdn.discordapp.com/a.png",
"proxy_url": "https://media.discordapp.net/a.png",
"width": 64, "height": 32, "content_type": "image/png",
"placeholder": "abc", "placeholder_version": 1, "flags": 1,
"attachment_id": "2", "loading_state": 2,
});
let json = json!([{
"type": 17, "id": 1, "accent_color": null, "spoiler": false,
"components": [
{ "type": 9, "id": 2,
"components": [{ "type": 10, "id": 3, "content": "hi" }],
"accessory": { "type": 11, "id": 4, "media": media,
"description": null, "spoiler": false } },
{ "type": 14, "id": 5, "divider": true, "spacing": 1 },
{ "type": 12, "id": 6, "items": [
{ "media": media, "description": null, "spoiler": false },
] },
{ "type": 13, "id": 7, "file": media, "name": "a.png", "size": 10,
"spoiler": false },
{ "type": 99, "id": 8, "new": true },
],
}]);
let components: Vec<Component> = serde_json::from_value(json).unwrap();
let Component::Container(container) = &components[0] else {
panic!("not a container");
};
assert_eq!(container.accent_color, None);
let ids: Vec<_> = container.components.iter().map(Component::id).collect();
assert_eq!(ids, [Some(2), Some(5), Some(6), Some(7), Some(8)]);
let Component::Section(section) = &container.components[0] else {
panic!("not a section");
};
let Component::Thumbnail(thumbnail) = section.accessory.as_ref() else {
panic!("not a thumbnail");
};
assert_eq!(thumbnail.media.width, Some(64));
assert_eq!(thumbnail.media.flags, Some(UnfurledMediaFlags::IsAnimated));
let Component::File(file) = &container.components[3] else {
panic!("not a file");
};
assert_eq!((file.name.as_deref(), file.size), (Some("a.png"), Some(10)));
assert!(matches!(container.components[4], Component::Unknown(_)));
}
}