Skip to content

Commit

Permalink
feat(twilight-model)!: Implement additional select menu types (#2219)
Browse files Browse the repository at this point in the history
* feat(twilight-model)!: Implement additional select menu types

This patch implements the additional select menu types `user`, `role`,
`mentionable`, and `channel`. Moreover, it moves the type-specific data
for the existing text select menu implementation into a separate enum
variant.

The new types are implemented by "semi-flattening" the `SelectMenu`
struct: fields common to all select menu types are implemented in the
struct itself, and type-specific fields like the available options or
channel types are moved into a `SelectMenuData` enum. This enum is also
used to select a select menu's type.

This approach de-duplicates the common fields while preventing users
from accessing fields irrelevant to the current select menu type.

Finally, this commit updates the documentation and existing tests to
reflect these changes. The tests, however, might require additional
attention in case new behaviour introduced by this commit should be
tested as well.

* refactor(twilight-model): Use unreachable! in ComponentVisitor

This patch makes `ComponentVisitor::visit_map` use `unreachable!`
instead of `panic!` in case a select menu type was not implemented while
being listed in a previous match statement.

Moreover, this commit sneaks in a minor formatting fix inside
`Components` doc-comment example.

* feat(twilight-model)!: Implement `resolved` for select menu interactions

This commit implements the `resolved` field for
`MessageComponentInteraction`. This field holds resolved users, roles,
channels, or attachments for select menu interactions.

Unfortunately, the new field makes it impossible to implement `Eq` and
`Hash` for `MessageComponentInteractionData`.

* test(twilight-model): Fix tests for `InteractionDataResolved`

This commit fixes the tests testing the (de-)serialization of
`InteractionDataResolved`. Previously, the tests were failing, as the
type was renamed from `CommandInteractionDataResolved`.

Moreover, this commit fixes the message component interaction tests by
adding the missing `resolved` field to the expected fields in
`message_component_interaction_data`.

* refactor(twilight-model)!: Un-`Box` `SelectMenuData` variants

This commit unboxes `SelectMenuData`'s variants. They were boxed in a
previous patch to prevent future fields from unnecessarily increasing
the enum's memory footprint, but considering that, with the current
structures, new fields are a breaking change already, it's OK to leave
the variants unboxed for now and re-box as necessary.

For the current code, this saves a heap allocation while only marginally
increasing the enum's memory footprint.

This change is only breaking in the context of PR #2219. When added to
the commits in this PR, it doesn't add any breaking changes to the PR's
list of breaking changes.

* refactor(twilight-model)!: Fully flatten SelectMenu

This commit removes the SelectMenuData type and embeds all
variant-specific fields in the base struct. Moreover, it adds
documentation indicating which fields are required variant-specific
fields.

This commit also updates the component validation module in
`twilight-validate` by introducing a new error type for text select
menus that don't have the necessary `options` field.

* docs(twilight-model)!: Streamline select menu documentation

This commit streamlines the language in the `SelectMenu` and
`SelectMenuType` documentation. In particular, it removes duplicated
sections outlining required and optional fields. Moreover, it makes
language describing which menu types use a specific option more in
line with the rest of the library.
  • Loading branch information
archer-321 authored Jul 1, 2023
1 parent fc1a1d2 commit 089b84d
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 91 deletions.
8 changes: 3 additions & 5 deletions twilight-cache-inmemory/src/event/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,8 @@ mod tests {
application::{
command::CommandType,
interaction::{
application_command::{
CommandData, CommandInteractionDataResolved, InteractionMember,
},
Interaction, InteractionData, InteractionType,
application_command::CommandData, Interaction, InteractionData,
InteractionDataResolved, InteractionMember, InteractionType,
},
},
channel::{
Expand Down Expand Up @@ -143,7 +141,7 @@ mod tests {
name: "command name".into(),
kind: CommandType::ChatInput, // This isn't actually a valid command, so just mark it as a slash command.
options: Vec::new(),
resolved: Some(CommandInteractionDataResolved {
resolved: Some(InteractionDataResolved {
attachments: HashMap::new(),
channels: HashMap::new(),
members: HashMap::from([(
Expand Down
2 changes: 1 addition & 1 deletion twilight-cache-inmemory/src/event/member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
};
use std::borrow::Cow;
use twilight_model::{
application::interaction::application_command::InteractionMember,
application::interaction::InteractionMember,
gateway::payload::incoming::{MemberAdd, MemberChunk, MemberRemove, MemberUpdate},
guild::{Member, PartialMember},
id::{
Expand Down
2 changes: 1 addition & 1 deletion twilight-cache-inmemory/src/model/member.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::Serialize;
use twilight_model::{
application::interaction::application_command::InteractionMember,
application::interaction::InteractionMember,
guild::{Member, MemberFlags, PartialMember},
id::{
marker::{RoleMarker, UserMarker},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
//! [`ApplicationCommand`]: crate::application::interaction::InteractionType::ApplicationCommand

mod option;
mod resolved;

pub use self::{
option::{CommandDataOption, CommandOptionValue},
resolved::{CommandInteractionDataResolved, InteractionChannel, InteractionMember},
};
pub use self::option::{CommandDataOption, CommandOptionValue};

use crate::{
application::command::CommandType,
application::{command::CommandType, interaction::InteractionDataResolved},
id::{
marker::{CommandMarker, GenericMarker, GuildMarker},
Id,
Expand Down Expand Up @@ -44,7 +40,7 @@ pub struct CommandData {
pub options: Vec<CommandDataOption>,
/// Resolved data from the interaction's options.
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved: Option<CommandInteractionDataResolved>,
pub resolved: Option<InteractionDataResolved>,
/// If this is a user or message command, the ID of the targeted user/message.
#[serde(skip_serializing_if = "Option::is_none")]
pub target_id: Option<Id<GenericMarker>>,
Expand Down
22 changes: 15 additions & 7 deletions twilight-model/src/application/interaction/message_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
//!
//! [`MessageComponent`]: crate::application::interaction::InteractionType::MessageComponent

use crate::application::interaction::InteractionDataResolved;
use crate::channel::message::component::ComponentType;
use serde::{Deserialize, Serialize};

/// Data received when an [`MessageComponent`] interaction is executed.
/// Data received when a [`MessageComponent`] interaction is executed.
///
/// See [Discord Docs/Message Component Data Structure].
///
/// [`MessageComponent`]: crate::application::interaction::InteractionType::MessageComponent
/// [Discord Docs/Message Component Data Structure]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-message-component-data-structure
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MessageComponentInteractionData {
/// User defined identifier for the component.
///
Expand All @@ -21,11 +22,17 @@ pub struct MessageComponentInteractionData {
pub custom_id: String,
/// Type of the component.
pub component_type: ComponentType,
/// Converted users, roles, channels, or attachments.
///
/// Only used for [`SelectMenu`] components.
///
/// [`SelectMenu`]: crate::channel::message::component::SelectMenu
pub resolved: Option<InteractionDataResolved>,
/// Values selected by the user.
///
/// Only used for [`SelectMenu`] components.
///
/// [`SelectMenu`]: ComponentType::SelectMenu
/// [`SelectMenu`]: crate::channel::message::component::SelectMenu
#[serde(default)]
pub values: Vec<String>,
}
Expand All @@ -37,7 +44,7 @@ mod tests {
use serde::{Deserialize, Serialize};
use serde_test::Token;
use static_assertions::{assert_fields, assert_impl_all};
use std::{fmt::Debug, hash::Hash};
use std::fmt::Debug;

assert_fields!(
MessageComponentInteractionData: custom_id,
Expand All @@ -48,8 +55,6 @@ mod tests {
MessageComponentInteractionData: Clone,
Debug,
Deserialize<'static>,
Eq,
Hash,
PartialEq,
Send,
Serialize,
Expand All @@ -61,6 +66,7 @@ mod tests {
let value = MessageComponentInteractionData {
custom_id: "test".to_owned(),
component_type: ComponentType::Button,
resolved: None,
values: Vec::from(["1".to_owned(), "2".to_owned()]),
};

Expand All @@ -69,12 +75,14 @@ mod tests {
&[
Token::Struct {
name: "MessageComponentInteractionData",
len: 3,
len: 4,
},
Token::String("custom_id"),
Token::String("test"),
Token::String("component_type"),
Token::U8(ComponentType::Button.into()),
Token::String("resolved"),
Token::None,
Token::String("values"),
Token::Seq { len: Some(2) },
Token::String("1"),
Expand Down
19 changes: 10 additions & 9 deletions twilight-model/src/application/interaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ pub mod message_component;
pub mod modal;

mod interaction_type;
mod resolved;

pub use self::interaction_type::InteractionType;
pub use self::{
interaction_type::InteractionType,
resolved::{InteractionChannel, InteractionDataResolved, InteractionMember},
};

use self::{
application_command::CommandData, message_component::MessageComponentInteractionData,
Expand Down Expand Up @@ -424,7 +428,7 @@ pub enum InteractionData {
/// Data received for the [`MessageComponent`] interaction type.
///
/// [`MessageComponent`]: InteractionType::MessageComponent
MessageComponent(MessageComponentInteractionData),
MessageComponent(Box<MessageComponentInteractionData>),
/// Data received for the [`ModalSubmit`] interaction type.
///
/// [`ModalSubmit`]: InteractionType::ModalSubmit
Expand All @@ -434,11 +438,8 @@ pub enum InteractionData {
#[cfg(test)]
mod tests {
use super::{
application_command::{
CommandData, CommandDataOption, CommandInteractionDataResolved, CommandOptionValue,
InteractionMember,
},
Interaction, InteractionData, InteractionType,
application_command::{CommandData, CommandDataOption, CommandOptionValue},
Interaction, InteractionData, InteractionDataResolved, InteractionMember, InteractionType,
};
use crate::{
application::command::{CommandOptionType, CommandType},
Expand Down Expand Up @@ -508,7 +509,7 @@ mod tests {
name: "member".into(),
value: CommandOptionValue::User(Id::new(600)),
}]),
resolved: Some(CommandInteractionDataResolved {
resolved: Some(InteractionDataResolved {
attachments: HashMap::new(),
channels: HashMap::new(),
members: IntoIterator::into_iter([(
Expand Down Expand Up @@ -652,7 +653,7 @@ mod tests {
Token::Str("resolved"),
Token::Some,
Token::Struct {
name: "CommandInteractionDataResolved",
name: "InteractionDataResolved",
len: 2,
},
Token::Str("members"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::collections::hash_map::HashMap;
/// [`ApplicationCommand`]: crate::application::interaction::InteractionType::ApplicationCommand
/// [Discord Docs/Resolved Data Structure]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct CommandInteractionDataResolved {
pub struct InteractionDataResolved {
/// Map of resolved attachments.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub attachments: HashMap<Id<AttachmentMarker>, Attachment>,
Expand Down Expand Up @@ -97,7 +97,7 @@ pub struct InteractionMember {

#[cfg(test)]
mod tests {
use super::{CommandInteractionDataResolved, InteractionChannel, InteractionMember};
use super::{InteractionChannel, InteractionDataResolved, InteractionMember};
use crate::{
channel::{
message::{
Expand All @@ -122,7 +122,7 @@ mod tests {
let timestamp = Timestamp::from_str("2020-02-02T02:02:02.020000+00:00")?;
let flags = MemberFlags::BYPASSES_VERIFICATION | MemberFlags::DID_REJOIN;

let value = CommandInteractionDataResolved {
let value = InteractionDataResolved {
attachments: IntoIterator::into_iter([(
Id::new(400),
Attachment {
Expand Down Expand Up @@ -282,7 +282,7 @@ mod tests {
&value,
&[
Token::Struct {
name: "CommandInteractionDataResolved",
name: "InteractionDataResolved",
len: 6,
},
Token::Str("attachments"),
Expand Down
50 changes: 43 additions & 7 deletions twilight-model/src/channel/message/component/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,30 @@ pub enum ComponentType {
///
/// [`Button`]: super::Button
Button,
/// Component is an [`SelectMenu`].
/// Component is a [`SelectMenu`] with custom string options.
///
/// [`SelectMenu`]: super::SelectMenu
SelectMenu,
TextSelectMenu,
/// Component is an [`TextInput`].
///
/// [`TextInput`]: super::TextInput
TextInput,
/// Component is a [`SelectMenu`] for users.
///
/// [`SelectMenu`]: super::SelectMenu
UserSelectMenu,
/// Component is a [`SelectMenu`] for roles.
///
/// [`SelectMenu`]: super::SelectMenu
RoleSelectMenu,
/// Component is a [`SelectMenu`] for mentionables.
///
/// [`SelectMenu`]: super::SelectMenu
MentionableSelectMenu,
/// Component is a [`SelectMenu`] for channels.
///
/// [`SelectMenu`]: super::SelectMenu
ChannelSelectMenu,
/// Variant value is unknown to the library.
Unknown(u8),
}
Expand All @@ -33,8 +49,12 @@ impl From<u8> for ComponentType {
match value {
1 => ComponentType::ActionRow,
2 => ComponentType::Button,
3 => ComponentType::SelectMenu,
3 => ComponentType::TextSelectMenu,
4 => ComponentType::TextInput,
5 => ComponentType::UserSelectMenu,
6 => ComponentType::RoleSelectMenu,
7 => ComponentType::MentionableSelectMenu,
8 => ComponentType::ChannelSelectMenu,
unknown => ComponentType::Unknown(unknown),
}
}
Expand All @@ -45,8 +65,12 @@ impl From<ComponentType> for u8 {
match value {
ComponentType::ActionRow => 1,
ComponentType::Button => 2,
ComponentType::SelectMenu => 3,
ComponentType::TextSelectMenu => 3,
ComponentType::TextInput => 4,
ComponentType::UserSelectMenu => 5,
ComponentType::RoleSelectMenu => 6,
ComponentType::MentionableSelectMenu => 7,
ComponentType::ChannelSelectMenu => 8,
ComponentType::Unknown(unknown) => unknown,
}
}
Expand All @@ -72,7 +96,11 @@ impl ComponentType {
match self {
Self::ActionRow => "ActionRow",
Self::Button => "Button",
Self::SelectMenu => "SelectMenu",
Self::TextSelectMenu
| Self::UserSelectMenu
| Self::RoleSelectMenu
| Self::MentionableSelectMenu
| Self::ChannelSelectMenu => "SelectMenu",
Self::TextInput => "TextInput",
Self::Unknown(_) => "Unknown",
}
Expand Down Expand Up @@ -110,16 +138,24 @@ mod tests {
fn variants() {
serde_test::assert_tokens(&ComponentType::ActionRow, &[Token::U8(1)]);
serde_test::assert_tokens(&ComponentType::Button, &[Token::U8(2)]);
serde_test::assert_tokens(&ComponentType::SelectMenu, &[Token::U8(3)]);
serde_test::assert_tokens(&ComponentType::TextSelectMenu, &[Token::U8(3)]);
serde_test::assert_tokens(&ComponentType::TextInput, &[Token::U8(4)]);
serde_test::assert_tokens(&ComponentType::UserSelectMenu, &[Token::U8(5)]);
serde_test::assert_tokens(&ComponentType::RoleSelectMenu, &[Token::U8(6)]);
serde_test::assert_tokens(&ComponentType::MentionableSelectMenu, &[Token::U8(7)]);
serde_test::assert_tokens(&ComponentType::ChannelSelectMenu, &[Token::U8(8)]);
serde_test::assert_tokens(&ComponentType::Unknown(99), &[Token::U8(99)]);
}

#[test]
fn names() {
assert_eq!("ActionRow", ComponentType::ActionRow.name());
assert_eq!("Button", ComponentType::Button.name());
assert_eq!("SelectMenu", ComponentType::SelectMenu.name());
assert_eq!("SelectMenu", ComponentType::TextSelectMenu.name());
assert_eq!("SelectMenu", ComponentType::UserSelectMenu.name());
assert_eq!("SelectMenu", ComponentType::RoleSelectMenu.name());
assert_eq!("SelectMenu", ComponentType::MentionableSelectMenu.name());
assert_eq!("SelectMenu", ComponentType::ChannelSelectMenu.name());
assert_eq!("TextInput", ComponentType::TextInput.name());
assert_eq!("Unknown", ComponentType::Unknown(99).name());
}
Expand Down
Loading

0 comments on commit 089b84d

Please sign in to comment.