twilight_model/application/interaction/modal/
mod.rs

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