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 kind;
11mod select_menu;
12mod text_input;
13
14pub use self::{
15    action_row::ActionRow,
16    button::{Button, ButtonStyle},
17    kind::ComponentType,
18    select_menu::{SelectDefaultValue, SelectMenu, SelectMenuOption, SelectMenuType},
19    text_input::{TextInput, TextInputStyle},
20};
21
22use super::EmojiReactionType;
23use crate::{
24    channel::ChannelType,
25    id::{marker::SkuMarker, Id},
26};
27use serde::{
28    de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
29    ser::{Error as SerError, SerializeStruct},
30    Deserialize, Serialize, Serializer,
31};
32use serde_value::{DeserializerError, Value};
33use std::fmt::{Formatter, Result as FmtResult};
34
35/// Interactive message element.
36///
37/// Must be either a top level [`ActionRow`] or nested inside one.
38///
39/// # Examples
40///
41/// ## Button
42///
43/// ```
44/// use twilight_model::channel::message::component::{ActionRow, Button, ButtonStyle, Component};
45///
46/// Component::ActionRow(ActionRow {
47///     components: Vec::from([Component::Button(Button {
48///         custom_id: Some("click_one".to_owned()),
49///         disabled: false,
50///         emoji: None,
51///         label: Some("Click me!".to_owned()),
52///         style: ButtonStyle::Primary,
53///         url: None,
54///         sku_id: None,
55///     })]),
56/// });
57/// ```
58///
59/// ## Select menu
60///
61/// ```
62/// use twilight_model::{
63///     channel::message::{
64///         component::{ActionRow, Component, SelectMenu, SelectMenuOption, SelectMenuType},
65///         EmojiReactionType,
66///     },
67///     id::Id,
68/// };
69///
70/// Component::ActionRow(ActionRow {
71///     components: vec![Component::SelectMenu(SelectMenu {
72///         channel_types: None,
73///         custom_id: "class_select_1".to_owned(),
74///         default_values: None,
75///         disabled: false,
76///         kind: SelectMenuType::Text,
77///         max_values: Some(3),
78///         min_values: Some(1),
79///         options: Some(Vec::from([
80///             SelectMenuOption {
81///                 default: false,
82///                 emoji: Some(EmojiReactionType::Custom {
83///                     animated: false,
84///                     id: Id::new(625891304148303894),
85///                     name: Some("rogue".to_owned()),
86///                 }),
87///                 description: Some("Sneak n stab".to_owned()),
88///                 label: "Rogue".to_owned(),
89///                 value: "rogue".to_owned(),
90///             },
91///             SelectMenuOption {
92///                 default: false,
93///                 emoji: Some(EmojiReactionType::Custom {
94///                     animated: false,
95///                     id: Id::new(625891304081063986),
96///                     name: Some("mage".to_owned()),
97///                 }),
98///                 description: Some("Turn 'em into a sheep".to_owned()),
99///                 label: "Mage".to_owned(),
100///                 value: "mage".to_owned(),
101///             },
102///             SelectMenuOption {
103///                 default: false,
104///                 emoji: Some(EmojiReactionType::Custom {
105///                     animated: false,
106///                     id: Id::new(625891303795982337),
107///                     name: Some("priest".to_owned()),
108///                 }),
109///                 description: Some("You get heals when I'm done doing damage".to_owned()),
110///                 label: "Priest".to_owned(),
111///                 value: "priest".to_owned(),
112///             },
113///         ])),
114///         placeholder: Some("Choose a class".to_owned()),
115///     })],
116/// });
117/// ```
118#[derive(Clone, Debug, Eq, Hash, PartialEq)]
119pub enum Component {
120    /// Top level, non-interactive container of other (non action row) components.
121    ActionRow(ActionRow),
122    /// Clickable item that renders below messages.
123    Button(Button),
124    /// Dropdown-style item that renders below messages.
125    SelectMenu(SelectMenu),
126    /// Pop-up item that renders on modals.
127    TextInput(TextInput),
128    /// Variant value is unknown to the library.
129    Unknown(u8),
130}
131
132impl Component {
133    /// Type of component that this is.
134    ///
135    /// ```
136    /// use twilight_model::channel::message::component::{
137    ///     Button, ButtonStyle, Component, ComponentType,
138    /// };
139    ///
140    /// let component = Component::Button(Button {
141    ///     custom_id: None,
142    ///     disabled: false,
143    ///     emoji: None,
144    ///     label: Some("ping".to_owned()),
145    ///     style: ButtonStyle::Primary,
146    ///     url: None,
147    ///     sku_id: None,
148    /// });
149    ///
150    /// assert_eq!(ComponentType::Button, component.kind());
151    /// ```
152    pub const fn kind(&self) -> ComponentType {
153        match self {
154            Self::ActionRow(_) => ComponentType::ActionRow,
155            Self::Button(_) => ComponentType::Button,
156            Self::SelectMenu(SelectMenu { kind, .. }) => match kind {
157                SelectMenuType::Text => ComponentType::TextSelectMenu,
158                SelectMenuType::User => ComponentType::UserSelectMenu,
159                SelectMenuType::Role => ComponentType::RoleSelectMenu,
160                SelectMenuType::Mentionable => ComponentType::MentionableSelectMenu,
161                SelectMenuType::Channel => ComponentType::ChannelSelectMenu,
162            },
163            Self::TextInput(_) => ComponentType::TextInput,
164            Component::Unknown(unknown) => ComponentType::Unknown(*unknown),
165        }
166    }
167}
168
169impl From<ActionRow> for Component {
170    fn from(action_row: ActionRow) -> Self {
171        Self::ActionRow(action_row)
172    }
173}
174
175impl From<Button> for Component {
176    fn from(button: Button) -> Self {
177        Self::Button(button)
178    }
179}
180
181impl From<SelectMenu> for Component {
182    fn from(select_menu: SelectMenu) -> Self {
183        Self::SelectMenu(select_menu)
184    }
185}
186
187impl From<TextInput> for Component {
188    fn from(text_input: TextInput) -> Self {
189        Self::TextInput(text_input)
190    }
191}
192
193impl<'de> Deserialize<'de> for Component {
194    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
195        deserializer.deserialize_any(ComponentVisitor)
196    }
197}
198
199#[derive(Debug, Deserialize)]
200#[serde(field_identifier, rename_all = "snake_case")]
201enum Field {
202    ChannelTypes,
203    Components,
204    CustomId,
205    DefaultValues,
206    Disabled,
207    Emoji,
208    Label,
209    MaxLength,
210    MaxValues,
211    MinLength,
212    MinValues,
213    Options,
214    Placeholder,
215    Required,
216    Style,
217    Type,
218    Url,
219    SkuId,
220    Value,
221}
222
223struct ComponentVisitor;
224
225impl<'de> Visitor<'de> for ComponentVisitor {
226    type Value = Component;
227
228    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
229        f.write_str("struct Component")
230    }
231
232    #[allow(clippy::too_many_lines)]
233    fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
234        // Required fields.
235        let mut components: Option<Vec<Component>> = None;
236        let mut kind: Option<ComponentType> = None;
237        let mut options: Option<Vec<SelectMenuOption>> = None;
238        let mut style: Option<Value> = None;
239
240        // Liminal fields.
241        let mut custom_id: Option<Option<Value>> = None;
242        let mut label: Option<Option<String>> = None;
243
244        // Optional fields.
245        let mut channel_types: Option<Vec<ChannelType>> = None;
246        let mut default_values: Option<Vec<SelectDefaultValue>> = None;
247        let mut disabled: Option<bool> = None;
248        let mut emoji: Option<Option<EmojiReactionType>> = None;
249        let mut max_length: Option<Option<u16>> = None;
250        let mut max_values: Option<Option<u8>> = None;
251        let mut min_length: Option<Option<u16>> = None;
252        let mut min_values: Option<Option<u8>> = None;
253        let mut placeholder: Option<Option<String>> = None;
254        let mut required: Option<Option<bool>> = None;
255        let mut url: Option<Option<String>> = None;
256        let mut sku_id: Option<Id<SkuMarker>> = None;
257        let mut value: Option<Option<String>> = None;
258
259        loop {
260            let key = match map.next_key() {
261                Ok(Some(key)) => key,
262                Ok(None) => break,
263                Err(_) => {
264                    map.next_value::<IgnoredAny>()?;
265
266                    continue;
267                }
268            };
269
270            match key {
271                Field::ChannelTypes => {
272                    if channel_types.is_some() {
273                        return Err(DeError::duplicate_field("channel_types"));
274                    }
275
276                    channel_types = Some(map.next_value()?);
277                }
278                Field::Components => {
279                    if components.is_some() {
280                        return Err(DeError::duplicate_field("components"));
281                    }
282
283                    components = Some(map.next_value()?);
284                }
285                Field::CustomId => {
286                    if custom_id.is_some() {
287                        return Err(DeError::duplicate_field("custom_id"));
288                    }
289
290                    custom_id = Some(map.next_value()?);
291                }
292                Field::DefaultValues => {
293                    if default_values.is_some() {
294                        return Err(DeError::duplicate_field("default_values"));
295                    }
296
297                    default_values = map.next_value()?;
298                }
299                Field::Disabled => {
300                    if disabled.is_some() {
301                        return Err(DeError::duplicate_field("disabled"));
302                    }
303
304                    disabled = Some(map.next_value()?);
305                }
306                Field::Emoji => {
307                    if emoji.is_some() {
308                        return Err(DeError::duplicate_field("emoji"));
309                    }
310
311                    emoji = Some(map.next_value()?);
312                }
313                Field::Label => {
314                    if label.is_some() {
315                        return Err(DeError::duplicate_field("label"));
316                    }
317
318                    label = Some(map.next_value()?);
319                }
320                Field::MaxLength => {
321                    if max_length.is_some() {
322                        return Err(DeError::duplicate_field("max_length"));
323                    }
324
325                    max_length = Some(map.next_value()?);
326                }
327                Field::MaxValues => {
328                    if max_values.is_some() {
329                        return Err(DeError::duplicate_field("max_values"));
330                    }
331
332                    max_values = Some(map.next_value()?);
333                }
334                Field::MinLength => {
335                    if min_length.is_some() {
336                        return Err(DeError::duplicate_field("min_length"));
337                    }
338
339                    min_length = Some(map.next_value()?);
340                }
341                Field::MinValues => {
342                    if min_values.is_some() {
343                        return Err(DeError::duplicate_field("min_values"));
344                    }
345
346                    min_values = Some(map.next_value()?);
347                }
348                Field::Options => {
349                    if options.is_some() {
350                        return Err(DeError::duplicate_field("options"));
351                    }
352
353                    options = Some(map.next_value()?);
354                }
355                Field::Placeholder => {
356                    if placeholder.is_some() {
357                        return Err(DeError::duplicate_field("placeholder"));
358                    }
359
360                    placeholder = Some(map.next_value()?);
361                }
362                Field::Required => {
363                    if required.is_some() {
364                        return Err(DeError::duplicate_field("required"));
365                    }
366
367                    required = Some(map.next_value()?);
368                }
369                Field::Style => {
370                    if style.is_some() {
371                        return Err(DeError::duplicate_field("style"));
372                    }
373
374                    style = Some(map.next_value()?);
375                }
376                Field::Type => {
377                    if kind.is_some() {
378                        return Err(DeError::duplicate_field("type"));
379                    }
380
381                    kind = Some(map.next_value()?);
382                }
383                Field::Url => {
384                    if url.is_some() {
385                        return Err(DeError::duplicate_field("url"));
386                    }
387
388                    url = Some(map.next_value()?);
389                }
390                Field::SkuId => {
391                    if sku_id.is_some() {
392                        return Err(DeError::duplicate_field("sku_id"));
393                    }
394
395                    sku_id = map.next_value()?;
396                }
397                Field::Value => {
398                    if value.is_some() {
399                        return Err(DeError::duplicate_field("value"));
400                    }
401
402                    value = Some(map.next_value()?);
403                }
404            };
405        }
406
407        let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
408
409        Ok(match kind {
410            // Required fields:
411            // - components
412            ComponentType::ActionRow => {
413                let components = components.ok_or_else(|| DeError::missing_field("components"))?;
414
415                Self::Value::ActionRow(ActionRow { components })
416            }
417            // Required fields:
418            // - style
419            //
420            // Optional fields:
421            // - custom_id
422            // - disabled
423            // - emoji
424            // - label
425            // - url
426            // - sku_id
427            ComponentType::Button => {
428                let style = style
429                    .ok_or_else(|| DeError::missing_field("style"))?
430                    .deserialize_into()
431                    .map_err(DeserializerError::into_error)?;
432
433                let custom_id = custom_id
434                    .flatten()
435                    .map(Value::deserialize_into)
436                    .transpose()
437                    .map_err(DeserializerError::into_error)?;
438
439                Self::Value::Button(Button {
440                    custom_id,
441                    disabled: disabled.unwrap_or_default(),
442                    emoji: emoji.unwrap_or_default(),
443                    label: label.flatten(),
444                    style,
445                    url: url.unwrap_or_default(),
446                    sku_id,
447                })
448            }
449            // Required fields:
450            // - custom_id
451            // - options (if this is a text select menu)
452            //
453            // Optional fields:
454            // - default_values
455            // - disabled
456            // - max_values
457            // - min_values
458            // - placeholder
459            // - channel_types (if this is a channel select menu)
460            kind @ (ComponentType::TextSelectMenu
461            | ComponentType::UserSelectMenu
462            | ComponentType::RoleSelectMenu
463            | ComponentType::MentionableSelectMenu
464            | ComponentType::ChannelSelectMenu) => {
465                // Verify the individual variants' required fields
466                if let ComponentType::TextSelectMenu = kind {
467                    if options.is_none() {
468                        return Err(DeError::missing_field("options"));
469                    }
470                }
471
472                let custom_id = custom_id
473                    .flatten()
474                    .ok_or_else(|| DeError::missing_field("custom_id"))?
475                    .deserialize_into()
476                    .map_err(DeserializerError::into_error)?;
477
478                Self::Value::SelectMenu(SelectMenu {
479                    channel_types,
480                    custom_id,
481                    default_values,
482                    disabled: disabled.unwrap_or_default(),
483                    kind: match kind {
484                        ComponentType::TextSelectMenu => SelectMenuType::Text,
485                        ComponentType::UserSelectMenu => SelectMenuType::User,
486                        ComponentType::RoleSelectMenu => SelectMenuType::Role,
487                        ComponentType::MentionableSelectMenu => SelectMenuType::Mentionable,
488                        ComponentType::ChannelSelectMenu => SelectMenuType::Channel,
489                        // This branch is unreachable unless we add a new type above and forget to
490                        // also add it here
491                        _ => {
492                            unreachable!("select menu component type is only partially implemented")
493                        }
494                    },
495                    max_values: max_values.unwrap_or_default(),
496                    min_values: min_values.unwrap_or_default(),
497                    options,
498                    placeholder: placeholder.unwrap_or_default(),
499                })
500            }
501            // Required fields:
502            // - custom_id
503            // - label
504            // - style
505            //
506            // Optional fields:
507            // - max_length
508            // - min_length
509            // - placeholder
510            // - required
511            // - value
512            ComponentType::TextInput => {
513                let custom_id = custom_id
514                    .flatten()
515                    .ok_or_else(|| DeError::missing_field("custom_id"))?
516                    .deserialize_into()
517                    .map_err(DeserializerError::into_error)?;
518
519                let label = label
520                    .flatten()
521                    .ok_or_else(|| DeError::missing_field("label"))?;
522
523                let style = style
524                    .ok_or_else(|| DeError::missing_field("style"))?
525                    .deserialize_into()
526                    .map_err(DeserializerError::into_error)?;
527
528                Self::Value::TextInput(TextInput {
529                    custom_id,
530                    label,
531                    max_length: max_length.unwrap_or_default(),
532                    min_length: min_length.unwrap_or_default(),
533                    placeholder: placeholder.unwrap_or_default(),
534                    required: required.unwrap_or_default(),
535                    style,
536                    value: value.unwrap_or_default(),
537                })
538            }
539            ComponentType::Unknown(unknown) => Self::Value::Unknown(unknown),
540        })
541    }
542}
543
544impl Serialize for Component {
545    #[allow(clippy::too_many_lines)]
546    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
547        let len = match self {
548            // Required fields:
549            // - type
550            // - components
551            Component::ActionRow(_) => 2,
552            // Required fields:
553            // - type
554            // - style
555            //
556            // Optional fields:
557            // - custom_id
558            // - disabled
559            // - emoji
560            // - label
561            // - url
562            // - sku_id
563            Component::Button(button) => {
564                2 + usize::from(button.custom_id.is_some())
565                    + usize::from(button.disabled)
566                    + usize::from(button.emoji.is_some())
567                    + usize::from(button.label.is_some())
568                    + usize::from(button.url.is_some())
569                    + usize::from(button.sku_id.is_some())
570            }
571            // Required fields:
572            // - custom_id
573            // - options (for text select menus)
574            // - type
575            //
576            // Optional fields:
577            // - channel_types (for channel select menus)
578            // - default_values
579            // - disabled
580            // - max_values
581            // - min_values
582            // - placeholder
583            Component::SelectMenu(select_menu) => {
584                // We ignore text menus that don't include the `options` field, as those are
585                // detected later in the serialization process
586                2 + usize::from(select_menu.channel_types.is_some())
587                    + usize::from(select_menu.default_values.is_some())
588                    + usize::from(select_menu.disabled)
589                    + usize::from(select_menu.max_values.is_some())
590                    + usize::from(select_menu.min_values.is_some())
591                    + usize::from(select_menu.options.is_some())
592                    + usize::from(select_menu.placeholder.is_some())
593            }
594            // Required fields:
595            // - custom_id
596            // - label
597            // - style
598            // - type
599            //
600            // Optional fields:
601            // - max_length
602            // - min_length
603            // - placeholder
604            // - required
605            // - value
606            Component::TextInput(text_input) => {
607                4 + usize::from(text_input.max_length.is_some())
608                    + usize::from(text_input.min_length.is_some())
609                    + usize::from(text_input.placeholder.is_some())
610                    + usize::from(text_input.required.is_some())
611                    + usize::from(text_input.value.is_some())
612            }
613            // We are dropping fields here but nothing we can do about that for
614            // the time being.
615            Component::Unknown(_) => 1,
616        };
617
618        let mut state = serializer.serialize_struct("Component", len)?;
619
620        match self {
621            Component::ActionRow(action_row) => {
622                state.serialize_field("type", &ComponentType::ActionRow)?;
623
624                state.serialize_field("components", &action_row.components)?;
625            }
626            Component::Button(button) => {
627                state.serialize_field("type", &ComponentType::Button)?;
628
629                if button.custom_id.is_some() {
630                    state.serialize_field("custom_id", &button.custom_id)?;
631                }
632
633                if button.disabled {
634                    state.serialize_field("disabled", &button.disabled)?;
635                }
636
637                if button.emoji.is_some() {
638                    state.serialize_field("emoji", &button.emoji)?;
639                }
640
641                if button.label.is_some() {
642                    state.serialize_field("label", &button.label)?;
643                }
644
645                state.serialize_field("style", &button.style)?;
646
647                if button.url.is_some() {
648                    state.serialize_field("url", &button.url)?;
649                }
650
651                if button.sku_id.is_some() {
652                    state.serialize_field("sku_id", &button.sku_id)?;
653                }
654            }
655            Component::SelectMenu(select_menu) => {
656                match &select_menu.kind {
657                    SelectMenuType::Text => {
658                        state.serialize_field("type", &ComponentType::TextSelectMenu)?;
659                        state.serialize_field(
660                            "options",
661                            &select_menu.options.as_ref().ok_or(SerError::custom(
662                                "required field \"option\" missing for text select menu",
663                            ))?,
664                        )?;
665                    }
666                    SelectMenuType::User => {
667                        state.serialize_field("type", &ComponentType::UserSelectMenu)?;
668                    }
669                    SelectMenuType::Role => {
670                        state.serialize_field("type", &ComponentType::RoleSelectMenu)?;
671                    }
672                    SelectMenuType::Mentionable => {
673                        state.serialize_field("type", &ComponentType::MentionableSelectMenu)?;
674                    }
675                    SelectMenuType::Channel => {
676                        state.serialize_field("type", &ComponentType::ChannelSelectMenu)?;
677                        if let Some(channel_types) = &select_menu.channel_types {
678                            state.serialize_field("channel_types", channel_types)?;
679                        }
680                    }
681                }
682
683                // Due to `custom_id` being required in some variants and
684                // optional in others, serialize as an Option.
685                state.serialize_field("custom_id", &Some(&select_menu.custom_id))?;
686
687                if select_menu.default_values.is_some() {
688                    state.serialize_field("default_values", &select_menu.default_values)?;
689                }
690
691                state.serialize_field("disabled", &select_menu.disabled)?;
692
693                if select_menu.max_values.is_some() {
694                    state.serialize_field("max_values", &select_menu.max_values)?;
695                }
696
697                if select_menu.min_values.is_some() {
698                    state.serialize_field("min_values", &select_menu.min_values)?;
699                }
700
701                if select_menu.placeholder.is_some() {
702                    state.serialize_field("placeholder", &select_menu.placeholder)?;
703                }
704            }
705            Component::TextInput(text_input) => {
706                state.serialize_field("type", &ComponentType::TextInput)?;
707
708                // Due to `custom_id` and `label` being required in some
709                // variants and optional in others, serialize as an Option.
710                state.serialize_field("custom_id", &Some(&text_input.custom_id))?;
711                state.serialize_field("label", &Some(&text_input.label))?;
712
713                if text_input.max_length.is_some() {
714                    state.serialize_field("max_length", &text_input.max_length)?;
715                }
716
717                if text_input.min_length.is_some() {
718                    state.serialize_field("min_length", &text_input.min_length)?;
719                }
720
721                if text_input.placeholder.is_some() {
722                    state.serialize_field("placeholder", &text_input.placeholder)?;
723                }
724
725                if text_input.required.is_some() {
726                    state.serialize_field("required", &text_input.required)?;
727                }
728
729                state.serialize_field("style", &text_input.style)?;
730
731                if text_input.value.is_some() {
732                    state.serialize_field("value", &text_input.value)?;
733                }
734            }
735            // We are not serializing all fields so this will fail to
736            // deserialize. But it is all that can be done to avoid losing
737            // incoming messages at this time.
738            Component::Unknown(unknown) => {
739                state.serialize_field("type", &ComponentType::Unknown(*unknown))?;
740            }
741        }
742
743        state.end()
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    // Required due to the use of a unicode emoji in a constant.
750    #![allow(clippy::non_ascii_literal)]
751
752    use super::*;
753    use crate::id::Id;
754    use serde_test::Token;
755    use static_assertions::assert_impl_all;
756
757    assert_impl_all!(
758        Component: From<ActionRow>,
759        From<Button>,
760        From<SelectMenu>,
761        From<TextInput>
762    );
763
764    #[allow(clippy::too_many_lines)]
765    #[test]
766    fn component_full() {
767        let component = Component::ActionRow(ActionRow {
768            components: Vec::from([
769                Component::Button(Button {
770                    custom_id: Some("test custom id".into()),
771                    disabled: true,
772                    emoji: None,
773                    label: Some("test label".into()),
774                    style: ButtonStyle::Primary,
775                    url: None,
776                    sku_id: None,
777                }),
778                Component::SelectMenu(SelectMenu {
779                    channel_types: None,
780                    custom_id: "test custom id 2".into(),
781                    default_values: None,
782                    disabled: false,
783                    kind: SelectMenuType::Text,
784                    max_values: Some(25),
785                    min_values: Some(5),
786                    options: Some(Vec::from([SelectMenuOption {
787                        label: "test option label".into(),
788                        value: "test option value".into(),
789                        description: Some("test description".into()),
790                        emoji: None,
791                        default: false,
792                    }])),
793                    placeholder: Some("test placeholder".into()),
794                }),
795            ]),
796        });
797
798        serde_test::assert_tokens(
799            &component,
800            &[
801                Token::Struct {
802                    name: "Component",
803                    len: 2,
804                },
805                Token::Str("type"),
806                Token::U8(ComponentType::ActionRow.into()),
807                Token::Str("components"),
808                Token::Seq { len: Some(2) },
809                Token::Struct {
810                    name: "Component",
811                    len: 5,
812                },
813                Token::Str("type"),
814                Token::U8(ComponentType::Button.into()),
815                Token::Str("custom_id"),
816                Token::Some,
817                Token::Str("test custom id"),
818                Token::Str("disabled"),
819                Token::Bool(true),
820                Token::Str("label"),
821                Token::Some,
822                Token::Str("test label"),
823                Token::Str("style"),
824                Token::U8(ButtonStyle::Primary.into()),
825                Token::StructEnd,
826                Token::Struct {
827                    name: "Component",
828                    len: 6,
829                },
830                Token::Str("type"),
831                Token::U8(ComponentType::TextSelectMenu.into()),
832                Token::Str("options"),
833                Token::Seq { len: Some(1) },
834                Token::Struct {
835                    name: "SelectMenuOption",
836                    len: 4,
837                },
838                Token::Str("default"),
839                Token::Bool(false),
840                Token::Str("description"),
841                Token::Some,
842                Token::Str("test description"),
843                Token::Str("label"),
844                Token::Str("test option label"),
845                Token::Str("value"),
846                Token::Str("test option value"),
847                Token::StructEnd,
848                Token::SeqEnd,
849                Token::Str("custom_id"),
850                Token::Some,
851                Token::Str("test custom id 2"),
852                Token::Str("disabled"),
853                Token::Bool(false),
854                Token::Str("max_values"),
855                Token::Some,
856                Token::U8(25),
857                Token::Str("min_values"),
858                Token::Some,
859                Token::U8(5),
860                Token::Str("placeholder"),
861                Token::Some,
862                Token::Str("test placeholder"),
863                Token::StructEnd,
864                Token::SeqEnd,
865                Token::StructEnd,
866            ],
867        );
868    }
869
870    #[test]
871    fn action_row() {
872        let value = Component::ActionRow(ActionRow {
873            components: Vec::from([Component::Button(Button {
874                custom_id: Some("button-1".to_owned()),
875                disabled: false,
876                emoji: None,
877                style: ButtonStyle::Primary,
878                label: Some("Button".to_owned()),
879                url: None,
880                sku_id: None,
881            })]),
882        });
883
884        serde_test::assert_tokens(
885            &value,
886            &[
887                Token::Struct {
888                    name: "Component",
889                    len: 2,
890                },
891                Token::String("type"),
892                Token::U8(ComponentType::ActionRow.into()),
893                Token::String("components"),
894                Token::Seq { len: Some(1) },
895                Token::Struct {
896                    name: "Component",
897                    len: 4,
898                },
899                Token::String("type"),
900                Token::U8(2),
901                Token::String("custom_id"),
902                Token::Some,
903                Token::String("button-1"),
904                Token::String("label"),
905                Token::Some,
906                Token::String("Button"),
907                Token::String("style"),
908                Token::U8(1),
909                Token::StructEnd,
910                Token::SeqEnd,
911                Token::StructEnd,
912            ],
913        );
914    }
915
916    #[test]
917    fn button() {
918        // Free Palestine.
919        //
920        // Palestinian Flag.
921        const FLAG: &str = "🇵🇸";
922
923        let value = Component::Button(Button {
924            custom_id: Some("test".to_owned()),
925            disabled: false,
926            emoji: Some(EmojiReactionType::Unicode {
927                name: FLAG.to_owned(),
928            }),
929            label: Some("Test".to_owned()),
930            style: ButtonStyle::Link,
931            url: Some("https://twilight.rs".to_owned()),
932            sku_id: None,
933        });
934
935        serde_test::assert_tokens(
936            &value,
937            &[
938                Token::Struct {
939                    name: "Component",
940                    len: 6,
941                },
942                Token::String("type"),
943                Token::U8(ComponentType::Button.into()),
944                Token::String("custom_id"),
945                Token::Some,
946                Token::String("test"),
947                Token::String("emoji"),
948                Token::Some,
949                Token::Struct {
950                    name: "EmojiReactionType",
951                    len: 1,
952                },
953                Token::String("name"),
954                Token::String(FLAG),
955                Token::StructEnd,
956                Token::String("label"),
957                Token::Some,
958                Token::String("Test"),
959                Token::String("style"),
960                Token::U8(ButtonStyle::Link.into()),
961                Token::String("url"),
962                Token::Some,
963                Token::String("https://twilight.rs"),
964                Token::StructEnd,
965            ],
966        );
967    }
968
969    #[test]
970    fn select_menu() {
971        fn check_select(default_values: Option<Vec<(SelectDefaultValue, &'static str)>>) {
972            let select_menu = Component::SelectMenu(SelectMenu {
973                channel_types: None,
974                custom_id: String::from("my_select"),
975                default_values: default_values
976                    .clone()
977                    .map(|values| values.into_iter().map(|pair| pair.0).collect()),
978                disabled: false,
979                kind: SelectMenuType::User,
980                max_values: None,
981                min_values: None,
982                options: None,
983                placeholder: None,
984            });
985            let mut tokens = vec![
986                Token::Struct {
987                    name: "Component",
988                    len: 2 + usize::from(default_values.is_some()),
989                },
990                Token::String("type"),
991                Token::U8(ComponentType::UserSelectMenu.into()),
992                Token::Str("custom_id"),
993                Token::Some,
994                Token::Str("my_select"),
995            ];
996            if let Some(default_values) = default_values {
997                tokens.extend_from_slice(&[
998                    Token::Str("default_values"),
999                    Token::Some,
1000                    Token::Seq {
1001                        len: Some(default_values.len()),
1002                    },
1003                ]);
1004                for (_, id) in default_values {
1005                    tokens.extend_from_slice(&[
1006                        Token::Struct {
1007                            name: "SelectDefaultValue",
1008                            len: 2,
1009                        },
1010                        Token::Str("type"),
1011                        Token::UnitVariant {
1012                            name: "SelectDefaultValue",
1013                            variant: "user",
1014                        },
1015                        Token::Str("id"),
1016                        Token::NewtypeStruct { name: "Id" },
1017                        Token::Str(id),
1018                        Token::StructEnd,
1019                    ])
1020                }
1021                tokens.push(Token::SeqEnd);
1022            }
1023            tokens.extend_from_slice(&[
1024                Token::Str("disabled"),
1025                Token::Bool(false),
1026                Token::StructEnd,
1027            ]);
1028            serde_test::assert_tokens(&select_menu, &tokens);
1029        }
1030
1031        check_select(None);
1032        check_select(Some(vec![(
1033            SelectDefaultValue::User(Id::new(1234)),
1034            "1234",
1035        )]));
1036        check_select(Some(vec![
1037            (SelectDefaultValue::User(Id::new(1234)), "1234"),
1038            (SelectDefaultValue::User(Id::new(5432)), "5432"),
1039        ]));
1040    }
1041
1042    #[test]
1043    fn text_input() {
1044        let value = Component::TextInput(TextInput {
1045            custom_id: "test".to_owned(),
1046            label: "The label".to_owned(),
1047            max_length: Some(100),
1048            min_length: Some(1),
1049            placeholder: Some("Taking this place".to_owned()),
1050            required: Some(true),
1051            style: TextInputStyle::Short,
1052            value: Some("Hello World!".to_owned()),
1053        });
1054
1055        serde_test::assert_tokens(
1056            &value,
1057            &[
1058                Token::Struct {
1059                    name: "Component",
1060                    len: 9,
1061                },
1062                Token::String("type"),
1063                Token::U8(ComponentType::TextInput.into()),
1064                Token::String("custom_id"),
1065                Token::Some,
1066                Token::String("test"),
1067                Token::String("label"),
1068                Token::Some,
1069                Token::String("The label"),
1070                Token::String("max_length"),
1071                Token::Some,
1072                Token::U16(100),
1073                Token::String("min_length"),
1074                Token::Some,
1075                Token::U16(1),
1076                Token::String("placeholder"),
1077                Token::Some,
1078                Token::String("Taking this place"),
1079                Token::String("required"),
1080                Token::Some,
1081                Token::Bool(true),
1082                Token::String("style"),
1083                Token::U8(TextInputStyle::Short as u8),
1084                Token::String("value"),
1085                Token::Some,
1086                Token::String("Hello World!"),
1087                Token::StructEnd,
1088            ],
1089        );
1090    }
1091
1092    #[test]
1093    fn premium_button() {
1094        let value = Component::Button(Button {
1095            custom_id: None,
1096            disabled: false,
1097            emoji: None,
1098            label: None,
1099            style: ButtonStyle::Premium,
1100            url: None,
1101            sku_id: Some(Id::new(114_941_315_417_899_012)),
1102        });
1103
1104        serde_test::assert_tokens(
1105            &value,
1106            &[
1107                Token::Struct {
1108                    name: "Component",
1109                    len: 3,
1110                },
1111                Token::String("type"),
1112                Token::U8(ComponentType::Button.into()),
1113                Token::String("style"),
1114                Token::U8(ButtonStyle::Premium.into()),
1115                Token::String("sku_id"),
1116                Token::Some,
1117                Token::NewtypeStruct { name: "Id" },
1118                Token::Str("114941315417899012"),
1119                Token::StructEnd,
1120            ],
1121        );
1122    }
1123}