1mod action_row;
9mod button;
10mod container;
11mod file_display;
12mod file_upload;
13mod kind;
14mod label;
15mod media_gallery;
16mod section;
17mod select_menu;
18mod separator;
19mod text_display;
20mod text_input;
21mod thumbnail;
22mod unfurled_media;
23
24pub use self::{
25 action_row::ActionRow,
26 button::{Button, ButtonStyle},
27 container::Container,
28 file_display::FileDisplay,
29 file_upload::FileUpload,
30 kind::ComponentType,
31 label::Label,
32 media_gallery::{MediaGallery, MediaGalleryItem},
33 section::Section,
34 select_menu::{SelectDefaultValue, SelectMenu, SelectMenuOption, SelectMenuType},
35 separator::{Separator, SeparatorSpacingSize},
36 text_display::TextDisplay,
37 text_input::{TextInput, TextInputStyle},
38 thumbnail::Thumbnail,
39 unfurled_media::UnfurledMediaItem,
40};
41
42use super::EmojiReactionType;
43use crate::{
44 channel::ChannelType,
45 id::{Id, marker::SkuMarker},
46};
47use serde::{
48 Deserialize, Serialize, Serializer,
49 de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
50 ser::{Error as SerError, SerializeStruct},
51};
52use serde_value::{DeserializerError, Value};
53use std::fmt::{Formatter, Result as FmtResult};
54
55#[derive(Clone, Debug, Eq, Hash, PartialEq)]
144pub enum Component {
145 ActionRow(ActionRow),
147 Button(Button),
149 SelectMenu(SelectMenu),
151 TextInput(TextInput),
153 TextDisplay(TextDisplay),
155 MediaGallery(MediaGallery),
157 Separator(Separator),
159 File(FileDisplay),
161 Section(Section),
163 Container(Container),
165 Thumbnail(Thumbnail),
167 Label(Label),
169 FileUpload(FileUpload),
171 Unknown(u8),
173}
174
175impl Component {
176 pub const fn kind(&self) -> ComponentType {
197 match self {
198 Component::ActionRow(_) => ComponentType::ActionRow,
199 Component::Button(_) => ComponentType::Button,
200 Component::SelectMenu(SelectMenu { kind, .. }) => match kind {
201 SelectMenuType::Text => ComponentType::TextSelectMenu,
202 SelectMenuType::User => ComponentType::UserSelectMenu,
203 SelectMenuType::Role => ComponentType::RoleSelectMenu,
204 SelectMenuType::Mentionable => ComponentType::MentionableSelectMenu,
205 SelectMenuType::Channel => ComponentType::ChannelSelectMenu,
206 },
207 Component::TextInput(_) => ComponentType::TextInput,
208 Component::TextDisplay(_) => ComponentType::TextDisplay,
209 Component::MediaGallery(_) => ComponentType::MediaGallery,
210 Component::Separator(_) => ComponentType::Separator,
211 Component::File(_) => ComponentType::File,
212 Component::Section(_) => ComponentType::Section,
213 Component::Container(_) => ComponentType::Container,
214 Component::Thumbnail(_) => ComponentType::Thumbnail,
215 Component::Label(_) => ComponentType::Label,
216 Component::FileUpload(_) => ComponentType::FileUpload,
217 Component::Unknown(unknown) => ComponentType::Unknown(*unknown),
218 }
219 }
220
221 pub const fn component_count(&self) -> usize {
223 match self {
224 Component::ActionRow(action_row) => 1 + action_row.components.len(),
225 Component::Section(section) => 1 + section.components.len(),
226 Component::Container(container) => 1 + container.components.len(),
227 Component::Button(_)
228 | Component::SelectMenu(_)
229 | Component::TextInput(_)
230 | Component::TextDisplay(_)
231 | Component::MediaGallery(_)
232 | Component::Separator(_)
233 | Component::File(_)
234 | Component::Thumbnail(_)
235 | Component::FileUpload(_)
236 | Component::Unknown(_) => 1,
237 Component::Label(_) => 2,
238 }
239 }
240}
241
242impl From<ActionRow> for Component {
243 fn from(action_row: ActionRow) -> Self {
244 Self::ActionRow(action_row)
245 }
246}
247
248impl From<Button> for Component {
249 fn from(button: Button) -> Self {
250 Self::Button(button)
251 }
252}
253
254impl From<Container> for Component {
255 fn from(container: Container) -> Self {
256 Self::Container(container)
257 }
258}
259
260impl From<FileDisplay> for Component {
261 fn from(file_display: FileDisplay) -> Self {
262 Self::File(file_display)
263 }
264}
265
266impl From<MediaGallery> for Component {
267 fn from(media_gallery: MediaGallery) -> Self {
268 Self::MediaGallery(media_gallery)
269 }
270}
271
272impl From<Section> for Component {
273 fn from(section: Section) -> Self {
274 Self::Section(section)
275 }
276}
277
278impl From<SelectMenu> for Component {
279 fn from(select_menu: SelectMenu) -> Self {
280 Self::SelectMenu(select_menu)
281 }
282}
283
284impl From<Separator> for Component {
285 fn from(separator: Separator) -> Self {
286 Self::Separator(separator)
287 }
288}
289
290impl From<TextDisplay> for Component {
291 fn from(text_display: TextDisplay) -> Self {
292 Self::TextDisplay(text_display)
293 }
294}
295
296impl From<TextInput> for Component {
297 fn from(text_input: TextInput) -> Self {
298 Self::TextInput(text_input)
299 }
300}
301
302impl From<Thumbnail> for Component {
303 fn from(thumbnail: Thumbnail) -> Self {
304 Self::Thumbnail(thumbnail)
305 }
306}
307
308impl From<Label> for Component {
309 fn from(label: Label) -> Self {
310 Self::Label(label)
311 }
312}
313
314impl From<FileUpload> for Component {
315 fn from(file_upload: FileUpload) -> Self {
316 Self::FileUpload(file_upload)
317 }
318}
319
320impl<'de> Deserialize<'de> for Component {
321 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
322 deserializer.deserialize_any(ComponentVisitor)
323 }
324}
325
326#[derive(Debug, Deserialize)]
327#[serde(field_identifier, rename_all = "snake_case")]
328enum Field {
329 ChannelTypes,
330 Components,
331 CustomId,
332 DefaultValues,
333 Disabled,
334 Emoji,
335 Label,
336 MaxLength,
337 MaxValues,
338 MinLength,
339 MinValues,
340 Options,
341 Placeholder,
342 Required,
343 Style,
344 Type,
345 Url,
346 SkuId,
347 Value,
348 Id,
349 Content,
350 Items,
351 Divider,
352 Spacing,
353 File,
354 Spoiler,
355 Accessory,
356 Media,
357 Description,
358 AccentColor,
359 Component,
360}
361
362struct ComponentVisitor;
363
364impl<'de> Visitor<'de> for ComponentVisitor {
365 type Value = Component;
366
367 fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
368 f.write_str("struct Component")
369 }
370
371 #[allow(clippy::too_many_lines)]
372 fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
373 let mut components: Option<Vec<Component>> = None;
375 let mut kind: Option<ComponentType> = None;
376 let mut options: Option<Vec<SelectMenuOption>> = None;
377 let mut style: Option<Value> = None;
378
379 let mut custom_id: Option<Option<Value>> = None;
381 let mut label: Option<Option<String>> = None;
382
383 let mut channel_types: Option<Vec<ChannelType>> = None;
385 let mut default_values: Option<Vec<SelectDefaultValue>> = None;
386 let mut disabled: Option<bool> = None;
387 let mut emoji: Option<Option<EmojiReactionType>> = None;
388 let mut max_length: Option<Option<u16>> = None;
389 let mut max_values: Option<Option<u8>> = None;
390 let mut min_length: Option<Option<u16>> = None;
391 let mut min_values: Option<Option<u8>> = None;
392 let mut placeholder: Option<Option<String>> = None;
393 let mut required: Option<Option<bool>> = None;
394 let mut url: Option<Option<String>> = None;
395 let mut sku_id: Option<Id<SkuMarker>> = None;
396 let mut value: Option<Option<String>> = None;
397
398 let mut id: Option<i32> = None;
399 let mut content: Option<String> = None;
400 let mut items: Option<Vec<MediaGalleryItem>> = None;
401 let mut divider: Option<bool> = None;
402 let mut spacing: Option<SeparatorSpacingSize> = None;
403 let mut file: Option<UnfurledMediaItem> = None;
404 let mut spoiler: Option<bool> = None;
405 let mut accessory: Option<Component> = None;
406 let mut media: Option<UnfurledMediaItem> = None;
407 let mut description: Option<Option<String>> = None;
408 let mut accent_color: Option<Option<u32>> = None;
409 let mut component: Option<Component> = None;
410
411 loop {
412 let key = match map.next_key() {
413 Ok(Some(key)) => key,
414 Ok(None) => break,
415 Err(_) => {
416 map.next_value::<IgnoredAny>()?;
417
418 continue;
419 }
420 };
421
422 match key {
423 Field::ChannelTypes => {
424 if channel_types.is_some() {
425 return Err(DeError::duplicate_field("channel_types"));
426 }
427
428 channel_types = Some(map.next_value()?);
429 }
430 Field::Components => {
431 if components.is_some() {
432 return Err(DeError::duplicate_field("components"));
433 }
434
435 components = Some(map.next_value()?);
436 }
437 Field::CustomId => {
438 if custom_id.is_some() {
439 return Err(DeError::duplicate_field("custom_id"));
440 }
441
442 custom_id = Some(map.next_value()?);
443 }
444 Field::DefaultValues => {
445 if default_values.is_some() {
446 return Err(DeError::duplicate_field("default_values"));
447 }
448
449 default_values = map.next_value()?;
450 }
451 Field::Disabled => {
452 if disabled.is_some() {
453 return Err(DeError::duplicate_field("disabled"));
454 }
455
456 disabled = Some(map.next_value()?);
457 }
458 Field::Emoji => {
459 if emoji.is_some() {
460 return Err(DeError::duplicate_field("emoji"));
461 }
462
463 emoji = Some(map.next_value()?);
464 }
465 Field::Label => {
466 if label.is_some() {
467 return Err(DeError::duplicate_field("label"));
468 }
469
470 label = Some(map.next_value()?);
471 }
472 Field::MaxLength => {
473 if max_length.is_some() {
474 return Err(DeError::duplicate_field("max_length"));
475 }
476
477 max_length = Some(map.next_value()?);
478 }
479 Field::MaxValues => {
480 if max_values.is_some() {
481 return Err(DeError::duplicate_field("max_values"));
482 }
483
484 max_values = Some(map.next_value()?);
485 }
486 Field::MinLength => {
487 if min_length.is_some() {
488 return Err(DeError::duplicate_field("min_length"));
489 }
490
491 min_length = Some(map.next_value()?);
492 }
493 Field::MinValues => {
494 if min_values.is_some() {
495 return Err(DeError::duplicate_field("min_values"));
496 }
497
498 min_values = Some(map.next_value()?);
499 }
500 Field::Options => {
501 if options.is_some() {
502 return Err(DeError::duplicate_field("options"));
503 }
504
505 options = Some(map.next_value()?);
506 }
507 Field::Placeholder => {
508 if placeholder.is_some() {
509 return Err(DeError::duplicate_field("placeholder"));
510 }
511
512 placeholder = Some(map.next_value()?);
513 }
514 Field::Required => {
515 if required.is_some() {
516 return Err(DeError::duplicate_field("required"));
517 }
518
519 required = Some(map.next_value()?);
520 }
521 Field::Style => {
522 if style.is_some() {
523 return Err(DeError::duplicate_field("style"));
524 }
525
526 style = Some(map.next_value()?);
527 }
528 Field::Type => {
529 if kind.is_some() {
530 return Err(DeError::duplicate_field("type"));
531 }
532
533 kind = Some(map.next_value()?);
534 }
535 Field::Url => {
536 if url.is_some() {
537 return Err(DeError::duplicate_field("url"));
538 }
539
540 url = Some(map.next_value()?);
541 }
542 Field::SkuId => {
543 if sku_id.is_some() {
544 return Err(DeError::duplicate_field("sku_id"));
545 }
546
547 sku_id = map.next_value()?;
548 }
549 Field::Value => {
550 if value.is_some() {
551 return Err(DeError::duplicate_field("value"));
552 }
553
554 value = Some(map.next_value()?);
555 }
556 Field::Id => {
557 if id.is_some() {
558 return Err(DeError::duplicate_field("id"));
559 }
560
561 id = Some(map.next_value()?);
562 }
563 Field::Content => {
564 if content.is_some() {
565 return Err(DeError::duplicate_field("content"));
566 }
567
568 content = Some(map.next_value()?);
569 }
570 Field::Items => {
571 if items.is_some() {
572 return Err(DeError::duplicate_field("items"));
573 }
574
575 items = Some(map.next_value()?);
576 }
577 Field::Divider => {
578 if divider.is_some() {
579 return Err(DeError::duplicate_field("divider"));
580 }
581
582 divider = Some(map.next_value()?);
583 }
584 Field::Spacing => {
585 if spacing.is_some() {
586 return Err(DeError::duplicate_field("spacing"));
587 }
588
589 spacing = Some(map.next_value()?);
590 }
591 Field::File => {
592 if file.is_some() {
593 return Err(DeError::duplicate_field("file"));
594 }
595
596 file = Some(map.next_value()?);
597 }
598 Field::Spoiler => {
599 if spoiler.is_some() {
600 return Err(DeError::duplicate_field("spoiler"));
601 }
602
603 spoiler = Some(map.next_value()?);
604 }
605 Field::Accessory => {
606 if accessory.is_some() {
607 return Err(DeError::duplicate_field("accessory"));
608 }
609
610 accessory = Some(map.next_value()?);
611 }
612 Field::Media => {
613 if media.is_some() {
614 return Err(DeError::duplicate_field("media"));
615 }
616
617 media = Some(map.next_value()?);
618 }
619 Field::Description => {
620 if description.is_some() {
621 return Err(DeError::duplicate_field("description"));
622 }
623
624 description = Some(map.next_value()?);
625 }
626 Field::AccentColor => {
627 if accent_color.is_some() {
628 return Err(DeError::duplicate_field("accent_color"));
629 }
630
631 accent_color = Some(map.next_value()?);
632 }
633 Field::Component => {
634 if component.is_some() {
635 return Err(DeError::duplicate_field("component"));
636 }
637
638 component = Some(map.next_value()?);
639 }
640 }
641 }
642
643 let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
644
645 Ok(match kind {
646 ComponentType::ActionRow => {
649 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
650
651 Self::Value::ActionRow(ActionRow { id, components })
652 }
653 ComponentType::Button => {
664 let style = style
665 .ok_or_else(|| DeError::missing_field("style"))?
666 .deserialize_into()
667 .map_err(DeserializerError::into_error)?;
668
669 let custom_id = custom_id
670 .flatten()
671 .map(Value::deserialize_into)
672 .transpose()
673 .map_err(DeserializerError::into_error)?;
674
675 Self::Value::Button(Button {
676 custom_id,
677 disabled: disabled.unwrap_or_default(),
678 emoji: emoji.unwrap_or_default(),
679 label: label.flatten(),
680 style,
681 url: url.unwrap_or_default(),
682 sku_id,
683 id,
684 })
685 }
686 kind @ (ComponentType::TextSelectMenu
699 | ComponentType::UserSelectMenu
700 | ComponentType::RoleSelectMenu
701 | ComponentType::MentionableSelectMenu
702 | ComponentType::ChannelSelectMenu) => {
703 if let ComponentType::TextSelectMenu = kind
705 && options.is_none()
706 {
707 return Err(DeError::missing_field("options"));
708 }
709
710 let custom_id = custom_id
711 .flatten()
712 .ok_or_else(|| DeError::missing_field("custom_id"))?
713 .deserialize_into()
714 .map_err(DeserializerError::into_error)?;
715
716 Self::Value::SelectMenu(SelectMenu {
717 channel_types,
718 custom_id,
719 default_values,
720 disabled: disabled.unwrap_or_default(),
721 kind: match kind {
722 ComponentType::TextSelectMenu => SelectMenuType::Text,
723 ComponentType::UserSelectMenu => SelectMenuType::User,
724 ComponentType::RoleSelectMenu => SelectMenuType::Role,
725 ComponentType::MentionableSelectMenu => SelectMenuType::Mentionable,
726 ComponentType::ChannelSelectMenu => SelectMenuType::Channel,
727 _ => {
730 unreachable!("select menu component type is only partially implemented")
731 }
732 },
733 max_values: max_values.unwrap_or_default(),
734 min_values: min_values.unwrap_or_default(),
735 options,
736 placeholder: placeholder.unwrap_or_default(),
737 id,
738 required: required.unwrap_or_default(),
739 })
740 }
741 ComponentType::TextInput => {
753 let custom_id = custom_id
754 .flatten()
755 .ok_or_else(|| DeError::missing_field("custom_id"))?
756 .deserialize_into()
757 .map_err(DeserializerError::into_error)?;
758
759 let style = style
760 .ok_or_else(|| DeError::missing_field("style"))?
761 .deserialize_into()
762 .map_err(DeserializerError::into_error)?;
763
764 #[allow(deprecated)]
765 Self::Value::TextInput(TextInput {
766 custom_id,
767 label: label.unwrap_or_default(),
768 max_length: max_length.unwrap_or_default(),
769 min_length: min_length.unwrap_or_default(),
770 placeholder: placeholder.unwrap_or_default(),
771 required: required.unwrap_or_default(),
772 style,
773 value: value.unwrap_or_default(),
774 id,
775 })
776 }
777 ComponentType::TextDisplay => {
778 let content = content.ok_or_else(|| DeError::missing_field("content"))?;
779
780 Self::Value::TextDisplay(TextDisplay { id, content })
781 }
782 ComponentType::MediaGallery => {
783 let items = items.ok_or_else(|| DeError::missing_field("items"))?;
784
785 Self::Value::MediaGallery(MediaGallery { id, items })
786 }
787 ComponentType::Separator => Self::Value::Separator(Separator {
788 id,
789 divider,
790 spacing,
791 }),
792 ComponentType::File => {
793 let file = file.ok_or_else(|| DeError::missing_field("file"))?;
794
795 Self::Value::File(FileDisplay { id, file, spoiler })
796 }
797 ComponentType::Unknown(unknown) => Self::Value::Unknown(unknown),
798 ComponentType::Section => {
799 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
800 let accessory = accessory.ok_or_else(|| DeError::missing_field("accessory"))?;
801 Self::Value::Section(Section {
802 id,
803 components,
804 accessory: Box::new(accessory),
805 })
806 }
807 ComponentType::Thumbnail => {
808 let media = media.ok_or_else(|| DeError::missing_field("media"))?;
809 Self::Value::Thumbnail(Thumbnail {
810 id,
811 media,
812 description,
813 spoiler,
814 })
815 }
816 ComponentType::Container => {
817 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
818 Self::Value::Container(Container {
819 id,
820 accent_color,
821 spoiler,
822 components,
823 })
824 }
825 ComponentType::Label => {
826 let label = label
827 .flatten()
828 .ok_or_else(|| DeError::missing_field("label"))?;
829 let component = component.ok_or_else(|| DeError::missing_field("component"))?;
830 Self::Value::Label(Label {
831 id,
832 label,
833 description: description.unwrap_or_default(),
834 component: Box::new(component),
835 })
836 }
837 ComponentType::FileUpload => {
838 let custom_id = custom_id
839 .flatten()
840 .ok_or_else(|| DeError::missing_field("custom_id"))?
841 .deserialize_into()
842 .map_err(DeserializerError::into_error)?;
843
844 Self::Value::FileUpload(FileUpload {
845 id,
846 custom_id,
847 max_values: max_values.unwrap_or_default(),
848 min_values: min_values.unwrap_or_default(),
849 required: required.unwrap_or_default(),
850 })
851 }
852 })
853 }
854}
855
856impl Serialize for Component {
857 #[allow(clippy::too_many_lines)]
858 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
859 let len = match self {
860 Component::ActionRow(row) => 2 + usize::from(row.id.is_some()),
867 Component::Button(button) => {
880 2 + usize::from(button.custom_id.is_some())
881 + usize::from(button.disabled)
882 + usize::from(button.emoji.is_some())
883 + usize::from(button.label.is_some())
884 + usize::from(button.url.is_some())
885 + usize::from(button.sku_id.is_some())
886 + usize::from(button.id.is_some())
887 }
888 Component::SelectMenu(select_menu) => {
903 2 + usize::from(select_menu.channel_types.is_some())
906 + usize::from(select_menu.default_values.is_some())
907 + usize::from(select_menu.disabled)
908 + usize::from(select_menu.max_values.is_some())
909 + usize::from(select_menu.min_values.is_some())
910 + usize::from(select_menu.options.is_some())
911 + usize::from(select_menu.placeholder.is_some())
912 + usize::from(select_menu.id.is_some())
913 + usize::from(select_menu.required.is_some())
914 }
915 #[allow(deprecated)]
929 Component::TextInput(text_input) => {
930 3 + usize::from(text_input.label.is_some())
931 + usize::from(text_input.max_length.is_some())
932 + usize::from(text_input.min_length.is_some())
933 + usize::from(text_input.placeholder.is_some())
934 + usize::from(text_input.required.is_some())
935 + usize::from(text_input.value.is_some())
936 + usize::from(text_input.id.is_some())
937 }
938 Component::TextDisplay(text_display) => 2 + usize::from(text_display.id.is_some()),
944 Component::MediaGallery(media_gallery) => 2 + usize::from(media_gallery.id.is_some()),
950 Component::Separator(separator) => {
957 1 + usize::from(separator.divider.is_some())
958 + usize::from(separator.spacing.is_some())
959 + usize::from(separator.id.is_some())
960 }
961 Component::File(file) => {
968 2 + usize::from(file.spoiler.is_some()) + usize::from(file.id.is_some())
969 }
970 Component::Section(section) => 3 + usize::from(section.id.is_some()),
977 Component::Container(container) => {
985 2 + usize::from(container.accent_color.is_some())
986 + usize::from(container.spoiler.is_some())
987 + usize::from(container.id.is_some())
988 }
989 Component::Thumbnail(thumbnail) => {
997 2 + usize::from(thumbnail.spoiler.is_some())
998 + usize::from(thumbnail.description.is_some())
999 + usize::from(thumbnail.id.is_some())
1000 }
1001 Component::Label(label) => {
1009 3 + usize::from(label.description.is_some()) + usize::from(label.id.is_some())
1010 }
1011 Component::FileUpload(file_upload) => {
1020 2 + usize::from(file_upload.min_values.is_some())
1021 + usize::from(file_upload.max_values.is_some())
1022 + usize::from(file_upload.required.is_some())
1023 + usize::from(file_upload.id.is_some())
1024 }
1025 Component::Unknown(_) => 1,
1028 };
1029
1030 let mut state = serializer.serialize_struct("Component", len)?;
1031
1032 match self {
1033 Component::ActionRow(action_row) => {
1034 state.serialize_field("type", &ComponentType::ActionRow)?;
1035 if let Some(id) = action_row.id {
1036 state.serialize_field("id", &id)?;
1037 }
1038
1039 state.serialize_field("components", &action_row.components)?;
1040 }
1041 Component::Button(button) => {
1042 state.serialize_field("type", &ComponentType::Button)?;
1043 if let Some(id) = button.id {
1044 state.serialize_field("id", &id)?;
1045 }
1046
1047 if button.custom_id.is_some() {
1048 state.serialize_field("custom_id", &button.custom_id)?;
1049 }
1050
1051 if button.disabled {
1052 state.serialize_field("disabled", &button.disabled)?;
1053 }
1054
1055 if button.emoji.is_some() {
1056 state.serialize_field("emoji", &button.emoji)?;
1057 }
1058
1059 if button.label.is_some() {
1060 state.serialize_field("label", &button.label)?;
1061 }
1062
1063 state.serialize_field("style", &button.style)?;
1064
1065 if button.url.is_some() {
1066 state.serialize_field("url", &button.url)?;
1067 }
1068
1069 if button.sku_id.is_some() {
1070 state.serialize_field("sku_id", &button.sku_id)?;
1071 }
1072 }
1073 Component::SelectMenu(select_menu) => {
1074 match &select_menu.kind {
1075 SelectMenuType::Text => {
1076 state.serialize_field("type", &ComponentType::TextSelectMenu)?;
1077 if let Some(id) = select_menu.id {
1078 state.serialize_field("id", &id)?;
1079 }
1080
1081 state.serialize_field(
1082 "options",
1083 &select_menu.options.as_ref().ok_or(SerError::custom(
1084 "required field \"option\" missing for text select menu",
1085 ))?,
1086 )?;
1087 }
1088 SelectMenuType::User => {
1089 state.serialize_field("type", &ComponentType::UserSelectMenu)?;
1090 if let Some(id) = select_menu.id {
1091 state.serialize_field("id", &id)?;
1092 }
1093 }
1094 SelectMenuType::Role => {
1095 state.serialize_field("type", &ComponentType::RoleSelectMenu)?;
1096 if let Some(id) = select_menu.id {
1097 state.serialize_field("id", &id)?;
1098 }
1099 }
1100 SelectMenuType::Mentionable => {
1101 state.serialize_field("type", &ComponentType::MentionableSelectMenu)?;
1102 if let Some(id) = select_menu.id {
1103 state.serialize_field("id", &id)?;
1104 }
1105 }
1106 SelectMenuType::Channel => {
1107 state.serialize_field("type", &ComponentType::ChannelSelectMenu)?;
1108 if let Some(id) = select_menu.id {
1109 state.serialize_field("id", &id)?;
1110 }
1111
1112 if let Some(channel_types) = &select_menu.channel_types {
1113 state.serialize_field("channel_types", channel_types)?;
1114 }
1115 }
1116 }
1117
1118 state.serialize_field("custom_id", &Some(&select_menu.custom_id))?;
1121
1122 if select_menu.default_values.is_some() {
1123 state.serialize_field("default_values", &select_menu.default_values)?;
1124 }
1125
1126 state.serialize_field("disabled", &select_menu.disabled)?;
1127
1128 if select_menu.max_values.is_some() {
1129 state.serialize_field("max_values", &select_menu.max_values)?;
1130 }
1131
1132 if select_menu.min_values.is_some() {
1133 state.serialize_field("min_values", &select_menu.min_values)?;
1134 }
1135
1136 if select_menu.placeholder.is_some() {
1137 state.serialize_field("placeholder", &select_menu.placeholder)?;
1138 }
1139
1140 if select_menu.required.is_some() {
1141 state.serialize_field("required", &select_menu.required)?;
1142 }
1143 }
1144 Component::TextInput(text_input) => {
1145 state.serialize_field("type", &ComponentType::TextInput)?;
1146 if let Some(id) = text_input.id {
1147 state.serialize_field("id", &id)?;
1148 }
1149
1150 state.serialize_field("custom_id", &Some(&text_input.custom_id))?;
1153
1154 #[allow(deprecated)]
1155 if text_input.label.is_some() {
1156 state.serialize_field("label", &text_input.label)?;
1157 }
1158
1159 if text_input.max_length.is_some() {
1160 state.serialize_field("max_length", &text_input.max_length)?;
1161 }
1162
1163 if text_input.min_length.is_some() {
1164 state.serialize_field("min_length", &text_input.min_length)?;
1165 }
1166
1167 if text_input.placeholder.is_some() {
1168 state.serialize_field("placeholder", &text_input.placeholder)?;
1169 }
1170
1171 if text_input.required.is_some() {
1172 state.serialize_field("required", &text_input.required)?;
1173 }
1174
1175 state.serialize_field("style", &text_input.style)?;
1176
1177 if text_input.value.is_some() {
1178 state.serialize_field("value", &text_input.value)?;
1179 }
1180 }
1181 Component::TextDisplay(text_display) => {
1182 state.serialize_field("type", &ComponentType::TextDisplay)?;
1183 if let Some(id) = text_display.id {
1184 state.serialize_field("id", &id)?;
1185 }
1186
1187 state.serialize_field("content", &text_display.content)?;
1188 }
1189 Component::MediaGallery(media_gallery) => {
1190 state.serialize_field("type", &ComponentType::MediaGallery)?;
1191 if let Some(id) = media_gallery.id {
1192 state.serialize_field("id", &id)?;
1193 }
1194
1195 state.serialize_field("items", &media_gallery.items)?;
1196 }
1197 Component::Separator(separator) => {
1198 state.serialize_field("type", &ComponentType::Separator)?;
1199 if let Some(id) = separator.id {
1200 state.serialize_field("id", &id)?;
1201 }
1202 if let Some(divider) = separator.divider {
1203 state.serialize_field("divider", ÷r)?;
1204 }
1205 if let Some(spacing) = &separator.spacing {
1206 state.serialize_field("spacing", spacing)?;
1207 }
1208 }
1209 Component::File(file) => {
1210 state.serialize_field("type", &ComponentType::File)?;
1211 if let Some(id) = file.id {
1212 state.serialize_field("id", &id)?;
1213 }
1214
1215 state.serialize_field("file", &file.file)?;
1216 if let Some(spoiler) = file.spoiler {
1217 state.serialize_field("spoiler", &spoiler)?;
1218 }
1219 }
1220 Component::Section(section) => {
1221 state.serialize_field("type", &ComponentType::Section)?;
1222 if let Some(id) = section.id {
1223 state.serialize_field("id", &id)?;
1224 }
1225
1226 state.serialize_field("components", §ion.components)?;
1227 state.serialize_field("accessory", §ion.accessory)?;
1228 }
1229 Component::Container(container) => {
1230 state.serialize_field("type", &ComponentType::Container)?;
1231 if let Some(id) = container.id {
1232 state.serialize_field("id", &id)?;
1233 }
1234
1235 if let Some(accent_color) = container.accent_color {
1236 state.serialize_field("accent_color", &accent_color)?;
1237 }
1238 if let Some(spoiler) = container.spoiler {
1239 state.serialize_field("spoiler", &spoiler)?;
1240 }
1241 state.serialize_field("components", &container.components)?;
1242 }
1243 Component::Thumbnail(thumbnail) => {
1244 state.serialize_field("type", &ComponentType::Thumbnail)?;
1245 if let Some(id) = thumbnail.id {
1246 state.serialize_field("id", &id)?;
1247 }
1248
1249 state.serialize_field("media", &thumbnail.media)?;
1250 if let Some(description) = &thumbnail.description {
1251 state.serialize_field("description", description)?;
1252 }
1253 if let Some(spoiler) = thumbnail.spoiler {
1254 state.serialize_field("spoiler", &spoiler)?;
1255 }
1256 }
1257 Component::Label(label) => {
1258 state.serialize_field("type", &ComponentType::Label)?;
1259 if label.id.is_some() {
1260 state.serialize_field("id", &label.id)?;
1261 }
1262 state.serialize_field("label", &Some(&label.label))?;
1265 if label.description.is_some() {
1266 state.serialize_field("description", &label.description)?;
1267 }
1268 state.serialize_field("component", &label.component)?;
1269 }
1270 Component::FileUpload(file_upload) => {
1271 state.serialize_field("type", &ComponentType::FileUpload)?;
1272 if file_upload.id.is_some() {
1273 state.serialize_field("id", &file_upload.id)?;
1274 }
1275
1276 state.serialize_field("custom_id", &Some(&file_upload.custom_id))?;
1279 if file_upload.min_values.is_some() {
1280 state.serialize_field("min_values", &file_upload.min_values)?;
1281 }
1282 if file_upload.max_values.is_some() {
1283 state.serialize_field("max_values", &file_upload.max_values)?;
1284 }
1285 if file_upload.required.is_some() {
1286 state.serialize_field("required", &file_upload.required)?;
1287 }
1288 }
1289 Component::Unknown(unknown) => {
1293 state.serialize_field("type", &ComponentType::Unknown(*unknown))?;
1294 }
1295 }
1296
1297 state.end()
1298 }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 #![allow(clippy::non_ascii_literal)]
1305
1306 use super::*;
1307 use crate::id::Id;
1308 use serde_test::Token;
1309 use static_assertions::assert_impl_all;
1310
1311 assert_impl_all!(
1312 Component: From<ActionRow>,
1313 From<Button>,
1314 From<SelectMenu>,
1315 From<TextInput>
1316 );
1317
1318 #[allow(clippy::too_many_lines)]
1319 #[test]
1320 fn component_full() {
1321 let component = Component::ActionRow(ActionRow {
1322 components: Vec::from([
1323 Component::Button(Button {
1324 custom_id: Some("test custom id".into()),
1325 disabled: true,
1326 emoji: None,
1327 label: Some("test label".into()),
1328 style: ButtonStyle::Primary,
1329 url: None,
1330 sku_id: None,
1331 id: None,
1332 }),
1333 Component::SelectMenu(SelectMenu {
1334 channel_types: None,
1335 custom_id: "test custom id 2".into(),
1336 default_values: None,
1337 disabled: false,
1338 kind: SelectMenuType::Text,
1339 max_values: Some(25),
1340 min_values: Some(5),
1341 options: Some(Vec::from([SelectMenuOption {
1342 label: "test option label".into(),
1343 value: "test option value".into(),
1344 description: Some("test description".into()),
1345 emoji: None,
1346 default: false,
1347 }])),
1348 placeholder: Some("test placeholder".into()),
1349 id: None,
1350 required: Some(true),
1351 }),
1352 ]),
1353 id: None,
1354 });
1355
1356 serde_test::assert_tokens(
1357 &component,
1358 &[
1359 Token::Struct {
1360 name: "Component",
1361 len: 2,
1362 },
1363 Token::Str("type"),
1364 Token::U8(ComponentType::ActionRow.into()),
1365 Token::Str("components"),
1366 Token::Seq { len: Some(2) },
1367 Token::Struct {
1368 name: "Component",
1369 len: 5,
1370 },
1371 Token::Str("type"),
1372 Token::U8(ComponentType::Button.into()),
1373 Token::Str("custom_id"),
1374 Token::Some,
1375 Token::Str("test custom id"),
1376 Token::Str("disabled"),
1377 Token::Bool(true),
1378 Token::Str("label"),
1379 Token::Some,
1380 Token::Str("test label"),
1381 Token::Str("style"),
1382 Token::U8(ButtonStyle::Primary.into()),
1383 Token::StructEnd,
1384 Token::Struct {
1385 name: "Component",
1386 len: 7,
1387 },
1388 Token::Str("type"),
1389 Token::U8(ComponentType::TextSelectMenu.into()),
1390 Token::Str("options"),
1391 Token::Seq { len: Some(1) },
1392 Token::Struct {
1393 name: "SelectMenuOption",
1394 len: 4,
1395 },
1396 Token::Str("default"),
1397 Token::Bool(false),
1398 Token::Str("description"),
1399 Token::Some,
1400 Token::Str("test description"),
1401 Token::Str("label"),
1402 Token::Str("test option label"),
1403 Token::Str("value"),
1404 Token::Str("test option value"),
1405 Token::StructEnd,
1406 Token::SeqEnd,
1407 Token::Str("custom_id"),
1408 Token::Some,
1409 Token::Str("test custom id 2"),
1410 Token::Str("disabled"),
1411 Token::Bool(false),
1412 Token::Str("max_values"),
1413 Token::Some,
1414 Token::U8(25),
1415 Token::Str("min_values"),
1416 Token::Some,
1417 Token::U8(5),
1418 Token::Str("placeholder"),
1419 Token::Some,
1420 Token::Str("test placeholder"),
1421 Token::Str("required"),
1422 Token::Some,
1423 Token::Bool(true),
1424 Token::StructEnd,
1425 Token::SeqEnd,
1426 Token::StructEnd,
1427 ],
1428 );
1429 }
1430
1431 #[test]
1432 fn action_row() {
1433 let value = Component::ActionRow(ActionRow {
1434 components: Vec::from([Component::Button(Button {
1435 custom_id: Some("button-1".to_owned()),
1436 disabled: false,
1437 emoji: None,
1438 style: ButtonStyle::Primary,
1439 label: Some("Button".to_owned()),
1440 url: None,
1441 sku_id: None,
1442 id: None,
1443 })]),
1444 id: None,
1445 });
1446
1447 serde_test::assert_tokens(
1448 &value,
1449 &[
1450 Token::Struct {
1451 name: "Component",
1452 len: 2,
1453 },
1454 Token::String("type"),
1455 Token::U8(ComponentType::ActionRow.into()),
1456 Token::String("components"),
1457 Token::Seq { len: Some(1) },
1458 Token::Struct {
1459 name: "Component",
1460 len: 4,
1461 },
1462 Token::String("type"),
1463 Token::U8(2),
1464 Token::String("custom_id"),
1465 Token::Some,
1466 Token::String("button-1"),
1467 Token::String("label"),
1468 Token::Some,
1469 Token::String("Button"),
1470 Token::String("style"),
1471 Token::U8(1),
1472 Token::StructEnd,
1473 Token::SeqEnd,
1474 Token::StructEnd,
1475 ],
1476 );
1477 }
1478
1479 #[test]
1480 fn button() {
1481 const FLAG: &str = "🇵🇸";
1485
1486 let value = Component::Button(Button {
1487 custom_id: Some("test".to_owned()),
1488 disabled: false,
1489 emoji: Some(EmojiReactionType::Unicode {
1490 name: FLAG.to_owned(),
1491 }),
1492 label: Some("Test".to_owned()),
1493 style: ButtonStyle::Link,
1494 url: Some("https://twilight.rs".to_owned()),
1495 sku_id: None,
1496 id: None,
1497 });
1498
1499 serde_test::assert_tokens(
1500 &value,
1501 &[
1502 Token::Struct {
1503 name: "Component",
1504 len: 6,
1505 },
1506 Token::String("type"),
1507 Token::U8(ComponentType::Button.into()),
1508 Token::String("custom_id"),
1509 Token::Some,
1510 Token::String("test"),
1511 Token::String("emoji"),
1512 Token::Some,
1513 Token::Struct {
1514 name: "EmojiReactionType",
1515 len: 1,
1516 },
1517 Token::String("name"),
1518 Token::String(FLAG),
1519 Token::StructEnd,
1520 Token::String("label"),
1521 Token::Some,
1522 Token::String("Test"),
1523 Token::String("style"),
1524 Token::U8(ButtonStyle::Link.into()),
1525 Token::String("url"),
1526 Token::Some,
1527 Token::String("https://twilight.rs"),
1528 Token::StructEnd,
1529 ],
1530 );
1531 }
1532
1533 #[test]
1534 fn select_menu() {
1535 fn check_select(default_values: Option<Vec<(SelectDefaultValue, &'static str)>>) {
1536 let select_menu = Component::SelectMenu(SelectMenu {
1537 channel_types: None,
1538 custom_id: String::from("my_select"),
1539 default_values: default_values
1540 .clone()
1541 .map(|values| values.into_iter().map(|pair| pair.0).collect()),
1542 disabled: false,
1543 kind: SelectMenuType::User,
1544 max_values: None,
1545 min_values: None,
1546 options: None,
1547 placeholder: None,
1548 id: None,
1549 required: None,
1550 });
1551 let mut tokens = vec![
1552 Token::Struct {
1553 name: "Component",
1554 len: 2 + usize::from(default_values.is_some()),
1555 },
1556 Token::String("type"),
1557 Token::U8(ComponentType::UserSelectMenu.into()),
1558 Token::Str("custom_id"),
1559 Token::Some,
1560 Token::Str("my_select"),
1561 ];
1562 if let Some(default_values) = default_values {
1563 tokens.extend_from_slice(&[
1564 Token::Str("default_values"),
1565 Token::Some,
1566 Token::Seq {
1567 len: Some(default_values.len()),
1568 },
1569 ]);
1570 for (_, id) in default_values {
1571 tokens.extend_from_slice(&[
1572 Token::Struct {
1573 name: "SelectDefaultValue",
1574 len: 2,
1575 },
1576 Token::Str("type"),
1577 Token::UnitVariant {
1578 name: "SelectDefaultValue",
1579 variant: "user",
1580 },
1581 Token::Str("id"),
1582 Token::NewtypeStruct { name: "Id" },
1583 Token::Str(id),
1584 Token::StructEnd,
1585 ])
1586 }
1587 tokens.push(Token::SeqEnd);
1588 }
1589 tokens.extend_from_slice(&[
1590 Token::Str("disabled"),
1591 Token::Bool(false),
1592 Token::StructEnd,
1593 ]);
1594 serde_test::assert_tokens(&select_menu, &tokens);
1595 }
1596
1597 check_select(None);
1598 check_select(Some(vec![(
1599 SelectDefaultValue::User(Id::new(1234)),
1600 "1234",
1601 )]));
1602 check_select(Some(vec![
1603 (SelectDefaultValue::User(Id::new(1234)), "1234"),
1604 (SelectDefaultValue::User(Id::new(5432)), "5432"),
1605 ]));
1606 }
1607
1608 #[test]
1609 fn text_input() {
1610 #[allow(deprecated)]
1611 let value = Component::TextInput(TextInput {
1612 custom_id: "test".to_owned(),
1613 label: Some("The label".to_owned()),
1614 max_length: Some(100),
1615 min_length: Some(1),
1616 placeholder: Some("Taking this place".to_owned()),
1617 required: Some(true),
1618 style: TextInputStyle::Short,
1619 value: Some("Hello World!".to_owned()),
1620 id: None,
1621 });
1622
1623 serde_test::assert_tokens(
1624 &value,
1625 &[
1626 Token::Struct {
1627 name: "Component",
1628 len: 9,
1629 },
1630 Token::String("type"),
1631 Token::U8(ComponentType::TextInput.into()),
1632 Token::String("custom_id"),
1633 Token::Some,
1634 Token::String("test"),
1635 Token::String("label"),
1636 Token::Some,
1637 Token::String("The label"),
1638 Token::String("max_length"),
1639 Token::Some,
1640 Token::U16(100),
1641 Token::String("min_length"),
1642 Token::Some,
1643 Token::U16(1),
1644 Token::String("placeholder"),
1645 Token::Some,
1646 Token::String("Taking this place"),
1647 Token::String("required"),
1648 Token::Some,
1649 Token::Bool(true),
1650 Token::String("style"),
1651 Token::U8(TextInputStyle::Short as u8),
1652 Token::String("value"),
1653 Token::Some,
1654 Token::String("Hello World!"),
1655 Token::StructEnd,
1656 ],
1657 );
1658 }
1659
1660 #[test]
1661 fn premium_button() {
1662 let value = Component::Button(Button {
1663 custom_id: None,
1664 disabled: false,
1665 emoji: None,
1666 label: None,
1667 style: ButtonStyle::Premium,
1668 url: None,
1669 sku_id: Some(Id::new(114_941_315_417_899_012)),
1670 id: None,
1671 });
1672
1673 serde_test::assert_tokens(
1674 &value,
1675 &[
1676 Token::Struct {
1677 name: "Component",
1678 len: 3,
1679 },
1680 Token::String("type"),
1681 Token::U8(ComponentType::Button.into()),
1682 Token::String("style"),
1683 Token::U8(ButtonStyle::Premium.into()),
1684 Token::String("sku_id"),
1685 Token::Some,
1686 Token::NewtypeStruct { name: "Id" },
1687 Token::Str("114941315417899012"),
1688 Token::StructEnd,
1689 ],
1690 );
1691 }
1692
1693 #[test]
1694 fn label() {
1695 #[allow(deprecated)]
1696 let value = Component::Label(Label {
1697 id: None,
1698 label: "The label".to_owned(),
1699 description: Some("The description".to_owned()),
1700 component: Box::new(Component::TextInput(TextInput {
1701 id: None,
1702 custom_id: "The custom id".to_owned(),
1703 label: None,
1704 max_length: None,
1705 min_length: None,
1706 placeholder: None,
1707 required: None,
1708 style: TextInputStyle::Paragraph,
1709 value: None,
1710 })),
1711 });
1712
1713 serde_test::assert_tokens(
1714 &value,
1715 &[
1716 Token::Struct {
1717 name: "Component",
1718 len: 4,
1719 },
1720 Token::String("type"),
1721 Token::U8(ComponentType::Label.into()),
1722 Token::String("label"),
1723 Token::Some,
1724 Token::String("The label"),
1725 Token::String("description"),
1726 Token::Some,
1727 Token::String("The description"),
1728 Token::String("component"),
1729 Token::Struct {
1730 name: "Component",
1731 len: 3,
1732 },
1733 Token::String("type"),
1734 Token::U8(ComponentType::TextInput.into()),
1735 Token::String("custom_id"),
1736 Token::Some,
1737 Token::String("The custom id"),
1738 Token::String("style"),
1739 Token::U8(TextInputStyle::Paragraph as u8),
1740 Token::StructEnd,
1741 Token::StructEnd,
1742 ],
1743 );
1744 }
1745
1746 #[test]
1747 fn file_upload() {
1748 let value = Component::FileUpload(FileUpload {
1749 id: None,
1750 custom_id: "test".to_owned(),
1751 max_values: None,
1752 min_values: None,
1753 required: Some(true),
1754 });
1755
1756 serde_test::assert_tokens(
1757 &value,
1758 &[
1759 Token::Struct {
1760 name: "Component",
1761 len: 3,
1762 },
1763 Token::String("type"),
1764 Token::U8(ComponentType::FileUpload.into()),
1765 Token::String("custom_id"),
1766 Token::Some,
1767 Token::String("test"),
1768 Token::String("required"),
1769 Token::Some,
1770 Token::Bool(true),
1771 Token::StructEnd,
1772 ],
1773 )
1774 }
1775}