1mod action_row;
9mod button;
10mod container;
11mod file_display;
12mod kind;
13mod media_gallery;
14mod section;
15mod select_menu;
16mod separator;
17mod text_display;
18mod text_input;
19mod thumbnail;
20mod unfurled_media;
21
22pub use self::{
23 action_row::ActionRow,
24 button::{Button, ButtonStyle},
25 container::Container,
26 file_display::FileDisplay,
27 kind::ComponentType,
28 media_gallery::{MediaGallery, MediaGalleryItem},
29 section::Section,
30 select_menu::{SelectDefaultValue, SelectMenu, SelectMenuOption, SelectMenuType},
31 separator::{Separator, SeparatorSpacingSize},
32 text_display::TextDisplay,
33 text_input::{TextInput, TextInputStyle},
34 thumbnail::Thumbnail,
35 unfurled_media::UnfurledMediaItem,
36};
37
38use super::EmojiReactionType;
39use crate::{
40 channel::ChannelType,
41 id::{marker::SkuMarker, Id},
42};
43use serde::{
44 de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
45 ser::{Error as SerError, SerializeStruct},
46 Deserialize, Serialize, Serializer,
47};
48use serde_value::{DeserializerError, Value};
49use std::fmt::{Formatter, Result as FmtResult};
50
51#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub enum Component {
140 ActionRow(ActionRow),
142 Button(Button),
144 SelectMenu(SelectMenu),
146 TextInput(TextInput),
148 TextDisplay(TextDisplay),
150 MediaGallery(MediaGallery),
152 Separator(Separator),
154 File(FileDisplay),
156 Section(Section),
158 Container(Container),
160 Thumbnail(Thumbnail),
162 Unknown(u8),
164}
165
166impl Component {
167 pub const fn kind(&self) -> ComponentType {
188 match self {
189 Component::ActionRow(_) => ComponentType::ActionRow,
190 Component::Button(_) => ComponentType::Button,
191 Component::SelectMenu(SelectMenu { kind, .. }) => match kind {
192 SelectMenuType::Text => ComponentType::TextSelectMenu,
193 SelectMenuType::User => ComponentType::UserSelectMenu,
194 SelectMenuType::Role => ComponentType::RoleSelectMenu,
195 SelectMenuType::Mentionable => ComponentType::MentionableSelectMenu,
196 SelectMenuType::Channel => ComponentType::ChannelSelectMenu,
197 },
198 Component::TextInput(_) => ComponentType::TextInput,
199 Component::TextDisplay(_) => ComponentType::TextDisplay,
200 Component::MediaGallery(_) => ComponentType::MediaGallery,
201 Component::Separator(_) => ComponentType::Separator,
202 Component::File(_) => ComponentType::File,
203 Component::Unknown(unknown) => ComponentType::Unknown(*unknown),
204 Component::Section(_) => ComponentType::Section,
205 Component::Container(_) => ComponentType::Container,
206 Component::Thumbnail(_) => ComponentType::Thumbnail,
207 }
208 }
209
210 pub fn component_count(&self) -> usize {
212 match self {
213 Component::ActionRow(action_row) => 1 + action_row.components.len(),
214 Component::Section(section) => 1 + section.components.len(),
215 Component::Container(container) => 1 + container.components.len(),
216 Component::Button(_)
217 | Component::SelectMenu(_)
218 | Component::TextInput(_)
219 | Component::TextDisplay(_)
220 | Component::MediaGallery(_)
221 | Component::Separator(_)
222 | Component::File(_)
223 | Component::Thumbnail(_)
224 | Component::Unknown(_) => 1,
225 }
226 }
227}
228
229impl From<ActionRow> for Component {
230 fn from(action_row: ActionRow) -> Self {
231 Self::ActionRow(action_row)
232 }
233}
234
235impl From<Button> for Component {
236 fn from(button: Button) -> Self {
237 Self::Button(button)
238 }
239}
240
241impl From<Container> for Component {
242 fn from(container: Container) -> Self {
243 Self::Container(container)
244 }
245}
246
247impl From<FileDisplay> for Component {
248 fn from(file_display: FileDisplay) -> Self {
249 Self::File(file_display)
250 }
251}
252
253impl From<MediaGallery> for Component {
254 fn from(media_gallery: MediaGallery) -> Self {
255 Self::MediaGallery(media_gallery)
256 }
257}
258
259impl From<Section> for Component {
260 fn from(section: Section) -> Self {
261 Self::Section(section)
262 }
263}
264
265impl From<SelectMenu> for Component {
266 fn from(select_menu: SelectMenu) -> Self {
267 Self::SelectMenu(select_menu)
268 }
269}
270
271impl From<Separator> for Component {
272 fn from(separator: Separator) -> Self {
273 Self::Separator(separator)
274 }
275}
276
277impl From<TextDisplay> for Component {
278 fn from(text_display: TextDisplay) -> Self {
279 Self::TextDisplay(text_display)
280 }
281}
282
283impl From<TextInput> for Component {
284 fn from(text_input: TextInput) -> Self {
285 Self::TextInput(text_input)
286 }
287}
288
289impl From<Thumbnail> for Component {
290 fn from(thumbnail: Thumbnail) -> Self {
291 Self::Thumbnail(thumbnail)
292 }
293}
294
295impl<'de> Deserialize<'de> for Component {
296 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
297 deserializer.deserialize_any(ComponentVisitor)
298 }
299}
300
301#[derive(Debug, Deserialize)]
302#[serde(field_identifier, rename_all = "snake_case")]
303enum Field {
304 ChannelTypes,
305 Components,
306 CustomId,
307 DefaultValues,
308 Disabled,
309 Emoji,
310 Label,
311 MaxLength,
312 MaxValues,
313 MinLength,
314 MinValues,
315 Options,
316 Placeholder,
317 Required,
318 Style,
319 Type,
320 Url,
321 SkuId,
322 Value,
323 Id,
324 Content,
325 Items,
326 Divider,
327 Spacing,
328 File,
329 Spoiler,
330 Accessory,
331 Media,
332 Description,
333 AccentColor,
334}
335
336struct ComponentVisitor;
337
338impl<'de> Visitor<'de> for ComponentVisitor {
339 type Value = Component;
340
341 fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
342 f.write_str("struct Component")
343 }
344
345 #[allow(clippy::too_many_lines)]
346 fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
347 let mut components: Option<Vec<Component>> = None;
349 let mut kind: Option<ComponentType> = None;
350 let mut options: Option<Vec<SelectMenuOption>> = None;
351 let mut style: Option<Value> = None;
352
353 let mut custom_id: Option<Option<Value>> = None;
355 let mut label: Option<Option<String>> = None;
356
357 let mut channel_types: Option<Vec<ChannelType>> = None;
359 let mut default_values: Option<Vec<SelectDefaultValue>> = None;
360 let mut disabled: Option<bool> = None;
361 let mut emoji: Option<Option<EmojiReactionType>> = None;
362 let mut max_length: Option<Option<u16>> = None;
363 let mut max_values: Option<Option<u8>> = None;
364 let mut min_length: Option<Option<u16>> = None;
365 let mut min_values: Option<Option<u8>> = None;
366 let mut placeholder: Option<Option<String>> = None;
367 let mut required: Option<Option<bool>> = None;
368 let mut url: Option<Option<String>> = None;
369 let mut sku_id: Option<Id<SkuMarker>> = None;
370 let mut value: Option<Option<String>> = None;
371
372 let mut id: Option<i32> = None;
373 let mut content: Option<String> = None;
374 let mut items: Option<Vec<MediaGalleryItem>> = None;
375 let mut divider: Option<bool> = None;
376 let mut spacing: Option<SeparatorSpacingSize> = None;
377 let mut file: Option<UnfurledMediaItem> = None;
378 let mut spoiler: Option<bool> = None;
379 let mut accessory: Option<Component> = None;
380 let mut media: Option<UnfurledMediaItem> = None;
381 let mut description: Option<Option<String>> = None;
382 let mut accent_color: Option<Option<u32>> = None;
383
384 loop {
385 let key = match map.next_key() {
386 Ok(Some(key)) => key,
387 Ok(None) => break,
388 Err(_) => {
389 map.next_value::<IgnoredAny>()?;
390
391 continue;
392 }
393 };
394
395 match key {
396 Field::ChannelTypes => {
397 if channel_types.is_some() {
398 return Err(DeError::duplicate_field("channel_types"));
399 }
400
401 channel_types = Some(map.next_value()?);
402 }
403 Field::Components => {
404 if components.is_some() {
405 return Err(DeError::duplicate_field("components"));
406 }
407
408 components = Some(map.next_value()?);
409 }
410 Field::CustomId => {
411 if custom_id.is_some() {
412 return Err(DeError::duplicate_field("custom_id"));
413 }
414
415 custom_id = Some(map.next_value()?);
416 }
417 Field::DefaultValues => {
418 if default_values.is_some() {
419 return Err(DeError::duplicate_field("default_values"));
420 }
421
422 default_values = map.next_value()?;
423 }
424 Field::Disabled => {
425 if disabled.is_some() {
426 return Err(DeError::duplicate_field("disabled"));
427 }
428
429 disabled = Some(map.next_value()?);
430 }
431 Field::Emoji => {
432 if emoji.is_some() {
433 return Err(DeError::duplicate_field("emoji"));
434 }
435
436 emoji = Some(map.next_value()?);
437 }
438 Field::Label => {
439 if label.is_some() {
440 return Err(DeError::duplicate_field("label"));
441 }
442
443 label = Some(map.next_value()?);
444 }
445 Field::MaxLength => {
446 if max_length.is_some() {
447 return Err(DeError::duplicate_field("max_length"));
448 }
449
450 max_length = Some(map.next_value()?);
451 }
452 Field::MaxValues => {
453 if max_values.is_some() {
454 return Err(DeError::duplicate_field("max_values"));
455 }
456
457 max_values = Some(map.next_value()?);
458 }
459 Field::MinLength => {
460 if min_length.is_some() {
461 return Err(DeError::duplicate_field("min_length"));
462 }
463
464 min_length = Some(map.next_value()?);
465 }
466 Field::MinValues => {
467 if min_values.is_some() {
468 return Err(DeError::duplicate_field("min_values"));
469 }
470
471 min_values = Some(map.next_value()?);
472 }
473 Field::Options => {
474 if options.is_some() {
475 return Err(DeError::duplicate_field("options"));
476 }
477
478 options = Some(map.next_value()?);
479 }
480 Field::Placeholder => {
481 if placeholder.is_some() {
482 return Err(DeError::duplicate_field("placeholder"));
483 }
484
485 placeholder = Some(map.next_value()?);
486 }
487 Field::Required => {
488 if required.is_some() {
489 return Err(DeError::duplicate_field("required"));
490 }
491
492 required = Some(map.next_value()?);
493 }
494 Field::Style => {
495 if style.is_some() {
496 return Err(DeError::duplicate_field("style"));
497 }
498
499 style = Some(map.next_value()?);
500 }
501 Field::Type => {
502 if kind.is_some() {
503 return Err(DeError::duplicate_field("type"));
504 }
505
506 kind = Some(map.next_value()?);
507 }
508 Field::Url => {
509 if url.is_some() {
510 return Err(DeError::duplicate_field("url"));
511 }
512
513 url = Some(map.next_value()?);
514 }
515 Field::SkuId => {
516 if sku_id.is_some() {
517 return Err(DeError::duplicate_field("sku_id"));
518 }
519
520 sku_id = map.next_value()?;
521 }
522 Field::Value => {
523 if value.is_some() {
524 return Err(DeError::duplicate_field("value"));
525 }
526
527 value = Some(map.next_value()?);
528 }
529 Field::Id => {
530 if id.is_some() {
531 return Err(DeError::duplicate_field("id"));
532 }
533
534 id = Some(map.next_value()?);
535 }
536 Field::Content => {
537 if content.is_some() {
538 return Err(DeError::duplicate_field("content"));
539 }
540
541 content = Some(map.next_value()?);
542 }
543 Field::Items => {
544 if items.is_some() {
545 return Err(DeError::duplicate_field("items"));
546 }
547
548 items = Some(map.next_value()?);
549 }
550 Field::Divider => {
551 if divider.is_some() {
552 return Err(DeError::duplicate_field("divider"));
553 }
554
555 divider = Some(map.next_value()?);
556 }
557 Field::Spacing => {
558 if spacing.is_some() {
559 return Err(DeError::duplicate_field("spacing"));
560 }
561
562 spacing = Some(map.next_value()?);
563 }
564 Field::File => {
565 if file.is_some() {
566 return Err(DeError::duplicate_field("file"));
567 }
568
569 file = Some(map.next_value()?);
570 }
571 Field::Spoiler => {
572 if spoiler.is_some() {
573 return Err(DeError::duplicate_field("spoiler"));
574 }
575
576 spoiler = Some(map.next_value()?);
577 }
578 Field::Accessory => {
579 if accessory.is_some() {
580 return Err(DeError::duplicate_field("accessory"));
581 }
582
583 accessory = Some(map.next_value()?);
584 }
585 Field::Media => {
586 if media.is_some() {
587 return Err(DeError::duplicate_field("media"));
588 }
589
590 media = Some(map.next_value()?);
591 }
592 Field::Description => {
593 if description.is_some() {
594 return Err(DeError::duplicate_field("description"));
595 }
596
597 description = Some(map.next_value()?);
598 }
599 Field::AccentColor => {
600 if accent_color.is_some() {
601 return Err(DeError::duplicate_field("accent_color"));
602 }
603
604 accent_color = Some(map.next_value()?);
605 }
606 }
607 }
608
609 let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
610
611 Ok(match kind {
612 ComponentType::ActionRow => {
615 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
616
617 Self::Value::ActionRow(ActionRow { id, components })
618 }
619 ComponentType::Button => {
630 let style = style
631 .ok_or_else(|| DeError::missing_field("style"))?
632 .deserialize_into()
633 .map_err(DeserializerError::into_error)?;
634
635 let custom_id = custom_id
636 .flatten()
637 .map(Value::deserialize_into)
638 .transpose()
639 .map_err(DeserializerError::into_error)?;
640
641 Self::Value::Button(Button {
642 custom_id,
643 disabled: disabled.unwrap_or_default(),
644 emoji: emoji.unwrap_or_default(),
645 label: label.flatten(),
646 style,
647 url: url.unwrap_or_default(),
648 sku_id,
649 id,
650 })
651 }
652 kind @ (ComponentType::TextSelectMenu
664 | ComponentType::UserSelectMenu
665 | ComponentType::RoleSelectMenu
666 | ComponentType::MentionableSelectMenu
667 | ComponentType::ChannelSelectMenu) => {
668 if let ComponentType::TextSelectMenu = kind {
670 if options.is_none() {
671 return Err(DeError::missing_field("options"));
672 }
673 }
674
675 let custom_id = custom_id
676 .flatten()
677 .ok_or_else(|| DeError::missing_field("custom_id"))?
678 .deserialize_into()
679 .map_err(DeserializerError::into_error)?;
680
681 Self::Value::SelectMenu(SelectMenu {
682 channel_types,
683 custom_id,
684 default_values,
685 disabled: disabled.unwrap_or_default(),
686 kind: match kind {
687 ComponentType::TextSelectMenu => SelectMenuType::Text,
688 ComponentType::UserSelectMenu => SelectMenuType::User,
689 ComponentType::RoleSelectMenu => SelectMenuType::Role,
690 ComponentType::MentionableSelectMenu => SelectMenuType::Mentionable,
691 ComponentType::ChannelSelectMenu => SelectMenuType::Channel,
692 _ => {
695 unreachable!("select menu component type is only partially implemented")
696 }
697 },
698 max_values: max_values.unwrap_or_default(),
699 min_values: min_values.unwrap_or_default(),
700 options,
701 placeholder: placeholder.unwrap_or_default(),
702 id,
703 })
704 }
705 ComponentType::TextInput => {
717 let custom_id = custom_id
718 .flatten()
719 .ok_or_else(|| DeError::missing_field("custom_id"))?
720 .deserialize_into()
721 .map_err(DeserializerError::into_error)?;
722
723 let label = label
724 .flatten()
725 .ok_or_else(|| DeError::missing_field("label"))?;
726
727 let style = style
728 .ok_or_else(|| DeError::missing_field("style"))?
729 .deserialize_into()
730 .map_err(DeserializerError::into_error)?;
731
732 Self::Value::TextInput(TextInput {
733 custom_id,
734 label,
735 max_length: max_length.unwrap_or_default(),
736 min_length: min_length.unwrap_or_default(),
737 placeholder: placeholder.unwrap_or_default(),
738 required: required.unwrap_or_default(),
739 style,
740 value: value.unwrap_or_default(),
741 id,
742 })
743 }
744 ComponentType::TextDisplay => {
745 let content = content.ok_or_else(|| DeError::missing_field("content"))?;
746
747 Self::Value::TextDisplay(TextDisplay { id, content })
748 }
749 ComponentType::MediaGallery => {
750 let items = items.ok_or_else(|| DeError::missing_field("items"))?;
751
752 Self::Value::MediaGallery(MediaGallery { id, items })
753 }
754 ComponentType::Separator => Self::Value::Separator(Separator {
755 id,
756 divider,
757 spacing,
758 }),
759 ComponentType::File => {
760 let file = file.ok_or_else(|| DeError::missing_field("file"))?;
761
762 Self::Value::File(FileDisplay { id, file, spoiler })
763 }
764 ComponentType::Unknown(unknown) => Self::Value::Unknown(unknown),
765 ComponentType::Section => {
766 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
767 let accessory = accessory.ok_or_else(|| DeError::missing_field("accessory"))?;
768 Self::Value::Section(Section {
769 id,
770 components,
771 accessory: Box::new(accessory),
772 })
773 }
774 ComponentType::Thumbnail => {
775 let media = media.ok_or_else(|| DeError::missing_field("media"))?;
776 Self::Value::Thumbnail(Thumbnail {
777 id,
778 media,
779 description,
780 spoiler,
781 })
782 }
783 ComponentType::Container => {
784 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
785 Self::Value::Container(Container {
786 id,
787 accent_color,
788 spoiler,
789 components,
790 })
791 }
792 })
793 }
794}
795
796impl Serialize for Component {
797 #[allow(clippy::too_many_lines)]
798 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
799 let len = match self {
800 Component::ActionRow(row) => 2 + usize::from(row.id.is_some()),
807 Component::Button(button) => {
820 2 + usize::from(button.custom_id.is_some())
821 + usize::from(button.disabled)
822 + usize::from(button.emoji.is_some())
823 + usize::from(button.label.is_some())
824 + usize::from(button.url.is_some())
825 + usize::from(button.sku_id.is_some())
826 + usize::from(button.id.is_some())
827 }
828 Component::SelectMenu(select_menu) => {
842 2 + usize::from(select_menu.channel_types.is_some())
845 + usize::from(select_menu.default_values.is_some())
846 + usize::from(select_menu.disabled)
847 + usize::from(select_menu.max_values.is_some())
848 + usize::from(select_menu.min_values.is_some())
849 + usize::from(select_menu.options.is_some())
850 + usize::from(select_menu.placeholder.is_some())
851 + usize::from(select_menu.id.is_some())
852 }
853 Component::TextInput(text_input) => {
867 4 + usize::from(text_input.max_length.is_some())
868 + usize::from(text_input.min_length.is_some())
869 + usize::from(text_input.placeholder.is_some())
870 + usize::from(text_input.required.is_some())
871 + usize::from(text_input.value.is_some())
872 + usize::from(text_input.id.is_some())
873 }
874 Component::TextDisplay(text_display) => 2 + usize::from(text_display.id.is_some()),
880 Component::MediaGallery(media_gallery) => 2 + usize::from(media_gallery.id.is_some()),
886 Component::Separator(separator) => {
893 1 + usize::from(separator.divider.is_some())
894 + usize::from(separator.spacing.is_some())
895 + usize::from(separator.id.is_some())
896 }
897 Component::File(file) => {
904 2 + usize::from(file.spoiler.is_some()) + usize::from(file.id.is_some())
905 }
906 Component::Section(section) => 3 + usize::from(section.id.is_some()),
913 Component::Container(container) => {
921 2 + usize::from(container.accent_color.is_some())
922 + usize::from(container.spoiler.is_some())
923 + usize::from(container.id.is_some())
924 }
925 Component::Thumbnail(thumbnail) => {
933 2 + usize::from(thumbnail.spoiler.is_some())
934 + usize::from(thumbnail.id.is_some())
935 + usize::from(thumbnail.description.is_some())
936 }
937 Component::Unknown(_) => 1,
940 };
941
942 let mut state = serializer.serialize_struct("Component", len)?;
943
944 match self {
945 Component::ActionRow(action_row) => {
946 state.serialize_field("type", &ComponentType::ActionRow)?;
947 if let Some(id) = action_row.id {
948 state.serialize_field("id", &id)?;
949 }
950
951 state.serialize_field("components", &action_row.components)?;
952 }
953 Component::Button(button) => {
954 state.serialize_field("type", &ComponentType::Button)?;
955 if let Some(id) = button.id {
956 state.serialize_field("id", &id)?;
957 }
958
959 if button.custom_id.is_some() {
960 state.serialize_field("custom_id", &button.custom_id)?;
961 }
962
963 if button.disabled {
964 state.serialize_field("disabled", &button.disabled)?;
965 }
966
967 if button.emoji.is_some() {
968 state.serialize_field("emoji", &button.emoji)?;
969 }
970
971 if button.label.is_some() {
972 state.serialize_field("label", &button.label)?;
973 }
974
975 state.serialize_field("style", &button.style)?;
976
977 if button.url.is_some() {
978 state.serialize_field("url", &button.url)?;
979 }
980
981 if button.sku_id.is_some() {
982 state.serialize_field("sku_id", &button.sku_id)?;
983 }
984 }
985 Component::SelectMenu(select_menu) => {
986 match &select_menu.kind {
987 SelectMenuType::Text => {
988 state.serialize_field("type", &ComponentType::TextSelectMenu)?;
989 if let Some(id) = select_menu.id {
990 state.serialize_field("id", &id)?;
991 }
992
993 state.serialize_field(
994 "options",
995 &select_menu.options.as_ref().ok_or(SerError::custom(
996 "required field \"option\" missing for text select menu",
997 ))?,
998 )?;
999 }
1000 SelectMenuType::User => {
1001 state.serialize_field("type", &ComponentType::UserSelectMenu)?;
1002 if let Some(id) = select_menu.id {
1003 state.serialize_field("id", &id)?;
1004 }
1005 }
1006 SelectMenuType::Role => {
1007 state.serialize_field("type", &ComponentType::RoleSelectMenu)?;
1008 if let Some(id) = select_menu.id {
1009 state.serialize_field("id", &id)?;
1010 }
1011 }
1012 SelectMenuType::Mentionable => {
1013 state.serialize_field("type", &ComponentType::MentionableSelectMenu)?;
1014 if let Some(id) = select_menu.id {
1015 state.serialize_field("id", &id)?;
1016 }
1017 }
1018 SelectMenuType::Channel => {
1019 state.serialize_field("type", &ComponentType::ChannelSelectMenu)?;
1020 if let Some(id) = select_menu.id {
1021 state.serialize_field("id", &id)?;
1022 }
1023
1024 if let Some(channel_types) = &select_menu.channel_types {
1025 state.serialize_field("channel_types", channel_types)?;
1026 }
1027 }
1028 }
1029
1030 state.serialize_field("custom_id", &Some(&select_menu.custom_id))?;
1033
1034 if select_menu.default_values.is_some() {
1035 state.serialize_field("default_values", &select_menu.default_values)?;
1036 }
1037
1038 state.serialize_field("disabled", &select_menu.disabled)?;
1039
1040 if select_menu.max_values.is_some() {
1041 state.serialize_field("max_values", &select_menu.max_values)?;
1042 }
1043
1044 if select_menu.min_values.is_some() {
1045 state.serialize_field("min_values", &select_menu.min_values)?;
1046 }
1047
1048 if select_menu.placeholder.is_some() {
1049 state.serialize_field("placeholder", &select_menu.placeholder)?;
1050 }
1051 }
1052 Component::TextInput(text_input) => {
1053 state.serialize_field("type", &ComponentType::TextInput)?;
1054 if let Some(id) = text_input.id {
1055 state.serialize_field("id", &id)?;
1056 }
1057
1058 state.serialize_field("custom_id", &Some(&text_input.custom_id))?;
1061 state.serialize_field("label", &Some(&text_input.label))?;
1062
1063 if text_input.max_length.is_some() {
1064 state.serialize_field("max_length", &text_input.max_length)?;
1065 }
1066
1067 if text_input.min_length.is_some() {
1068 state.serialize_field("min_length", &text_input.min_length)?;
1069 }
1070
1071 if text_input.placeholder.is_some() {
1072 state.serialize_field("placeholder", &text_input.placeholder)?;
1073 }
1074
1075 if text_input.required.is_some() {
1076 state.serialize_field("required", &text_input.required)?;
1077 }
1078
1079 state.serialize_field("style", &text_input.style)?;
1080
1081 if text_input.value.is_some() {
1082 state.serialize_field("value", &text_input.value)?;
1083 }
1084 }
1085 Component::TextDisplay(text_display) => {
1086 state.serialize_field("type", &ComponentType::TextDisplay)?;
1087 if let Some(id) = text_display.id {
1088 state.serialize_field("id", &id)?;
1089 }
1090
1091 state.serialize_field("content", &text_display.content)?;
1092 }
1093 Component::MediaGallery(media_gallery) => {
1094 state.serialize_field("type", &ComponentType::MediaGallery)?;
1095 if let Some(id) = media_gallery.id {
1096 state.serialize_field("id", &id)?;
1097 }
1098
1099 state.serialize_field("items", &media_gallery.items)?;
1100 }
1101 Component::Separator(separator) => {
1102 state.serialize_field("type", &ComponentType::Separator)?;
1103 if let Some(id) = separator.id {
1104 state.serialize_field("id", &id)?;
1105 }
1106 if let Some(divider) = separator.divider {
1107 state.serialize_field("divider", ÷r)?;
1108 }
1109 if let Some(spacing) = &separator.spacing {
1110 state.serialize_field("spacing", spacing)?;
1111 }
1112 }
1113 Component::File(file) => {
1114 state.serialize_field("type", &ComponentType::File)?;
1115 if let Some(id) = file.id {
1116 state.serialize_field("id", &id)?;
1117 }
1118
1119 state.serialize_field("file", &file.file)?;
1120 if let Some(spoiler) = file.spoiler {
1121 state.serialize_field("spoiler", &spoiler)?;
1122 }
1123 }
1124 Component::Section(section) => {
1125 state.serialize_field("type", &ComponentType::Section)?;
1126 if let Some(id) = section.id {
1127 state.serialize_field("id", &id)?;
1128 }
1129
1130 state.serialize_field("components", §ion.components)?;
1131 state.serialize_field("accessory", §ion.accessory)?;
1132 }
1133 Component::Container(container) => {
1134 state.serialize_field("type", &ComponentType::Container)?;
1135 if let Some(id) = container.id {
1136 state.serialize_field("id", &id)?;
1137 }
1138
1139 if let Some(accent_color) = container.accent_color {
1140 state.serialize_field("accent_color", &accent_color)?;
1141 }
1142 if let Some(spoiler) = container.spoiler {
1143 state.serialize_field("spoiler", &spoiler)?;
1144 }
1145 state.serialize_field("components", &container.components)?;
1146 }
1147 Component::Thumbnail(thumbnail) => {
1148 state.serialize_field("type", &ComponentType::Thumbnail)?;
1149 if let Some(id) = thumbnail.id {
1150 state.serialize_field("id", &id)?;
1151 }
1152
1153 state.serialize_field("media", &thumbnail.media)?;
1154 if let Some(description) = &thumbnail.description {
1155 state.serialize_field("description", description)?;
1156 }
1157 if let Some(spoiler) = thumbnail.spoiler {
1158 state.serialize_field("spoiler", &spoiler)?;
1159 }
1160 }
1161 Component::Unknown(unknown) => {
1165 state.serialize_field("type", &ComponentType::Unknown(*unknown))?;
1166 }
1167 }
1168
1169 state.end()
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 #![allow(clippy::non_ascii_literal)]
1177
1178 use super::*;
1179 use crate::id::Id;
1180 use serde_test::Token;
1181 use static_assertions::assert_impl_all;
1182
1183 assert_impl_all!(
1184 Component: From<ActionRow>,
1185 From<Button>,
1186 From<SelectMenu>,
1187 From<TextInput>
1188 );
1189
1190 #[allow(clippy::too_many_lines)]
1191 #[test]
1192 fn component_full() {
1193 let component = Component::ActionRow(ActionRow {
1194 components: Vec::from([
1195 Component::Button(Button {
1196 custom_id: Some("test custom id".into()),
1197 disabled: true,
1198 emoji: None,
1199 label: Some("test label".into()),
1200 style: ButtonStyle::Primary,
1201 url: None,
1202 sku_id: None,
1203 id: None,
1204 }),
1205 Component::SelectMenu(SelectMenu {
1206 channel_types: None,
1207 custom_id: "test custom id 2".into(),
1208 default_values: None,
1209 disabled: false,
1210 kind: SelectMenuType::Text,
1211 max_values: Some(25),
1212 min_values: Some(5),
1213 options: Some(Vec::from([SelectMenuOption {
1214 label: "test option label".into(),
1215 value: "test option value".into(),
1216 description: Some("test description".into()),
1217 emoji: None,
1218 default: false,
1219 }])),
1220 placeholder: Some("test placeholder".into()),
1221 id: None,
1222 }),
1223 ]),
1224 id: None,
1225 });
1226
1227 serde_test::assert_tokens(
1228 &component,
1229 &[
1230 Token::Struct {
1231 name: "Component",
1232 len: 2,
1233 },
1234 Token::Str("type"),
1235 Token::U8(ComponentType::ActionRow.into()),
1236 Token::Str("components"),
1237 Token::Seq { len: Some(2) },
1238 Token::Struct {
1239 name: "Component",
1240 len: 5,
1241 },
1242 Token::Str("type"),
1243 Token::U8(ComponentType::Button.into()),
1244 Token::Str("custom_id"),
1245 Token::Some,
1246 Token::Str("test custom id"),
1247 Token::Str("disabled"),
1248 Token::Bool(true),
1249 Token::Str("label"),
1250 Token::Some,
1251 Token::Str("test label"),
1252 Token::Str("style"),
1253 Token::U8(ButtonStyle::Primary.into()),
1254 Token::StructEnd,
1255 Token::Struct {
1256 name: "Component",
1257 len: 6,
1258 },
1259 Token::Str("type"),
1260 Token::U8(ComponentType::TextSelectMenu.into()),
1261 Token::Str("options"),
1262 Token::Seq { len: Some(1) },
1263 Token::Struct {
1264 name: "SelectMenuOption",
1265 len: 4,
1266 },
1267 Token::Str("default"),
1268 Token::Bool(false),
1269 Token::Str("description"),
1270 Token::Some,
1271 Token::Str("test description"),
1272 Token::Str("label"),
1273 Token::Str("test option label"),
1274 Token::Str("value"),
1275 Token::Str("test option value"),
1276 Token::StructEnd,
1277 Token::SeqEnd,
1278 Token::Str("custom_id"),
1279 Token::Some,
1280 Token::Str("test custom id 2"),
1281 Token::Str("disabled"),
1282 Token::Bool(false),
1283 Token::Str("max_values"),
1284 Token::Some,
1285 Token::U8(25),
1286 Token::Str("min_values"),
1287 Token::Some,
1288 Token::U8(5),
1289 Token::Str("placeholder"),
1290 Token::Some,
1291 Token::Str("test placeholder"),
1292 Token::StructEnd,
1293 Token::SeqEnd,
1294 Token::StructEnd,
1295 ],
1296 );
1297 }
1298
1299 #[test]
1300 fn action_row() {
1301 let value = Component::ActionRow(ActionRow {
1302 components: Vec::from([Component::Button(Button {
1303 custom_id: Some("button-1".to_owned()),
1304 disabled: false,
1305 emoji: None,
1306 style: ButtonStyle::Primary,
1307 label: Some("Button".to_owned()),
1308 url: None,
1309 sku_id: None,
1310 id: None,
1311 })]),
1312 id: None,
1313 });
1314
1315 serde_test::assert_tokens(
1316 &value,
1317 &[
1318 Token::Struct {
1319 name: "Component",
1320 len: 2,
1321 },
1322 Token::String("type"),
1323 Token::U8(ComponentType::ActionRow.into()),
1324 Token::String("components"),
1325 Token::Seq { len: Some(1) },
1326 Token::Struct {
1327 name: "Component",
1328 len: 4,
1329 },
1330 Token::String("type"),
1331 Token::U8(2),
1332 Token::String("custom_id"),
1333 Token::Some,
1334 Token::String("button-1"),
1335 Token::String("label"),
1336 Token::Some,
1337 Token::String("Button"),
1338 Token::String("style"),
1339 Token::U8(1),
1340 Token::StructEnd,
1341 Token::SeqEnd,
1342 Token::StructEnd,
1343 ],
1344 );
1345 }
1346
1347 #[test]
1348 fn button() {
1349 const FLAG: &str = "🇵🇸";
1353
1354 let value = Component::Button(Button {
1355 custom_id: Some("test".to_owned()),
1356 disabled: false,
1357 emoji: Some(EmojiReactionType::Unicode {
1358 name: FLAG.to_owned(),
1359 }),
1360 label: Some("Test".to_owned()),
1361 style: ButtonStyle::Link,
1362 url: Some("https://twilight.rs".to_owned()),
1363 sku_id: None,
1364 id: None,
1365 });
1366
1367 serde_test::assert_tokens(
1368 &value,
1369 &[
1370 Token::Struct {
1371 name: "Component",
1372 len: 6,
1373 },
1374 Token::String("type"),
1375 Token::U8(ComponentType::Button.into()),
1376 Token::String("custom_id"),
1377 Token::Some,
1378 Token::String("test"),
1379 Token::String("emoji"),
1380 Token::Some,
1381 Token::Struct {
1382 name: "EmojiReactionType",
1383 len: 1,
1384 },
1385 Token::String("name"),
1386 Token::String(FLAG),
1387 Token::StructEnd,
1388 Token::String("label"),
1389 Token::Some,
1390 Token::String("Test"),
1391 Token::String("style"),
1392 Token::U8(ButtonStyle::Link.into()),
1393 Token::String("url"),
1394 Token::Some,
1395 Token::String("https://twilight.rs"),
1396 Token::StructEnd,
1397 ],
1398 );
1399 }
1400
1401 #[test]
1402 fn select_menu() {
1403 fn check_select(default_values: Option<Vec<(SelectDefaultValue, &'static str)>>) {
1404 let select_menu = Component::SelectMenu(SelectMenu {
1405 channel_types: None,
1406 custom_id: String::from("my_select"),
1407 default_values: default_values
1408 .clone()
1409 .map(|values| values.into_iter().map(|pair| pair.0).collect()),
1410 disabled: false,
1411 kind: SelectMenuType::User,
1412 max_values: None,
1413 min_values: None,
1414 options: None,
1415 placeholder: None,
1416 id: None,
1417 });
1418 let mut tokens = vec![
1419 Token::Struct {
1420 name: "Component",
1421 len: 2 + usize::from(default_values.is_some()),
1422 },
1423 Token::String("type"),
1424 Token::U8(ComponentType::UserSelectMenu.into()),
1425 Token::Str("custom_id"),
1426 Token::Some,
1427 Token::Str("my_select"),
1428 ];
1429 if let Some(default_values) = default_values {
1430 tokens.extend_from_slice(&[
1431 Token::Str("default_values"),
1432 Token::Some,
1433 Token::Seq {
1434 len: Some(default_values.len()),
1435 },
1436 ]);
1437 for (_, id) in default_values {
1438 tokens.extend_from_slice(&[
1439 Token::Struct {
1440 name: "SelectDefaultValue",
1441 len: 2,
1442 },
1443 Token::Str("type"),
1444 Token::UnitVariant {
1445 name: "SelectDefaultValue",
1446 variant: "user",
1447 },
1448 Token::Str("id"),
1449 Token::NewtypeStruct { name: "Id" },
1450 Token::Str(id),
1451 Token::StructEnd,
1452 ])
1453 }
1454 tokens.push(Token::SeqEnd);
1455 }
1456 tokens.extend_from_slice(&[
1457 Token::Str("disabled"),
1458 Token::Bool(false),
1459 Token::StructEnd,
1460 ]);
1461 serde_test::assert_tokens(&select_menu, &tokens);
1462 }
1463
1464 check_select(None);
1465 check_select(Some(vec![(
1466 SelectDefaultValue::User(Id::new(1234)),
1467 "1234",
1468 )]));
1469 check_select(Some(vec![
1470 (SelectDefaultValue::User(Id::new(1234)), "1234"),
1471 (SelectDefaultValue::User(Id::new(5432)), "5432"),
1472 ]));
1473 }
1474
1475 #[test]
1476 fn text_input() {
1477 let value = Component::TextInput(TextInput {
1478 custom_id: "test".to_owned(),
1479 label: "The label".to_owned(),
1480 max_length: Some(100),
1481 min_length: Some(1),
1482 placeholder: Some("Taking this place".to_owned()),
1483 required: Some(true),
1484 style: TextInputStyle::Short,
1485 value: Some("Hello World!".to_owned()),
1486 id: None,
1487 });
1488
1489 serde_test::assert_tokens(
1490 &value,
1491 &[
1492 Token::Struct {
1493 name: "Component",
1494 len: 9,
1495 },
1496 Token::String("type"),
1497 Token::U8(ComponentType::TextInput.into()),
1498 Token::String("custom_id"),
1499 Token::Some,
1500 Token::String("test"),
1501 Token::String("label"),
1502 Token::Some,
1503 Token::String("The label"),
1504 Token::String("max_length"),
1505 Token::Some,
1506 Token::U16(100),
1507 Token::String("min_length"),
1508 Token::Some,
1509 Token::U16(1),
1510 Token::String("placeholder"),
1511 Token::Some,
1512 Token::String("Taking this place"),
1513 Token::String("required"),
1514 Token::Some,
1515 Token::Bool(true),
1516 Token::String("style"),
1517 Token::U8(TextInputStyle::Short as u8),
1518 Token::String("value"),
1519 Token::Some,
1520 Token::String("Hello World!"),
1521 Token::StructEnd,
1522 ],
1523 );
1524 }
1525
1526 #[test]
1527 fn premium_button() {
1528 let value = Component::Button(Button {
1529 custom_id: None,
1530 disabled: false,
1531 emoji: None,
1532 label: None,
1533 style: ButtonStyle::Premium,
1534 url: None,
1535 sku_id: Some(Id::new(114_941_315_417_899_012)),
1536 id: None,
1537 });
1538
1539 serde_test::assert_tokens(
1540 &value,
1541 &[
1542 Token::Struct {
1543 name: "Component",
1544 len: 3,
1545 },
1546 Token::String("type"),
1547 Token::U8(ComponentType::Button.into()),
1548 Token::String("style"),
1549 Token::U8(ButtonStyle::Premium.into()),
1550 Token::String("sku_id"),
1551 Token::Some,
1552 Token::NewtypeStruct { name: "Id" },
1553 Token::Str("114941315417899012"),
1554 Token::StructEnd,
1555 ],
1556 );
1557 }
1558}