twilight_model/channel/message/component/
mod.rs

1//! Interactive message elements for use with [`Interaction`]s.
2//!
3//! Refer to [Discord Docs/Message Components] for additional information.
4//!
5//! [`Interaction`]: crate::application::interaction::Interaction
6//! [Discord Docs/Message Components]: https://discord.com/developers/docs/interactions/message-components
7
8mod action_row;
9mod button;
10mod container;
11mod file_display;
12mod kind;
13mod media_gallery;
14mod section;
15mod select_menu;
16mod separator;
17mod text_display;
18mod text_input;
19mod thumbnail;
20mod unfurled_media;
21
22pub use self::{
23    action_row::ActionRow,
24    button::{Button, ButtonStyle},
25    container::Container,
26    file_display::FileDisplay,
27    kind::ComponentType,
28    media_gallery::{MediaGallery, MediaGalleryItem},
29    section::Section,
30    select_menu::{SelectDefaultValue, SelectMenu, SelectMenuOption, SelectMenuType},
31    separator::{Separator, SeparatorSpacingSize},
32    text_display::TextDisplay,
33    text_input::{TextInput, TextInputStyle},
34    thumbnail::Thumbnail,
35    unfurled_media::UnfurledMediaItem,
36};
37
38use super::EmojiReactionType;
39use crate::{
40    channel::ChannelType,
41    id::{marker::SkuMarker, Id},
42};
43use serde::{
44    de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
45    ser::{Error as SerError, SerializeStruct},
46    Deserialize, Serialize, Serializer,
47};
48use serde_value::{DeserializerError, Value};
49use std::fmt::{Formatter, Result as FmtResult};
50
51/// Interactive message element.
52///
53/// Must be either a top level [`ActionRow`] or nested inside one.
54///
55/// # Examples
56///
57/// ## Button
58///
59/// ```
60/// use twilight_model::channel::message::component::{ActionRow, Button, ButtonStyle, Component};
61///
62/// Component::ActionRow(ActionRow {
63///     id: None,
64///     components: Vec::from([Component::Button(Button {
65///         id: None,
66///         custom_id: Some("click_one".to_owned()),
67///         disabled: false,
68///         emoji: None,
69///         label: Some("Click me!".to_owned()),
70///         style: ButtonStyle::Primary,
71///         url: None,
72///         sku_id: None,
73///     })]),
74/// });
75/// ```
76///
77/// ## Select menu
78///
79/// ```
80/// use twilight_model::{
81///     channel::message::{
82///         component::{ActionRow, Component, SelectMenu, SelectMenuOption, SelectMenuType},
83///         EmojiReactionType,
84///     },
85///     id::Id,
86/// };
87///
88/// Component::ActionRow(ActionRow {
89///     id: None,
90///     components: vec![Component::SelectMenu(SelectMenu {
91///         id: None,
92///         channel_types: None,
93///         custom_id: "class_select_1".to_owned(),
94///         default_values: None,
95///         disabled: false,
96///         kind: SelectMenuType::Text,
97///         max_values: Some(3),
98///         min_values: Some(1),
99///         options: Some(Vec::from([
100///             SelectMenuOption {
101///                 default: false,
102///                 emoji: Some(EmojiReactionType::Custom {
103///                     animated: false,
104///                     id: Id::new(625891304148303894),
105///                     name: Some("rogue".to_owned()),
106///                 }),
107///                 description: Some("Sneak n stab".to_owned()),
108///                 label: "Rogue".to_owned(),
109///                 value: "rogue".to_owned(),
110///             },
111///             SelectMenuOption {
112///                 default: false,
113///                 emoji: Some(EmojiReactionType::Custom {
114///                     animated: false,
115///                     id: Id::new(625891304081063986),
116///                     name: Some("mage".to_owned()),
117///                 }),
118///                 description: Some("Turn 'em into a sheep".to_owned()),
119///                 label: "Mage".to_owned(),
120///                 value: "mage".to_owned(),
121///             },
122///             SelectMenuOption {
123///                 default: false,
124///                 emoji: Some(EmojiReactionType::Custom {
125///                     animated: false,
126///                     id: Id::new(625891303795982337),
127///                     name: Some("priest".to_owned()),
128///                 }),
129///                 description: Some("You get heals when I'm done doing damage".to_owned()),
130///                 label: "Priest".to_owned(),
131///                 value: "priest".to_owned(),
132///             },
133///         ])),
134///         placeholder: Some("Choose a class".to_owned()),
135///     })],
136/// });
137/// ```
138#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub enum Component {
140    /// Top level, non-interactive container of other (non action row) components.
141    ActionRow(ActionRow),
142    /// Clickable item that renders below messages.
143    Button(Button),
144    /// Dropdown-style item that renders below messages.
145    SelectMenu(SelectMenu),
146    /// Pop-up item that renders on modals.
147    TextInput(TextInput),
148    /// Markdown text.
149    TextDisplay(TextDisplay),
150    /// Display images and other media.
151    MediaGallery(MediaGallery),
152    /// Component to add vertical padding between other components.
153    Separator(Separator),
154    /// Displays an attached file.
155    File(FileDisplay),
156    /// Container to display text alongside an accessory component.
157    Section(Section),
158    /// Container that visually groups a set of components.
159    Container(Container),
160    /// Small image that can be used as an accessory.
161    Thumbnail(Thumbnail),
162    /// Variant value is unknown to the library.
163    Unknown(u8),
164}
165
166impl Component {
167    /// Type of component that this is.
168    ///
169    /// ```
170    /// use twilight_model::channel::message::component::{
171    ///     Button, ButtonStyle, Component, ComponentType,
172    /// };
173    ///
174    /// let component = Component::Button(Button {
175    ///     id: None,
176    ///     custom_id: None,
177    ///     disabled: false,
178    ///     emoji: None,
179    ///     label: Some("ping".to_owned()),
180    ///     style: ButtonStyle::Primary,
181    ///     url: None,
182    ///     sku_id: None,
183    /// });
184    ///
185    /// assert_eq!(ComponentType::Button, component.kind());
186    /// ```
187    pub const fn kind(&self) -> ComponentType {
188        match self {
189            Component::ActionRow(_) => ComponentType::ActionRow,
190            Component::Button(_) => ComponentType::Button,
191            Component::SelectMenu(SelectMenu { kind, .. }) => match kind {
192                SelectMenuType::Text => ComponentType::TextSelectMenu,
193                SelectMenuType::User => ComponentType::UserSelectMenu,
194                SelectMenuType::Role => ComponentType::RoleSelectMenu,
195                SelectMenuType::Mentionable => ComponentType::MentionableSelectMenu,
196                SelectMenuType::Channel => ComponentType::ChannelSelectMenu,
197            },
198            Component::TextInput(_) => ComponentType::TextInput,
199            Component::TextDisplay(_) => ComponentType::TextDisplay,
200            Component::MediaGallery(_) => ComponentType::MediaGallery,
201            Component::Separator(_) => ComponentType::Separator,
202            Component::File(_) => ComponentType::File,
203            Component::Unknown(unknown) => ComponentType::Unknown(*unknown),
204            Component::Section(_) => ComponentType::Section,
205            Component::Container(_) => ComponentType::Container,
206            Component::Thumbnail(_) => ComponentType::Thumbnail,
207        }
208    }
209
210    /// Get the amount of components a component should count as.
211    pub fn component_count(&self) -> usize {
212        match self {
213            Component::ActionRow(action_row) => 1 + action_row.components.len(),
214            Component::Section(section) => 1 + section.components.len(),
215            Component::Container(container) => 1 + container.components.len(),
216            Component::Button(_)
217            | Component::SelectMenu(_)
218            | Component::TextInput(_)
219            | Component::TextDisplay(_)
220            | Component::MediaGallery(_)
221            | Component::Separator(_)
222            | Component::File(_)
223            | Component::Thumbnail(_)
224            | Component::Unknown(_) => 1,
225        }
226    }
227}
228
229impl From<ActionRow> for Component {
230    fn from(action_row: ActionRow) -> Self {
231        Self::ActionRow(action_row)
232    }
233}
234
235impl From<Button> for Component {
236    fn from(button: Button) -> Self {
237        Self::Button(button)
238    }
239}
240
241impl From<Container> for Component {
242    fn from(container: Container) -> Self {
243        Self::Container(container)
244    }
245}
246
247impl From<FileDisplay> for Component {
248    fn from(file_display: FileDisplay) -> Self {
249        Self::File(file_display)
250    }
251}
252
253impl From<MediaGallery> for Component {
254    fn from(media_gallery: MediaGallery) -> Self {
255        Self::MediaGallery(media_gallery)
256    }
257}
258
259impl From<Section> for Component {
260    fn from(section: Section) -> Self {
261        Self::Section(section)
262    }
263}
264
265impl From<SelectMenu> for Component {
266    fn from(select_menu: SelectMenu) -> Self {
267        Self::SelectMenu(select_menu)
268    }
269}
270
271impl From<Separator> for Component {
272    fn from(separator: Separator) -> Self {
273        Self::Separator(separator)
274    }
275}
276
277impl From<TextDisplay> for Component {
278    fn from(text_display: TextDisplay) -> Self {
279        Self::TextDisplay(text_display)
280    }
281}
282
283impl From<TextInput> for Component {
284    fn from(text_input: TextInput) -> Self {
285        Self::TextInput(text_input)
286    }
287}
288
289impl From<Thumbnail> for Component {
290    fn from(thumbnail: Thumbnail) -> Self {
291        Self::Thumbnail(thumbnail)
292    }
293}
294
295impl<'de> Deserialize<'de> for Component {
296    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
297        deserializer.deserialize_any(ComponentVisitor)
298    }
299}
300
301#[derive(Debug, Deserialize)]
302#[serde(field_identifier, rename_all = "snake_case")]
303enum Field {
304    ChannelTypes,
305    Components,
306    CustomId,
307    DefaultValues,
308    Disabled,
309    Emoji,
310    Label,
311    MaxLength,
312    MaxValues,
313    MinLength,
314    MinValues,
315    Options,
316    Placeholder,
317    Required,
318    Style,
319    Type,
320    Url,
321    SkuId,
322    Value,
323    Id,
324    Content,
325    Items,
326    Divider,
327    Spacing,
328    File,
329    Spoiler,
330    Accessory,
331    Media,
332    Description,
333    AccentColor,
334}
335
336struct ComponentVisitor;
337
338impl<'de> Visitor<'de> for ComponentVisitor {
339    type Value = Component;
340
341    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
342        f.write_str("struct Component")
343    }
344
345    #[allow(clippy::too_many_lines)]
346    fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
347        // Required fields.
348        let mut components: Option<Vec<Component>> = None;
349        let mut kind: Option<ComponentType> = None;
350        let mut options: Option<Vec<SelectMenuOption>> = None;
351        let mut style: Option<Value> = None;
352
353        // Liminal fields.
354        let mut custom_id: Option<Option<Value>> = None;
355        let mut label: Option<Option<String>> = None;
356
357        // Optional fields.
358        let mut channel_types: Option<Vec<ChannelType>> = None;
359        let mut default_values: Option<Vec<SelectDefaultValue>> = None;
360        let mut disabled: Option<bool> = None;
361        let mut emoji: Option<Option<EmojiReactionType>> = None;
362        let mut max_length: Option<Option<u16>> = None;
363        let mut max_values: Option<Option<u8>> = None;
364        let mut min_length: Option<Option<u16>> = None;
365        let mut min_values: Option<Option<u8>> = None;
366        let mut placeholder: Option<Option<String>> = None;
367        let mut required: Option<Option<bool>> = None;
368        let mut url: Option<Option<String>> = None;
369        let mut sku_id: Option<Id<SkuMarker>> = None;
370        let mut value: Option<Option<String>> = None;
371
372        let mut id: Option<i32> = None;
373        let mut content: Option<String> = None;
374        let mut items: Option<Vec<MediaGalleryItem>> = None;
375        let mut divider: Option<bool> = None;
376        let mut spacing: Option<SeparatorSpacingSize> = None;
377        let mut file: Option<UnfurledMediaItem> = None;
378        let mut spoiler: Option<bool> = None;
379        let mut accessory: Option<Component> = None;
380        let mut media: Option<UnfurledMediaItem> = None;
381        let mut description: Option<Option<String>> = None;
382        let mut accent_color: Option<Option<u32>> = None;
383
384        loop {
385            let key = match map.next_key() {
386                Ok(Some(key)) => key,
387                Ok(None) => break,
388                Err(_) => {
389                    map.next_value::<IgnoredAny>()?;
390
391                    continue;
392                }
393            };
394
395            match key {
396                Field::ChannelTypes => {
397                    if channel_types.is_some() {
398                        return Err(DeError::duplicate_field("channel_types"));
399                    }
400
401                    channel_types = Some(map.next_value()?);
402                }
403                Field::Components => {
404                    if components.is_some() {
405                        return Err(DeError::duplicate_field("components"));
406                    }
407
408                    components = Some(map.next_value()?);
409                }
410                Field::CustomId => {
411                    if custom_id.is_some() {
412                        return Err(DeError::duplicate_field("custom_id"));
413                    }
414
415                    custom_id = Some(map.next_value()?);
416                }
417                Field::DefaultValues => {
418                    if default_values.is_some() {
419                        return Err(DeError::duplicate_field("default_values"));
420                    }
421
422                    default_values = map.next_value()?;
423                }
424                Field::Disabled => {
425                    if disabled.is_some() {
426                        return Err(DeError::duplicate_field("disabled"));
427                    }
428
429                    disabled = Some(map.next_value()?);
430                }
431                Field::Emoji => {
432                    if emoji.is_some() {
433                        return Err(DeError::duplicate_field("emoji"));
434                    }
435
436                    emoji = Some(map.next_value()?);
437                }
438                Field::Label => {
439                    if label.is_some() {
440                        return Err(DeError::duplicate_field("label"));
441                    }
442
443                    label = Some(map.next_value()?);
444                }
445                Field::MaxLength => {
446                    if max_length.is_some() {
447                        return Err(DeError::duplicate_field("max_length"));
448                    }
449
450                    max_length = Some(map.next_value()?);
451                }
452                Field::MaxValues => {
453                    if max_values.is_some() {
454                        return Err(DeError::duplicate_field("max_values"));
455                    }
456
457                    max_values = Some(map.next_value()?);
458                }
459                Field::MinLength => {
460                    if min_length.is_some() {
461                        return Err(DeError::duplicate_field("min_length"));
462                    }
463
464                    min_length = Some(map.next_value()?);
465                }
466                Field::MinValues => {
467                    if min_values.is_some() {
468                        return Err(DeError::duplicate_field("min_values"));
469                    }
470
471                    min_values = Some(map.next_value()?);
472                }
473                Field::Options => {
474                    if options.is_some() {
475                        return Err(DeError::duplicate_field("options"));
476                    }
477
478                    options = Some(map.next_value()?);
479                }
480                Field::Placeholder => {
481                    if placeholder.is_some() {
482                        return Err(DeError::duplicate_field("placeholder"));
483                    }
484
485                    placeholder = Some(map.next_value()?);
486                }
487                Field::Required => {
488                    if required.is_some() {
489                        return Err(DeError::duplicate_field("required"));
490                    }
491
492                    required = Some(map.next_value()?);
493                }
494                Field::Style => {
495                    if style.is_some() {
496                        return Err(DeError::duplicate_field("style"));
497                    }
498
499                    style = Some(map.next_value()?);
500                }
501                Field::Type => {
502                    if kind.is_some() {
503                        return Err(DeError::duplicate_field("type"));
504                    }
505
506                    kind = Some(map.next_value()?);
507                }
508                Field::Url => {
509                    if url.is_some() {
510                        return Err(DeError::duplicate_field("url"));
511                    }
512
513                    url = Some(map.next_value()?);
514                }
515                Field::SkuId => {
516                    if sku_id.is_some() {
517                        return Err(DeError::duplicate_field("sku_id"));
518                    }
519
520                    sku_id = map.next_value()?;
521                }
522                Field::Value => {
523                    if value.is_some() {
524                        return Err(DeError::duplicate_field("value"));
525                    }
526
527                    value = Some(map.next_value()?);
528                }
529                Field::Id => {
530                    if id.is_some() {
531                        return Err(DeError::duplicate_field("id"));
532                    }
533
534                    id = Some(map.next_value()?);
535                }
536                Field::Content => {
537                    if content.is_some() {
538                        return Err(DeError::duplicate_field("content"));
539                    }
540
541                    content = Some(map.next_value()?);
542                }
543                Field::Items => {
544                    if items.is_some() {
545                        return Err(DeError::duplicate_field("items"));
546                    }
547
548                    items = Some(map.next_value()?);
549                }
550                Field::Divider => {
551                    if divider.is_some() {
552                        return Err(DeError::duplicate_field("divider"));
553                    }
554
555                    divider = Some(map.next_value()?);
556                }
557                Field::Spacing => {
558                    if spacing.is_some() {
559                        return Err(DeError::duplicate_field("spacing"));
560                    }
561
562                    spacing = Some(map.next_value()?);
563                }
564                Field::File => {
565                    if file.is_some() {
566                        return Err(DeError::duplicate_field("file"));
567                    }
568
569                    file = Some(map.next_value()?);
570                }
571                Field::Spoiler => {
572                    if spoiler.is_some() {
573                        return Err(DeError::duplicate_field("spoiler"));
574                    }
575
576                    spoiler = Some(map.next_value()?);
577                }
578                Field::Accessory => {
579                    if accessory.is_some() {
580                        return Err(DeError::duplicate_field("accessory"));
581                    }
582
583                    accessory = Some(map.next_value()?);
584                }
585                Field::Media => {
586                    if media.is_some() {
587                        return Err(DeError::duplicate_field("media"));
588                    }
589
590                    media = Some(map.next_value()?);
591                }
592                Field::Description => {
593                    if description.is_some() {
594                        return Err(DeError::duplicate_field("description"));
595                    }
596
597                    description = Some(map.next_value()?);
598                }
599                Field::AccentColor => {
600                    if accent_color.is_some() {
601                        return Err(DeError::duplicate_field("accent_color"));
602                    }
603
604                    accent_color = Some(map.next_value()?);
605                }
606            }
607        }
608
609        let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
610
611        Ok(match kind {
612            // Required fields:
613            // - components
614            ComponentType::ActionRow => {
615                let components = components.ok_or_else(|| DeError::missing_field("components"))?;
616
617                Self::Value::ActionRow(ActionRow { id, components })
618            }
619            // Required fields:
620            // - style
621            //
622            // Optional fields:
623            // - custom_id
624            // - disabled
625            // - emoji
626            // - label
627            // - url
628            // - sku_id
629            ComponentType::Button => {
630                let style = style
631                    .ok_or_else(|| DeError::missing_field("style"))?
632                    .deserialize_into()
633                    .map_err(DeserializerError::into_error)?;
634
635                let custom_id = custom_id
636                    .flatten()
637                    .map(Value::deserialize_into)
638                    .transpose()
639                    .map_err(DeserializerError::into_error)?;
640
641                Self::Value::Button(Button {
642                    custom_id,
643                    disabled: disabled.unwrap_or_default(),
644                    emoji: emoji.unwrap_or_default(),
645                    label: label.flatten(),
646                    style,
647                    url: url.unwrap_or_default(),
648                    sku_id,
649                    id,
650                })
651            }
652            // Required fields:
653            // - custom_id
654            // - options (if this is a text select menu)
655            //
656            // Optional fields:
657            // - default_values
658            // - disabled
659            // - max_values
660            // - min_values
661            // - placeholder
662            // - channel_types (if this is a channel select menu)
663            kind @ (ComponentType::TextSelectMenu
664            | ComponentType::UserSelectMenu
665            | ComponentType::RoleSelectMenu
666            | ComponentType::MentionableSelectMenu
667            | ComponentType::ChannelSelectMenu) => {
668                // Verify the individual variants' required fields
669                if let ComponentType::TextSelectMenu = kind {
670                    if options.is_none() {
671                        return Err(DeError::missing_field("options"));
672                    }
673                }
674
675                let custom_id = custom_id
676                    .flatten()
677                    .ok_or_else(|| DeError::missing_field("custom_id"))?
678                    .deserialize_into()
679                    .map_err(DeserializerError::into_error)?;
680
681                Self::Value::SelectMenu(SelectMenu {
682                    channel_types,
683                    custom_id,
684                    default_values,
685                    disabled: disabled.unwrap_or_default(),
686                    kind: match kind {
687                        ComponentType::TextSelectMenu => SelectMenuType::Text,
688                        ComponentType::UserSelectMenu => SelectMenuType::User,
689                        ComponentType::RoleSelectMenu => SelectMenuType::Role,
690                        ComponentType::MentionableSelectMenu => SelectMenuType::Mentionable,
691                        ComponentType::ChannelSelectMenu => SelectMenuType::Channel,
692                        // This branch is unreachable unless we add a new type above and forget to
693                        // also add it here
694                        _ => {
695                            unreachable!("select menu component type is only partially implemented")
696                        }
697                    },
698                    max_values: max_values.unwrap_or_default(),
699                    min_values: min_values.unwrap_or_default(),
700                    options,
701                    placeholder: placeholder.unwrap_or_default(),
702                    id,
703                })
704            }
705            // Required fields:
706            // - custom_id
707            // - label
708            // - style
709            //
710            // Optional fields:
711            // - max_length
712            // - min_length
713            // - placeholder
714            // - required
715            // - value
716            ComponentType::TextInput => {
717                let custom_id = custom_id
718                    .flatten()
719                    .ok_or_else(|| DeError::missing_field("custom_id"))?
720                    .deserialize_into()
721                    .map_err(DeserializerError::into_error)?;
722
723                let label = label
724                    .flatten()
725                    .ok_or_else(|| DeError::missing_field("label"))?;
726
727                let style = style
728                    .ok_or_else(|| DeError::missing_field("style"))?
729                    .deserialize_into()
730                    .map_err(DeserializerError::into_error)?;
731
732                Self::Value::TextInput(TextInput {
733                    custom_id,
734                    label,
735                    max_length: max_length.unwrap_or_default(),
736                    min_length: min_length.unwrap_or_default(),
737                    placeholder: placeholder.unwrap_or_default(),
738                    required: required.unwrap_or_default(),
739                    style,
740                    value: value.unwrap_or_default(),
741                    id,
742                })
743            }
744            ComponentType::TextDisplay => {
745                let content = content.ok_or_else(|| DeError::missing_field("content"))?;
746
747                Self::Value::TextDisplay(TextDisplay { id, content })
748            }
749            ComponentType::MediaGallery => {
750                let items = items.ok_or_else(|| DeError::missing_field("items"))?;
751
752                Self::Value::MediaGallery(MediaGallery { id, items })
753            }
754            ComponentType::Separator => Self::Value::Separator(Separator {
755                id,
756                divider,
757                spacing,
758            }),
759            ComponentType::File => {
760                let file = file.ok_or_else(|| DeError::missing_field("file"))?;
761
762                Self::Value::File(FileDisplay { id, file, spoiler })
763            }
764            ComponentType::Unknown(unknown) => Self::Value::Unknown(unknown),
765            ComponentType::Section => {
766                let components = components.ok_or_else(|| DeError::missing_field("components"))?;
767                let accessory = accessory.ok_or_else(|| DeError::missing_field("accessory"))?;
768                Self::Value::Section(Section {
769                    id,
770                    components,
771                    accessory: Box::new(accessory),
772                })
773            }
774            ComponentType::Thumbnail => {
775                let media = media.ok_or_else(|| DeError::missing_field("media"))?;
776                Self::Value::Thumbnail(Thumbnail {
777                    id,
778                    media,
779                    description,
780                    spoiler,
781                })
782            }
783            ComponentType::Container => {
784                let components = components.ok_or_else(|| DeError::missing_field("components"))?;
785                Self::Value::Container(Container {
786                    id,
787                    accent_color,
788                    spoiler,
789                    components,
790                })
791            }
792        })
793    }
794}
795
796impl Serialize for Component {
797    #[allow(clippy::too_many_lines)]
798    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
799        let len = match self {
800            // Required fields:
801            // - type
802            // - components
803            //
804            // Optional fields:
805            // - id
806            Component::ActionRow(row) => 2 + usize::from(row.id.is_some()),
807            // Required fields:
808            // - type
809            // - style
810            //
811            // Optional fields:
812            // - id
813            // - custom_id
814            // - disabled
815            // - emoji
816            // - label
817            // - url
818            // - sku_id
819            Component::Button(button) => {
820                2 + usize::from(button.custom_id.is_some())
821                    + usize::from(button.disabled)
822                    + usize::from(button.emoji.is_some())
823                    + usize::from(button.label.is_some())
824                    + usize::from(button.url.is_some())
825                    + usize::from(button.sku_id.is_some())
826                    + usize::from(button.id.is_some())
827            }
828            // Required fields:
829            // - custom_id
830            // - options (for text select menus)
831            // - type
832            //
833            // Optional fields:
834            // - id
835            // - channel_types (for channel select menus)
836            // - default_values
837            // - disabled
838            // - max_values
839            // - min_values
840            // - placeholder
841            Component::SelectMenu(select_menu) => {
842                // We ignore text menus that don't include the `options` field, as those are
843                // detected later in the serialization process
844                2 + usize::from(select_menu.channel_types.is_some())
845                    + usize::from(select_menu.default_values.is_some())
846                    + usize::from(select_menu.disabled)
847                    + usize::from(select_menu.max_values.is_some())
848                    + usize::from(select_menu.min_values.is_some())
849                    + usize::from(select_menu.options.is_some())
850                    + usize::from(select_menu.placeholder.is_some())
851                    + usize::from(select_menu.id.is_some())
852            }
853            // Required fields:
854            // - custom_id
855            // - label
856            // - style
857            // - type
858            //
859            // Optional fields:
860            // - id
861            // - max_length
862            // - min_length
863            // - placeholder
864            // - required
865            // - value
866            Component::TextInput(text_input) => {
867                4 + usize::from(text_input.max_length.is_some())
868                    + usize::from(text_input.min_length.is_some())
869                    + usize::from(text_input.placeholder.is_some())
870                    + usize::from(text_input.required.is_some())
871                    + usize::from(text_input.value.is_some())
872                    + usize::from(text_input.id.is_some())
873            }
874            // Required fields:
875            // - type
876            // - content
877            // Optional fields:
878            // - id
879            Component::TextDisplay(text_display) => 2 + usize::from(text_display.id.is_some()),
880            // Required fields:
881            // - type
882            // - items
883            // Optional fields:
884            // - id
885            Component::MediaGallery(media_gallery) => 2 + usize::from(media_gallery.id.is_some()),
886            // Required fields:
887            // - type
888            // Optional fields:
889            // - id
890            // - divider
891            // - spacing
892            Component::Separator(separator) => {
893                1 + usize::from(separator.divider.is_some())
894                    + usize::from(separator.spacing.is_some())
895                    + usize::from(separator.id.is_some())
896            }
897            // Required fields:
898            // - type
899            // - file
900            // Optional fields:
901            // - id
902            // - spoiler
903            Component::File(file) => {
904                2 + usize::from(file.spoiler.is_some()) + usize::from(file.id.is_some())
905            }
906            // Required fields:
907            // - type
908            // - components
909            // - accessory
910            // Optional fields:
911            // - id
912            Component::Section(section) => 3 + usize::from(section.id.is_some()),
913            // Required fields:
914            // - type
915            // - components
916            // Optional fields:
917            // - id
918            // - accent_color
919            // - spoiler
920            Component::Container(container) => {
921                2 + usize::from(container.accent_color.is_some())
922                    + usize::from(container.spoiler.is_some())
923                    + usize::from(container.id.is_some())
924            }
925            // Required fields:
926            // - type
927            // - media
928            // Optional fields:
929            // - id
930            // - description
931            // - spoiler
932            Component::Thumbnail(thumbnail) => {
933                2 + usize::from(thumbnail.spoiler.is_some())
934                    + usize::from(thumbnail.id.is_some())
935                    + usize::from(thumbnail.description.is_some())
936            }
937            // We are dropping fields here but nothing we can do about that for
938            // the time being.
939            Component::Unknown(_) => 1,
940        };
941
942        let mut state = serializer.serialize_struct("Component", len)?;
943
944        match self {
945            Component::ActionRow(action_row) => {
946                state.serialize_field("type", &ComponentType::ActionRow)?;
947                if let Some(id) = action_row.id {
948                    state.serialize_field("id", &id)?;
949                }
950
951                state.serialize_field("components", &action_row.components)?;
952            }
953            Component::Button(button) => {
954                state.serialize_field("type", &ComponentType::Button)?;
955                if let Some(id) = button.id {
956                    state.serialize_field("id", &id)?;
957                }
958
959                if button.custom_id.is_some() {
960                    state.serialize_field("custom_id", &button.custom_id)?;
961                }
962
963                if button.disabled {
964                    state.serialize_field("disabled", &button.disabled)?;
965                }
966
967                if button.emoji.is_some() {
968                    state.serialize_field("emoji", &button.emoji)?;
969                }
970
971                if button.label.is_some() {
972                    state.serialize_field("label", &button.label)?;
973                }
974
975                state.serialize_field("style", &button.style)?;
976
977                if button.url.is_some() {
978                    state.serialize_field("url", &button.url)?;
979                }
980
981                if button.sku_id.is_some() {
982                    state.serialize_field("sku_id", &button.sku_id)?;
983                }
984            }
985            Component::SelectMenu(select_menu) => {
986                match &select_menu.kind {
987                    SelectMenuType::Text => {
988                        state.serialize_field("type", &ComponentType::TextSelectMenu)?;
989                        if let Some(id) = select_menu.id {
990                            state.serialize_field("id", &id)?;
991                        }
992
993                        state.serialize_field(
994                            "options",
995                            &select_menu.options.as_ref().ok_or(SerError::custom(
996                                "required field \"option\" missing for text select menu",
997                            ))?,
998                        )?;
999                    }
1000                    SelectMenuType::User => {
1001                        state.serialize_field("type", &ComponentType::UserSelectMenu)?;
1002                        if let Some(id) = select_menu.id {
1003                            state.serialize_field("id", &id)?;
1004                        }
1005                    }
1006                    SelectMenuType::Role => {
1007                        state.serialize_field("type", &ComponentType::RoleSelectMenu)?;
1008                        if let Some(id) = select_menu.id {
1009                            state.serialize_field("id", &id)?;
1010                        }
1011                    }
1012                    SelectMenuType::Mentionable => {
1013                        state.serialize_field("type", &ComponentType::MentionableSelectMenu)?;
1014                        if let Some(id) = select_menu.id {
1015                            state.serialize_field("id", &id)?;
1016                        }
1017                    }
1018                    SelectMenuType::Channel => {
1019                        state.serialize_field("type", &ComponentType::ChannelSelectMenu)?;
1020                        if let Some(id) = select_menu.id {
1021                            state.serialize_field("id", &id)?;
1022                        }
1023
1024                        if let Some(channel_types) = &select_menu.channel_types {
1025                            state.serialize_field("channel_types", channel_types)?;
1026                        }
1027                    }
1028                }
1029
1030                // Due to `custom_id` being required in some variants and
1031                // optional in others, serialize as an Option.
1032                state.serialize_field("custom_id", &Some(&select_menu.custom_id))?;
1033
1034                if select_menu.default_values.is_some() {
1035                    state.serialize_field("default_values", &select_menu.default_values)?;
1036                }
1037
1038                state.serialize_field("disabled", &select_menu.disabled)?;
1039
1040                if select_menu.max_values.is_some() {
1041                    state.serialize_field("max_values", &select_menu.max_values)?;
1042                }
1043
1044                if select_menu.min_values.is_some() {
1045                    state.serialize_field("min_values", &select_menu.min_values)?;
1046                }
1047
1048                if select_menu.placeholder.is_some() {
1049                    state.serialize_field("placeholder", &select_menu.placeholder)?;
1050                }
1051            }
1052            Component::TextInput(text_input) => {
1053                state.serialize_field("type", &ComponentType::TextInput)?;
1054                if let Some(id) = text_input.id {
1055                    state.serialize_field("id", &id)?;
1056                }
1057
1058                // Due to `custom_id` and `label` being required in some
1059                // variants and optional in others, serialize as an Option.
1060                state.serialize_field("custom_id", &Some(&text_input.custom_id))?;
1061                state.serialize_field("label", &Some(&text_input.label))?;
1062
1063                if text_input.max_length.is_some() {
1064                    state.serialize_field("max_length", &text_input.max_length)?;
1065                }
1066
1067                if text_input.min_length.is_some() {
1068                    state.serialize_field("min_length", &text_input.min_length)?;
1069                }
1070
1071                if text_input.placeholder.is_some() {
1072                    state.serialize_field("placeholder", &text_input.placeholder)?;
1073                }
1074
1075                if text_input.required.is_some() {
1076                    state.serialize_field("required", &text_input.required)?;
1077                }
1078
1079                state.serialize_field("style", &text_input.style)?;
1080
1081                if text_input.value.is_some() {
1082                    state.serialize_field("value", &text_input.value)?;
1083                }
1084            }
1085            Component::TextDisplay(text_display) => {
1086                state.serialize_field("type", &ComponentType::TextDisplay)?;
1087                if let Some(id) = text_display.id {
1088                    state.serialize_field("id", &id)?;
1089                }
1090
1091                state.serialize_field("content", &text_display.content)?;
1092            }
1093            Component::MediaGallery(media_gallery) => {
1094                state.serialize_field("type", &ComponentType::MediaGallery)?;
1095                if let Some(id) = media_gallery.id {
1096                    state.serialize_field("id", &id)?;
1097                }
1098
1099                state.serialize_field("items", &media_gallery.items)?;
1100            }
1101            Component::Separator(separator) => {
1102                state.serialize_field("type", &ComponentType::Separator)?;
1103                if let Some(id) = separator.id {
1104                    state.serialize_field("id", &id)?;
1105                }
1106                if let Some(divider) = separator.divider {
1107                    state.serialize_field("divider", &divider)?;
1108                }
1109                if let Some(spacing) = &separator.spacing {
1110                    state.serialize_field("spacing", spacing)?;
1111                }
1112            }
1113            Component::File(file) => {
1114                state.serialize_field("type", &ComponentType::File)?;
1115                if let Some(id) = file.id {
1116                    state.serialize_field("id", &id)?;
1117                }
1118
1119                state.serialize_field("file", &file.file)?;
1120                if let Some(spoiler) = file.spoiler {
1121                    state.serialize_field("spoiler", &spoiler)?;
1122                }
1123            }
1124            Component::Section(section) => {
1125                state.serialize_field("type", &ComponentType::Section)?;
1126                if let Some(id) = section.id {
1127                    state.serialize_field("id", &id)?;
1128                }
1129
1130                state.serialize_field("components", &section.components)?;
1131                state.serialize_field("accessory", &section.accessory)?;
1132            }
1133            Component::Container(container) => {
1134                state.serialize_field("type", &ComponentType::Container)?;
1135                if let Some(id) = container.id {
1136                    state.serialize_field("id", &id)?;
1137                }
1138
1139                if let Some(accent_color) = container.accent_color {
1140                    state.serialize_field("accent_color", &accent_color)?;
1141                }
1142                if let Some(spoiler) = container.spoiler {
1143                    state.serialize_field("spoiler", &spoiler)?;
1144                }
1145                state.serialize_field("components", &container.components)?;
1146            }
1147            Component::Thumbnail(thumbnail) => {
1148                state.serialize_field("type", &ComponentType::Thumbnail)?;
1149                if let Some(id) = thumbnail.id {
1150                    state.serialize_field("id", &id)?;
1151                }
1152
1153                state.serialize_field("media", &thumbnail.media)?;
1154                if let Some(description) = &thumbnail.description {
1155                    state.serialize_field("description", description)?;
1156                }
1157                if let Some(spoiler) = thumbnail.spoiler {
1158                    state.serialize_field("spoiler", &spoiler)?;
1159                }
1160            }
1161            // We are not serializing all fields so this will fail to
1162            // deserialize. But it is all that can be done to avoid losing
1163            // incoming messages at this time.
1164            Component::Unknown(unknown) => {
1165                state.serialize_field("type", &ComponentType::Unknown(*unknown))?;
1166            }
1167        }
1168
1169        state.end()
1170    }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    // Required due to the use of a unicode emoji in a constant.
1176    #![allow(clippy::non_ascii_literal)]
1177
1178    use super::*;
1179    use crate::id::Id;
1180    use serde_test::Token;
1181    use static_assertions::assert_impl_all;
1182
1183    assert_impl_all!(
1184        Component: From<ActionRow>,
1185        From<Button>,
1186        From<SelectMenu>,
1187        From<TextInput>
1188    );
1189
1190    #[allow(clippy::too_many_lines)]
1191    #[test]
1192    fn component_full() {
1193        let component = Component::ActionRow(ActionRow {
1194            components: Vec::from([
1195                Component::Button(Button {
1196                    custom_id: Some("test custom id".into()),
1197                    disabled: true,
1198                    emoji: None,
1199                    label: Some("test label".into()),
1200                    style: ButtonStyle::Primary,
1201                    url: None,
1202                    sku_id: None,
1203                    id: None,
1204                }),
1205                Component::SelectMenu(SelectMenu {
1206                    channel_types: None,
1207                    custom_id: "test custom id 2".into(),
1208                    default_values: None,
1209                    disabled: false,
1210                    kind: SelectMenuType::Text,
1211                    max_values: Some(25),
1212                    min_values: Some(5),
1213                    options: Some(Vec::from([SelectMenuOption {
1214                        label: "test option label".into(),
1215                        value: "test option value".into(),
1216                        description: Some("test description".into()),
1217                        emoji: None,
1218                        default: false,
1219                    }])),
1220                    placeholder: Some("test placeholder".into()),
1221                    id: None,
1222                }),
1223            ]),
1224            id: None,
1225        });
1226
1227        serde_test::assert_tokens(
1228            &component,
1229            &[
1230                Token::Struct {
1231                    name: "Component",
1232                    len: 2,
1233                },
1234                Token::Str("type"),
1235                Token::U8(ComponentType::ActionRow.into()),
1236                Token::Str("components"),
1237                Token::Seq { len: Some(2) },
1238                Token::Struct {
1239                    name: "Component",
1240                    len: 5,
1241                },
1242                Token::Str("type"),
1243                Token::U8(ComponentType::Button.into()),
1244                Token::Str("custom_id"),
1245                Token::Some,
1246                Token::Str("test custom id"),
1247                Token::Str("disabled"),
1248                Token::Bool(true),
1249                Token::Str("label"),
1250                Token::Some,
1251                Token::Str("test label"),
1252                Token::Str("style"),
1253                Token::U8(ButtonStyle::Primary.into()),
1254                Token::StructEnd,
1255                Token::Struct {
1256                    name: "Component",
1257                    len: 6,
1258                },
1259                Token::Str("type"),
1260                Token::U8(ComponentType::TextSelectMenu.into()),
1261                Token::Str("options"),
1262                Token::Seq { len: Some(1) },
1263                Token::Struct {
1264                    name: "SelectMenuOption",
1265                    len: 4,
1266                },
1267                Token::Str("default"),
1268                Token::Bool(false),
1269                Token::Str("description"),
1270                Token::Some,
1271                Token::Str("test description"),
1272                Token::Str("label"),
1273                Token::Str("test option label"),
1274                Token::Str("value"),
1275                Token::Str("test option value"),
1276                Token::StructEnd,
1277                Token::SeqEnd,
1278                Token::Str("custom_id"),
1279                Token::Some,
1280                Token::Str("test custom id 2"),
1281                Token::Str("disabled"),
1282                Token::Bool(false),
1283                Token::Str("max_values"),
1284                Token::Some,
1285                Token::U8(25),
1286                Token::Str("min_values"),
1287                Token::Some,
1288                Token::U8(5),
1289                Token::Str("placeholder"),
1290                Token::Some,
1291                Token::Str("test placeholder"),
1292                Token::StructEnd,
1293                Token::SeqEnd,
1294                Token::StructEnd,
1295            ],
1296        );
1297    }
1298
1299    #[test]
1300    fn action_row() {
1301        let value = Component::ActionRow(ActionRow {
1302            components: Vec::from([Component::Button(Button {
1303                custom_id: Some("button-1".to_owned()),
1304                disabled: false,
1305                emoji: None,
1306                style: ButtonStyle::Primary,
1307                label: Some("Button".to_owned()),
1308                url: None,
1309                sku_id: None,
1310                id: None,
1311            })]),
1312            id: None,
1313        });
1314
1315        serde_test::assert_tokens(
1316            &value,
1317            &[
1318                Token::Struct {
1319                    name: "Component",
1320                    len: 2,
1321                },
1322                Token::String("type"),
1323                Token::U8(ComponentType::ActionRow.into()),
1324                Token::String("components"),
1325                Token::Seq { len: Some(1) },
1326                Token::Struct {
1327                    name: "Component",
1328                    len: 4,
1329                },
1330                Token::String("type"),
1331                Token::U8(2),
1332                Token::String("custom_id"),
1333                Token::Some,
1334                Token::String("button-1"),
1335                Token::String("label"),
1336                Token::Some,
1337                Token::String("Button"),
1338                Token::String("style"),
1339                Token::U8(1),
1340                Token::StructEnd,
1341                Token::SeqEnd,
1342                Token::StructEnd,
1343            ],
1344        );
1345    }
1346
1347    #[test]
1348    fn button() {
1349        // Free Palestine.
1350        //
1351        // Palestinian Flag.
1352        const FLAG: &str = "🇵🇸";
1353
1354        let value = Component::Button(Button {
1355            custom_id: Some("test".to_owned()),
1356            disabled: false,
1357            emoji: Some(EmojiReactionType::Unicode {
1358                name: FLAG.to_owned(),
1359            }),
1360            label: Some("Test".to_owned()),
1361            style: ButtonStyle::Link,
1362            url: Some("https://twilight.rs".to_owned()),
1363            sku_id: None,
1364            id: None,
1365        });
1366
1367        serde_test::assert_tokens(
1368            &value,
1369            &[
1370                Token::Struct {
1371                    name: "Component",
1372                    len: 6,
1373                },
1374                Token::String("type"),
1375                Token::U8(ComponentType::Button.into()),
1376                Token::String("custom_id"),
1377                Token::Some,
1378                Token::String("test"),
1379                Token::String("emoji"),
1380                Token::Some,
1381                Token::Struct {
1382                    name: "EmojiReactionType",
1383                    len: 1,
1384                },
1385                Token::String("name"),
1386                Token::String(FLAG),
1387                Token::StructEnd,
1388                Token::String("label"),
1389                Token::Some,
1390                Token::String("Test"),
1391                Token::String("style"),
1392                Token::U8(ButtonStyle::Link.into()),
1393                Token::String("url"),
1394                Token::Some,
1395                Token::String("https://twilight.rs"),
1396                Token::StructEnd,
1397            ],
1398        );
1399    }
1400
1401    #[test]
1402    fn select_menu() {
1403        fn check_select(default_values: Option<Vec<(SelectDefaultValue, &'static str)>>) {
1404            let select_menu = Component::SelectMenu(SelectMenu {
1405                channel_types: None,
1406                custom_id: String::from("my_select"),
1407                default_values: default_values
1408                    .clone()
1409                    .map(|values| values.into_iter().map(|pair| pair.0).collect()),
1410                disabled: false,
1411                kind: SelectMenuType::User,
1412                max_values: None,
1413                min_values: None,
1414                options: None,
1415                placeholder: None,
1416                id: None,
1417            });
1418            let mut tokens = vec![
1419                Token::Struct {
1420                    name: "Component",
1421                    len: 2 + usize::from(default_values.is_some()),
1422                },
1423                Token::String("type"),
1424                Token::U8(ComponentType::UserSelectMenu.into()),
1425                Token::Str("custom_id"),
1426                Token::Some,
1427                Token::Str("my_select"),
1428            ];
1429            if let Some(default_values) = default_values {
1430                tokens.extend_from_slice(&[
1431                    Token::Str("default_values"),
1432                    Token::Some,
1433                    Token::Seq {
1434                        len: Some(default_values.len()),
1435                    },
1436                ]);
1437                for (_, id) in default_values {
1438                    tokens.extend_from_slice(&[
1439                        Token::Struct {
1440                            name: "SelectDefaultValue",
1441                            len: 2,
1442                        },
1443                        Token::Str("type"),
1444                        Token::UnitVariant {
1445                            name: "SelectDefaultValue",
1446                            variant: "user",
1447                        },
1448                        Token::Str("id"),
1449                        Token::NewtypeStruct { name: "Id" },
1450                        Token::Str(id),
1451                        Token::StructEnd,
1452                    ])
1453                }
1454                tokens.push(Token::SeqEnd);
1455            }
1456            tokens.extend_from_slice(&[
1457                Token::Str("disabled"),
1458                Token::Bool(false),
1459                Token::StructEnd,
1460            ]);
1461            serde_test::assert_tokens(&select_menu, &tokens);
1462        }
1463
1464        check_select(None);
1465        check_select(Some(vec![(
1466            SelectDefaultValue::User(Id::new(1234)),
1467            "1234",
1468        )]));
1469        check_select(Some(vec![
1470            (SelectDefaultValue::User(Id::new(1234)), "1234"),
1471            (SelectDefaultValue::User(Id::new(5432)), "5432"),
1472        ]));
1473    }
1474
1475    #[test]
1476    fn text_input() {
1477        let value = Component::TextInput(TextInput {
1478            custom_id: "test".to_owned(),
1479            label: "The label".to_owned(),
1480            max_length: Some(100),
1481            min_length: Some(1),
1482            placeholder: Some("Taking this place".to_owned()),
1483            required: Some(true),
1484            style: TextInputStyle::Short,
1485            value: Some("Hello World!".to_owned()),
1486            id: None,
1487        });
1488
1489        serde_test::assert_tokens(
1490            &value,
1491            &[
1492                Token::Struct {
1493                    name: "Component",
1494                    len: 9,
1495                },
1496                Token::String("type"),
1497                Token::U8(ComponentType::TextInput.into()),
1498                Token::String("custom_id"),
1499                Token::Some,
1500                Token::String("test"),
1501                Token::String("label"),
1502                Token::Some,
1503                Token::String("The label"),
1504                Token::String("max_length"),
1505                Token::Some,
1506                Token::U16(100),
1507                Token::String("min_length"),
1508                Token::Some,
1509                Token::U16(1),
1510                Token::String("placeholder"),
1511                Token::Some,
1512                Token::String("Taking this place"),
1513                Token::String("required"),
1514                Token::Some,
1515                Token::Bool(true),
1516                Token::String("style"),
1517                Token::U8(TextInputStyle::Short as u8),
1518                Token::String("value"),
1519                Token::Some,
1520                Token::String("Hello World!"),
1521                Token::StructEnd,
1522            ],
1523        );
1524    }
1525
1526    #[test]
1527    fn premium_button() {
1528        let value = Component::Button(Button {
1529            custom_id: None,
1530            disabled: false,
1531            emoji: None,
1532            label: None,
1533            style: ButtonStyle::Premium,
1534            url: None,
1535            sku_id: Some(Id::new(114_941_315_417_899_012)),
1536            id: None,
1537        });
1538
1539        serde_test::assert_tokens(
1540            &value,
1541            &[
1542                Token::Struct {
1543                    name: "Component",
1544                    len: 3,
1545                },
1546                Token::String("type"),
1547                Token::U8(ComponentType::Button.into()),
1548                Token::String("style"),
1549                Token::U8(ButtonStyle::Premium.into()),
1550                Token::String("sku_id"),
1551                Token::Some,
1552                Token::NewtypeStruct { name: "Id" },
1553                Token::Str("114941315417899012"),
1554                Token::StructEnd,
1555            ],
1556        );
1557    }
1558}