Skip to main content

twilight_model/application/interaction/modal/
mod.rs

1//! [`ModalSubmit`] interaction.
2//!
3//!
4//! [`ModalSubmit`]: crate::application::interaction::InteractionType::ModalSubmit
5mod action_row;
6mod checkbox;
7mod checkbox_group;
8mod file_upload;
9mod label;
10mod select_menu;
11mod text_display;
12mod text_input;
13
14pub use self::{
15    action_row::ModalInteractionActionRow,
16    checkbox::ModalInteractionCheckbox,
17    checkbox_group::ModalInteractionCheckboxGroup,
18    file_upload::ModalInteractionFileUpload,
19    label::ModalInteractionLabel,
20    select_menu::{
21        ModalInteractionChannelSelect, ModalInteractionMentionableSelect,
22        ModalInteractionRoleSelect, ModalInteractionStringSelect, ModalInteractionUserSelect,
23    },
24    text_display::ModalInteractionTextDisplay,
25    text_input::ModalInteractionTextInput,
26};
27use crate::application::interaction::InteractionDataResolved;
28use crate::application::interaction::modal::select_menu::ModalInteractionSelectMenu;
29use crate::channel::message::component::ComponentType;
30use crate::id::Id;
31use crate::id::marker::{ChannelMarker, GenericMarker, RoleMarker, UserMarker};
32use serde::{
33    Deserialize, Serialize, Serializer,
34    de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
35    ser::SerializeStruct,
36};
37use serde_value::{DeserializerError, Value};
38use std::fmt::Formatter;
39
40/// Data received when an [`ModalSubmit`] interaction is executed.
41///
42/// See [Discord Docs/Modal Submit Data Structure].
43///
44/// [`ModalSubmit`]: crate::application::interaction::InteractionType::ModalSubmit
45/// [Discord Docs/Modal Submit Data Structure]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-modal-submit-data-structure
46#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
47pub struct ModalInteractionData {
48    /// List of modal component responses.
49    pub components: Vec<ModalInteractionComponent>,
50    /// User defined identifier for the modal.
51    ///
52    /// See [Discord Docs/Custom ID].
53    ///
54    /// [Discord Docs/Custom ID]: https://discord.com/developers/docs/components/reference#anatomy-of-a-component-custom-id
55    pub custom_id: String,
56    /// Resolved data from select menus.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub resolved: Option<InteractionDataResolved>,
59}
60
61/// User filled in modal component.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub enum ModalInteractionComponent {
64    /// Top-level layout component. In modals, layout components are preferred over action rows.
65    ActionRow(ModalInteractionActionRow),
66    /// Dropdown selection component for channels.
67    ChannelSelect(ModalInteractionChannelSelect),
68    /// Checkbox Component
69    Checkbox(ModalInteractionCheckbox),
70    /// Checkbox Group Component.
71    CheckboxGroup(ModalInteractionCheckboxGroup),
72    /// File upload component.
73    FileUpload(ModalInteractionFileUpload),
74    /// Top-level layout component including a string label and optional description.
75    Label(ModalInteractionLabel),
76    /// Dropdown selection component for mentionables.
77    MentionableSelect(ModalInteractionMentionableSelect),
78    /// Dropdown selection component for roles.
79    RoleSelect(ModalInteractionRoleSelect),
80    /// Dropdown selection component for text.
81    StringSelect(ModalInteractionStringSelect),
82    /// Markdown text.
83    TextDisplay(ModalInteractionTextDisplay),
84    /// Text input component.
85    TextInput(ModalInteractionTextInput),
86    /// Variant value is unknown to the library in the context of modals.
87    Unknown(u8),
88    /// Dropdown selection component for users.
89    UserSelect(ModalInteractionUserSelect),
90}
91
92impl ModalInteractionComponent {
93    /// Type of component that this is.
94    pub fn kind(&self) -> ComponentType {
95        match self {
96            ModalInteractionComponent::ActionRow(_) => ComponentType::ActionRow,
97            ModalInteractionComponent::ChannelSelect(_) => ComponentType::ChannelSelectMenu,
98            ModalInteractionComponent::Checkbox(_) => ComponentType::Checkbox,
99            ModalInteractionComponent::CheckboxGroup(_) => ComponentType::CheckboxGroup,
100            ModalInteractionComponent::FileUpload(_) => ComponentType::FileUpload,
101            ModalInteractionComponent::Label(_) => ComponentType::Label,
102            ModalInteractionComponent::MentionableSelect(_) => ComponentType::MentionableSelectMenu,
103            ModalInteractionComponent::RoleSelect(_) => ComponentType::RoleSelectMenu,
104            ModalInteractionComponent::StringSelect(_) => ComponentType::TextSelectMenu,
105            ModalInteractionComponent::TextDisplay(_) => ComponentType::TextDisplay,
106            ModalInteractionComponent::TextInput(_) => ComponentType::TextInput,
107            ModalInteractionComponent::Unknown(unknown) => ComponentType::from(*unknown),
108            ModalInteractionComponent::UserSelect(_) => ComponentType::UserSelectMenu,
109        }
110    }
111}
112
113impl From<ModalInteractionActionRow> for ModalInteractionComponent {
114    fn from(action_row: ModalInteractionActionRow) -> Self {
115        Self::ActionRow(action_row)
116    }
117}
118
119impl From<ModalInteractionChannelSelect> for ModalInteractionComponent {
120    fn from(select: ModalInteractionChannelSelect) -> Self {
121        Self::ChannelSelect(select)
122    }
123}
124
125impl From<ModalInteractionCheckbox> for ModalInteractionComponent {
126    fn from(checkbox: ModalInteractionCheckbox) -> Self {
127        Self::Checkbox(checkbox)
128    }
129}
130
131impl From<ModalInteractionCheckboxGroup> for ModalInteractionComponent {
132    fn from(checkbox_group: ModalInteractionCheckboxGroup) -> Self {
133        Self::CheckboxGroup(checkbox_group)
134    }
135}
136
137impl From<ModalInteractionFileUpload> for ModalInteractionComponent {
138    fn from(file_upload: ModalInteractionFileUpload) -> Self {
139        Self::FileUpload(file_upload)
140    }
141}
142
143impl From<ModalInteractionLabel> for ModalInteractionComponent {
144    fn from(label: ModalInteractionLabel) -> Self {
145        Self::Label(label)
146    }
147}
148
149impl From<ModalInteractionMentionableSelect> for ModalInteractionComponent {
150    fn from(select: ModalInteractionMentionableSelect) -> Self {
151        Self::MentionableSelect(select)
152    }
153}
154
155impl From<ModalInteractionRoleSelect> for ModalInteractionComponent {
156    fn from(select: ModalInteractionRoleSelect) -> Self {
157        Self::RoleSelect(select)
158    }
159}
160
161impl From<ModalInteractionStringSelect> for ModalInteractionComponent {
162    fn from(select: ModalInteractionStringSelect) -> Self {
163        Self::StringSelect(select)
164    }
165}
166
167impl From<ModalInteractionTextDisplay> for ModalInteractionComponent {
168    fn from(text_display: ModalInteractionTextDisplay) -> Self {
169        Self::TextDisplay(text_display)
170    }
171}
172
173impl From<ModalInteractionTextInput> for ModalInteractionComponent {
174    fn from(text_input: ModalInteractionTextInput) -> Self {
175        Self::TextInput(text_input)
176    }
177}
178
179impl From<ModalInteractionUserSelect> for ModalInteractionComponent {
180    fn from(select: ModalInteractionUserSelect) -> Self {
181        Self::UserSelect(select)
182    }
183}
184
185impl TryFrom<ModalInteractionComponent> for ModalInteractionActionRow {
186    type Error = ModalInteractionComponent;
187
188    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
189        match value {
190            ModalInteractionComponent::ActionRow(inner) => Ok(inner),
191            _ => Err(value),
192        }
193    }
194}
195
196impl TryFrom<ModalInteractionComponent> for ModalInteractionChannelSelect {
197    type Error = ModalInteractionComponent;
198
199    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
200        match value {
201            ModalInteractionComponent::ChannelSelect(inner) => Ok(inner),
202            _ => Err(value),
203        }
204    }
205}
206
207impl TryFrom<ModalInteractionComponent> for ModalInteractionCheckbox {
208    type Error = ModalInteractionComponent;
209
210    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
211        match value {
212            ModalInteractionComponent::Checkbox(inner) => Ok(inner),
213            _ => Err(value),
214        }
215    }
216}
217
218impl TryFrom<ModalInteractionComponent> for ModalInteractionCheckboxGroup {
219    type Error = ModalInteractionComponent;
220
221    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
222        match value {
223            ModalInteractionComponent::CheckboxGroup(inner) => Ok(inner),
224            _ => Err(value),
225        }
226    }
227}
228
229impl TryFrom<ModalInteractionComponent> for ModalInteractionFileUpload {
230    type Error = ModalInteractionComponent;
231
232    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
233        match value {
234            ModalInteractionComponent::FileUpload(inner) => Ok(inner),
235            _ => Err(value),
236        }
237    }
238}
239
240impl TryFrom<ModalInteractionComponent> for ModalInteractionLabel {
241    type Error = ModalInteractionComponent;
242
243    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
244        match value {
245            ModalInteractionComponent::Label(inner) => Ok(inner),
246            _ => Err(value),
247        }
248    }
249}
250
251impl TryFrom<ModalInteractionComponent> for ModalInteractionMentionableSelect {
252    type Error = ModalInteractionComponent;
253
254    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
255        match value {
256            ModalInteractionComponent::MentionableSelect(inner) => Ok(inner),
257            _ => Err(value),
258        }
259    }
260}
261
262impl TryFrom<ModalInteractionComponent> for ModalInteractionRoleSelect {
263    type Error = ModalInteractionComponent;
264
265    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
266        match value {
267            ModalInteractionComponent::RoleSelect(inner) => Ok(inner),
268            _ => Err(value),
269        }
270    }
271}
272
273impl TryFrom<ModalInteractionComponent> for ModalInteractionStringSelect {
274    type Error = ModalInteractionComponent;
275
276    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
277        match value {
278            ModalInteractionComponent::StringSelect(inner) => Ok(inner),
279            _ => Err(value),
280        }
281    }
282}
283
284impl TryFrom<ModalInteractionComponent> for ModalInteractionTextDisplay {
285    type Error = ModalInteractionComponent;
286
287    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
288        match value {
289            ModalInteractionComponent::TextDisplay(inner) => Ok(inner),
290            _ => Err(value),
291        }
292    }
293}
294
295impl TryFrom<ModalInteractionComponent> for ModalInteractionTextInput {
296    type Error = ModalInteractionComponent;
297
298    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
299        match value {
300            ModalInteractionComponent::TextInput(inner) => Ok(inner),
301            _ => Err(value),
302        }
303    }
304}
305
306impl TryFrom<ModalInteractionComponent> for ModalInteractionUserSelect {
307    type Error = ModalInteractionComponent;
308
309    fn try_from(value: ModalInteractionComponent) -> Result<Self, Self::Error> {
310        match value {
311            ModalInteractionComponent::UserSelect(inner) => Ok(inner),
312            _ => Err(value),
313        }
314    }
315}
316
317impl<'de> Deserialize<'de> for ModalInteractionComponent {
318    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
319    where
320        D: Deserializer<'de>,
321    {
322        deserializer.deserialize_any(ModalInteractionDataComponentVisitor)
323    }
324}
325
326#[derive(Debug, Deserialize)]
327#[serde(field_identifier, rename_all = "snake_case")]
328enum Field {
329    Component,
330    Components,
331    CustomId,
332    Id,
333    Type,
334    Value,
335    Values,
336}
337
338struct ModalInteractionDataComponentVisitor;
339
340impl<'de> Visitor<'de> for ModalInteractionDataComponentVisitor {
341    type Value = ModalInteractionComponent;
342
343    fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
344        f.write_str("struct ModalInteractionDataComponent")
345    }
346
347    #[allow(clippy::too_many_lines)]
348    fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
349        fn deserialize_select_menu<'de, ValueType: Deserialize<'de>, Error: DeError>(
350            id: i32,
351            custom_id: Option<String>,
352            values: Option<Vec<Value>>,
353        ) -> Result<ModalInteractionSelectMenu<ValueType>, Error> {
354            let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
355            let values = values
356                .ok_or_else(|| DeError::missing_field("values"))?
357                .into_iter()
358                .map(Value::deserialize_into)
359                .collect::<Result<_, _>>()
360                .map_err(DeserializerError::into_error)?;
361
362            Ok(ModalInteractionSelectMenu {
363                id,
364                custom_id,
365                values,
366            })
367        }
368
369        // Required fields
370        let mut id: Option<i32> = None;
371        let mut kind: Option<ComponentType> = None;
372        let mut custom_id: Option<String> = None;
373        let mut values: Option<Vec<Value>> = None;
374        let mut components: Option<Vec<ModalInteractionComponent>> = None;
375        let mut component: Option<ModalInteractionComponent> = None;
376        let mut value: Option<String> = None;
377
378        loop {
379            let key = match map.next_key() {
380                Ok(Some(key)) => key,
381                Ok(None) => break,
382                Err(_) => {
383                    map.next_value::<IgnoredAny>()?;
384
385                    continue;
386                }
387            };
388
389            match key {
390                Field::Component => {
391                    if component.is_some() {
392                        return Err(DeError::duplicate_field("component"));
393                    }
394
395                    component = Some(map.next_value()?);
396                }
397                Field::Components => {
398                    if components.is_some() {
399                        return Err(DeError::duplicate_field("components"));
400                    }
401
402                    components = Some(map.next_value()?);
403                }
404                Field::CustomId => {
405                    if custom_id.is_some() {
406                        return Err(DeError::duplicate_field("custom_id"));
407                    }
408
409                    custom_id = Some(map.next_value()?);
410                }
411                Field::Id => {
412                    if id.is_some() {
413                        return Err(DeError::duplicate_field("id"));
414                    }
415
416                    id = Some(map.next_value()?);
417                }
418                Field::Type => {
419                    if kind.is_some() {
420                        return Err(DeError::duplicate_field("kind"));
421                    }
422
423                    kind = Some(map.next_value()?);
424                }
425                Field::Value => {
426                    if value.is_some() {
427                        return Err(DeError::duplicate_field("value"));
428                    }
429
430                    value = Some(map.next_value()?);
431                }
432                Field::Values => {
433                    if values.is_some() {
434                        return Err(DeError::duplicate_field("values"));
435                    }
436
437                    values = Some(map.next_value()?);
438                }
439            }
440        }
441
442        let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
443        let id = id.ok_or_else(|| DeError::missing_field("id"))?;
444
445        Ok(match kind {
446            ComponentType::ActionRow => {
447                let components = components.ok_or_else(|| DeError::missing_field("components"))?;
448
449                Self::Value::ActionRow(ModalInteractionActionRow { components, id })
450            }
451            ComponentType::TextSelectMenu => {
452                Self::Value::StringSelect(deserialize_select_menu::<String, _>(
453                    id, custom_id, values,
454                )?)
455            }
456            ComponentType::UserSelectMenu => {
457                Self::Value::UserSelect(deserialize_select_menu::<Id<UserMarker>, _>(
458                    id, custom_id, values,
459                )?)
460            }
461            ComponentType::RoleSelectMenu => {
462                Self::Value::RoleSelect(deserialize_select_menu::<Id<RoleMarker>, _>(
463                    id, custom_id, values,
464                )?)
465            }
466            ComponentType::MentionableSelectMenu => Self::Value::MentionableSelect(
467                deserialize_select_menu::<Id<GenericMarker>, _>(id, custom_id, values)?,
468            ),
469            ComponentType::ChannelSelectMenu => Self::Value::ChannelSelect(
470                deserialize_select_menu::<Id<ChannelMarker>, _>(id, custom_id, values)?,
471            ),
472            ComponentType::TextInput => {
473                let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
474                let value = value.ok_or_else(|| DeError::missing_field("value"))?;
475
476                Self::Value::TextInput(ModalInteractionTextInput {
477                    custom_id,
478                    id,
479                    value,
480                })
481            }
482            ComponentType::TextDisplay => {
483                Self::Value::TextDisplay(ModalInteractionTextDisplay { id })
484            }
485            ComponentType::Label => {
486                let component = component.ok_or_else(|| DeError::missing_field("component"))?;
487
488                Self::Value::Label(ModalInteractionLabel {
489                    id,
490                    component: Box::new(component),
491                })
492            }
493            ComponentType::FileUpload => {
494                let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
495                let values = values
496                    .ok_or_else(|| DeError::missing_field("values"))?
497                    .into_iter()
498                    .map(Value::deserialize_into)
499                    .collect::<Result<_, _>>()
500                    .map_err(DeserializerError::into_error)?;
501
502                Self::Value::FileUpload(ModalInteractionFileUpload {
503                    custom_id,
504                    id,
505                    values,
506                })
507            }
508            ComponentType::CheckboxGroup => {
509                let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
510                let values = values
511                    .ok_or_else(|| DeError::missing_field("values"))?
512                    .into_iter()
513                    .map(Value::deserialize_into)
514                    .collect::<Result<_, _>>()
515                    .map_err(DeserializerError::into_error)?;
516
517                Self::Value::CheckboxGroup(ModalInteractionCheckboxGroup {
518                    custom_id,
519                    id,
520                    values,
521                })
522            }
523            ComponentType::Checkbox => {
524                let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
525                let value = value.ok_or_else(|| DeError::missing_field("value"))?;
526
527                Self::Value::TextInput(ModalInteractionTextInput {
528                    custom_id,
529                    id,
530                    value,
531                })
532            }
533            ComponentType::Button
534            | ComponentType::Section
535            | ComponentType::Thumbnail
536            | ComponentType::MediaGallery
537            | ComponentType::File
538            | ComponentType::Separator
539            | ComponentType::Container
540            | ComponentType::Unknown(_) => Self::Value::Unknown(kind.into()),
541        })
542    }
543}
544
545impl Serialize for ModalInteractionComponent {
546    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
547        fn serialize_select_menu<State: SerializeStruct, ValueType: Serialize>(
548            state: &mut State,
549            select: &ModalInteractionSelectMenu<ValueType>,
550        ) -> Result<(), <State as SerializeStruct>::Error> {
551            state.serialize_field("custom_id", &select.custom_id)?;
552            state.serialize_field("id", &select.id)?;
553            state.serialize_field("values", &select.values)?;
554            Ok(())
555        }
556
557        #[allow(clippy::match_same_arms)]
558        let len = match self {
559            // Required fields:
560            // - type
561            // - id
562            // - components
563            ModalInteractionComponent::ActionRow(_) => 3,
564            // Required fields:
565            // - type
566            // - id
567            // - custom_id
568            // - values
569            ModalInteractionComponent::ChannelSelect(_)
570            | ModalInteractionComponent::MentionableSelect(_)
571            | ModalInteractionComponent::RoleSelect(_)
572            | ModalInteractionComponent::UserSelect(_)
573            | ModalInteractionComponent::StringSelect(_) => 4,
574            // Required fields:
575            // - type
576            // - id
577            // - custom_id
578            // - value
579            ModalInteractionComponent::Checkbox(_) => 4,
580            // Required fields:
581            // - type
582            // - id
583            // - custom_id
584            // - values
585            ModalInteractionComponent::CheckboxGroup(_) => 4,
586            // Required fields:
587            // - type
588            // - id
589            // - custom_id
590            // - values
591            ModalInteractionComponent::FileUpload(_) => 4,
592            // Required fields:
593            // - type
594            // - id
595            // - component
596            ModalInteractionComponent::Label(_) => 3,
597            // Required fields:
598            // - type
599            // - id
600            ModalInteractionComponent::TextDisplay(_) => 2,
601            // Required fields:
602            // - type
603            // - id
604            // - custom_id
605            // - value
606            ModalInteractionComponent::TextInput(_) => 4,
607            // We are dropping all fields but type here but nothing we can do about that for
608            // the time being
609            ModalInteractionComponent::Unknown(_) => 1,
610        };
611
612        let mut state = serializer.serialize_struct("ModalInteractionComponent", len)?;
613        state.serialize_field("type", &self.kind())?;
614
615        match self {
616            ModalInteractionComponent::ActionRow(action_row) => {
617                state.serialize_field("id", &action_row.id)?;
618                state.serialize_field("components", &action_row.components)?;
619            }
620            ModalInteractionComponent::ChannelSelect(select) => {
621                serialize_select_menu(&mut state, select)?;
622            }
623            ModalInteractionComponent::Checkbox(checkbox) => {
624                state.serialize_field("custom_id", &checkbox.custom_id)?;
625                state.serialize_field("id", &checkbox.id)?;
626                state.serialize_field("value", &checkbox.value)?;
627            }
628            ModalInteractionComponent::CheckboxGroup(checkbox_group) => {
629                state.serialize_field("custom_id", &checkbox_group.custom_id)?;
630                state.serialize_field("id", &checkbox_group.id)?;
631                state.serialize_field("values", &checkbox_group.values)?;
632            }
633            ModalInteractionComponent::FileUpload(file_upload) => {
634                state.serialize_field("custom_id", &file_upload.custom_id)?;
635                state.serialize_field("id", &file_upload.id)?;
636                state.serialize_field("values", &file_upload.values)?;
637            }
638            ModalInteractionComponent::Label(label) => {
639                state.serialize_field("id", &label.id)?;
640                state.serialize_field("component", &label.component)?;
641            }
642            ModalInteractionComponent::MentionableSelect(select) => {
643                serialize_select_menu(&mut state, select)?;
644            }
645            ModalInteractionComponent::RoleSelect(select) => {
646                serialize_select_menu(&mut state, select)?;
647            }
648            ModalInteractionComponent::StringSelect(select) => {
649                serialize_select_menu(&mut state, select)?;
650            }
651            ModalInteractionComponent::TextDisplay(text_display) => {
652                state.serialize_field("id", &text_display.id)?;
653            }
654            ModalInteractionComponent::TextInput(text_input) => {
655                state.serialize_field("custom_id", &text_input.custom_id)?;
656                state.serialize_field("id", &text_input.id)?;
657                state.serialize_field("value", &text_input.value)?;
658            }
659            // We are not serializing all fields so this will fail to
660            // deserialize. But it is all that can be done to avoid losing
661            // incoming messages at this time.
662            ModalInteractionComponent::Unknown(_) => {}
663            ModalInteractionComponent::UserSelect(select) => {
664                serialize_select_menu(&mut state, select)?;
665            }
666        }
667
668        state.end()
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675    use crate::application::interaction::InteractionChannel;
676    use crate::channel::ChannelType;
677    use crate::guild::Permissions;
678    use serde_test::Token;
679    use static_assertions::{assert_fields, assert_impl_all};
680    use std::collections::HashMap;
681    use std::fmt::Debug;
682
683    assert_fields!(ModalInteractionData: custom_id, components);
684    assert_impl_all!(
685        ModalInteractionData: Clone,
686        Debug,
687        Deserialize<'static>,
688        PartialEq,
689        Send,
690        Serialize,
691        Sync
692    );
693
694    assert_impl_all!(
695        ModalInteractionComponent: Clone,
696        Debug,
697        Deserialize<'static>,
698        Eq,
699        PartialEq,
700        Send,
701        Serialize,
702        Sync
703    );
704
705    fn label_tokens(id: i32, component_tokens: &[Token]) -> Vec<Token> {
706        let mut label_tokens = vec![
707            Token::Struct {
708                name: "ModalInteractionComponent",
709                len: 3,
710            },
711            Token::String("type"),
712            Token::U8(ComponentType::Label.into()),
713            Token::String("id"),
714            Token::I32(id),
715            Token::String("component"),
716        ];
717        label_tokens.extend_from_slice(component_tokens);
718        label_tokens.push(Token::StructEnd);
719
720        label_tokens
721    }
722
723    #[test]
724    fn modal_action_rows() {
725        let value = ModalInteractionData {
726            custom_id: "test-modal".to_owned(),
727            components: vec![ModalInteractionComponent::ActionRow(
728                ModalInteractionActionRow {
729                    id: 1,
730                    components: vec![ModalInteractionComponent::TextInput(
731                        ModalInteractionTextInput {
732                            custom_id: "the-data-id".to_owned(),
733                            id: 2,
734                            value: "input value".to_owned(),
735                        },
736                    )],
737                },
738            )],
739            resolved: None,
740        };
741
742        serde_test::assert_tokens(
743            &value,
744            &[
745                Token::Struct {
746                    name: "ModalInteractionData",
747                    len: 2,
748                },
749                Token::String("components"),
750                Token::Seq { len: Some(1) },
751                Token::Struct {
752                    name: "ModalInteractionComponent",
753                    len: 3,
754                },
755                Token::String("type"),
756                Token::U8(ComponentType::ActionRow.into()),
757                Token::String("id"),
758                Token::I32(1),
759                Token::String("components"),
760                Token::Seq { len: Some(1) },
761                Token::Struct {
762                    name: "ModalInteractionComponent",
763                    len: 4,
764                },
765                Token::String("type"),
766                Token::U8(ComponentType::TextInput.into()),
767                Token::String("custom_id"),
768                Token::String("the-data-id"),
769                Token::String("id"),
770                Token::I32(2),
771                Token::String("value"),
772                Token::String("input value"),
773                Token::StructEnd,
774                Token::SeqEnd,
775                Token::StructEnd,
776                Token::SeqEnd,
777                Token::String("custom_id"),
778                Token::String("test-modal"),
779                Token::StructEnd,
780            ],
781        );
782    }
783
784    #[test]
785    #[allow(clippy::too_many_lines)]
786    fn modal_labels() {
787        let value = ModalInteractionData {
788            custom_id: "test-modal".to_owned(),
789            components: vec![
790                ModalInteractionComponent::Label(ModalInteractionLabel {
791                    id: 1,
792                    component: Box::new(ModalInteractionComponent::TextInput(
793                        ModalInteractionTextInput {
794                            custom_id: "the-text-input-id".to_owned(),
795                            id: 2,
796                            value: "input value".to_owned(),
797                        },
798                    )),
799                }),
800                ModalInteractionComponent::Label(ModalInteractionLabel {
801                    id: 3,
802                    component: Box::new(ModalInteractionComponent::TextDisplay(
803                        ModalInteractionTextDisplay { id: 4 },
804                    )),
805                }),
806                ModalInteractionComponent::Label(ModalInteractionLabel {
807                    id: 5,
808                    component: Box::new(ModalInteractionComponent::ChannelSelect(
809                        ModalInteractionChannelSelect {
810                            id: 6,
811                            custom_id: "the-channel-select-id".to_owned(),
812                            values: vec![Id::new(42)],
813                        },
814                    )),
815                }),
816            ],
817            resolved: Some(InteractionDataResolved {
818                attachments: HashMap::new(),
819                channels: HashMap::from([(
820                    Id::new(42),
821                    InteractionChannel {
822                        id: Id::new(42),
823                        kind: ChannelType::GuildText,
824                        name: "the-channel-name".to_owned(),
825                        parent_id: None,
826                        permissions: Permissions::empty(),
827                        thread_metadata: None,
828                    },
829                )]),
830                members: HashMap::new(),
831                messages: HashMap::new(),
832                roles: HashMap::new(),
833                users: HashMap::new(),
834            }),
835        };
836
837        let text_input_tokens = [
838            Token::Struct {
839                name: "ModalInteractionComponent",
840                len: 4,
841            },
842            Token::String("type"),
843            Token::U8(ComponentType::TextInput.into()),
844            Token::String("custom_id"),
845            Token::String("the-text-input-id"),
846            Token::String("id"),
847            Token::I32(2),
848            Token::String("value"),
849            Token::String("input value"),
850            Token::StructEnd,
851        ];
852
853        let text_display_tokens = [
854            Token::Struct {
855                name: "ModalInteractionComponent",
856                len: 2,
857            },
858            Token::String("type"),
859            Token::U8(ComponentType::TextDisplay.into()),
860            Token::String("id"),
861            Token::I32(4),
862            Token::StructEnd,
863        ];
864
865        let channel_select_tokens = [
866            Token::Struct {
867                name: "ModalInteractionComponent",
868                len: 4,
869            },
870            Token::String("type"),
871            Token::U8(ComponentType::ChannelSelectMenu.into()),
872            Token::String("custom_id"),
873            Token::String("the-channel-select-id"),
874            Token::String("id"),
875            Token::I32(6),
876            Token::String("values"),
877            Token::Seq { len: Some(1) },
878            Token::NewtypeStruct { name: "Id" },
879            Token::String("42"),
880            Token::SeqEnd,
881            Token::StructEnd,
882        ];
883
884        let mut all_tokens = vec![
885            Token::Struct {
886                name: "ModalInteractionData",
887                len: 3,
888            },
889            Token::String("components"),
890            Token::Seq { len: Some(3) },
891        ];
892
893        all_tokens.extend_from_slice(&label_tokens(1, &text_input_tokens));
894        all_tokens.extend_from_slice(&label_tokens(3, &text_display_tokens));
895        all_tokens.extend_from_slice(&label_tokens(5, &channel_select_tokens));
896
897        all_tokens.extend_from_slice(&[
898            Token::SeqEnd,
899            Token::String("custom_id"),
900            Token::String("test-modal"),
901            Token::String("resolved"),
902            Token::Some,
903            Token::Struct {
904                name: "InteractionDataResolved",
905                len: 1,
906            },
907            Token::String("channels"),
908            Token::Map { len: Some(1) },
909            Token::NewtypeStruct { name: "Id" },
910            Token::String("42"),
911            Token::Struct {
912                name: "InteractionChannel",
913                len: 4,
914            },
915            Token::String("id"),
916            Token::NewtypeStruct { name: "Id" },
917            Token::String("42"),
918            Token::String("type"),
919            Token::U8(0),
920            Token::String("name"),
921            Token::String("the-channel-name"),
922            Token::String("permissions"),
923            Token::String("0"),
924            Token::StructEnd,
925            Token::MapEnd,
926            Token::StructEnd,
927            Token::StructEnd,
928        ]);
929
930        serde_test::assert_tokens(&value, &all_tokens);
931    }
932
933    #[test]
934    fn modal_file_upload() {
935        let value = ModalInteractionData {
936            custom_id: "test-modal".to_owned(),
937            components: vec![ModalInteractionComponent::FileUpload(
938                ModalInteractionFileUpload {
939                    id: 42,
940                    custom_id: "file-upload".to_owned(),
941                    values: vec![Id::new(1), Id::new(2)],
942                },
943            )],
944            // Having a None resolved data for the file upload response is not realistic,
945            // but (de)serialization of InteractionDataResolved is already tested sufficiently.
946            resolved: None,
947        };
948
949        serde_test::assert_tokens(
950            &value,
951            &[
952                Token::Struct {
953                    name: "ModalInteractionData",
954                    len: 2,
955                },
956                Token::String("components"),
957                Token::Seq { len: Some(1) },
958                Token::Struct {
959                    name: "ModalInteractionComponent",
960                    len: 4,
961                },
962                Token::String("type"),
963                Token::U8(ComponentType::FileUpload.into()),
964                Token::String("custom_id"),
965                Token::String("file-upload"),
966                Token::String("id"),
967                Token::I32(42),
968                Token::String("values"),
969                Token::Seq { len: Some(2) },
970                Token::NewtypeStruct { name: "Id" },
971                Token::Str("1"),
972                Token::NewtypeStruct { name: "Id" },
973                Token::Str("2"),
974                Token::SeqEnd,
975                Token::StructEnd,
976                Token::SeqEnd,
977                Token::String("custom_id"),
978                Token::String("test-modal"),
979                Token::StructEnd,
980            ],
981        )
982    }
983}