Skip to content

Commit

Permalink
feat(mention): add mention for commands (#2290)
Browse files Browse the repository at this point in the history
  • Loading branch information
cptpiepmatz authored Nov 2, 2023
1 parent 60c11e9 commit 138dfa3
Show file tree
Hide file tree
Showing 3 changed files with 443 additions and 3 deletions.
160 changes: 157 additions & 3 deletions twilight-mention/src/fmt.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Formatters for creating mentions.
use super::timestamp::Timestamp;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::fmt::{Display, Formatter, Result as FmtResult, Write};
use twilight_model::{
channel::Channel,
guild::{Emoji, Member, Role},
id::{
marker::{ChannelMarker, EmojiMarker, RoleMarker, UserMarker},
marker::{ChannelMarker, CommandMarker, EmojiMarker, RoleMarker, UserMarker},
Id,
},
user::{CurrentUser, User},
Expand Down Expand Up @@ -37,6 +37,54 @@ impl Display for MentionFormat<Id<ChannelMarker>> {
}
}

/// Mention a command. This will format as:
/// - `</NAME:COMMAND_ID>` for commands
/// - `</NAME SUBCOMMAND:ID>` for subcommands
/// - `</NAME SUBCOMMAND_GROUP SUBCOMMAND:ID>` for subcommand groups
impl Display for MentionFormat<CommandMention> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str("</")?;

match &self.0 {
CommandMention::Command { name, id } => {
// </NAME:COMMAND_ID>
f.write_str(name)?;
f.write_char(':')?;
Display::fmt(id, f)?;
}
CommandMention::SubCommand {
name,
sub_command,
id,
} => {
// </NAME SUBCOMMAND:ID>
f.write_str(name)?;
f.write_char(' ')?;
f.write_str(sub_command)?;
f.write_char(':')?;
Display::fmt(id, f)?;
}
CommandMention::SubCommandGroup {
name,
sub_command_group,
sub_command,
id,
} => {
// </NAME SUBCOMMAND_GROUP SUBCOMMAND:ID>
f.write_str(name)?;
f.write_char(' ')?;
f.write_str(sub_command_group)?;
f.write_char(' ')?;
f.write_str(sub_command)?;
f.write_char(':')?;
Display::fmt(id, f)?;
}
}

f.write_char('>')
}
}

/// Mention an emoji. This will format as `<:emoji:ID>`.
impl Display for MentionFormat<Id<EmojiMarker>> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Expand Down Expand Up @@ -125,6 +173,41 @@ impl Mention<Id<ChannelMarker>> for Channel {
}
}

/// Mention a command.
///
/// This will format as:
/// - `</NAME:COMMAND_ID>` for commands
/// - `</NAME SUBCOMMAND:ID>` for subcommands
/// - `</NAME SUBCOMMAND_GROUP SUBCOMMAND:ID>` for subcommand groups
///
/// # Cloning
///
/// This implementation uses [`clone`](Clone::clone) to construct a [`MentionFormat`] that owns the
/// inner `CommandMention` as [`mention`](Mention::mention) takes a `&self`.
/// The other implementations do this for types that are [`Copy`] and therefore do not need to use
/// [`clone`](Clone::clone).
///
/// To avoid cloning use [`CommandMention::into_mention`].
impl Mention<CommandMention> for CommandMention {
fn mention(&self) -> MentionFormat<CommandMention> {
MentionFormat(self.clone())
}
}

impl CommandMention {
/// Mention a command.
///
/// This will format as:
/// - `</NAME:COMMAND_ID>` for commands
/// - `</NAME SUBCOMMAND:ID>` for subcommands
/// - `</NAME SUBCOMMAND_GROUP SUBCOMMAND:ID>` for subcommand groups
///
/// This is a self-consuming alternative to [`CommandMention::mention`] and avoids cloning.
pub const fn into_mention(self) -> MentionFormat<CommandMention> {
MentionFormat(self)
}
}

/// Mention the current user. This will format as `<@ID>`.
impl Mention<Id<UserMarker>> for CurrentUser {
fn mention(&self) -> MentionFormat<Id<UserMarker>> {
Expand Down Expand Up @@ -189,13 +272,44 @@ impl Mention<Id<UserMarker>> for User {
}
}

/// Components to construct a slash command mention.
///
/// Format slash commands, subcommands and subcommand groups.
/// See [Discord Docs/Message Formatting].
/// See [Discord Docs Changelog/Slash Command Mentions].
///
/// [Discord Docs/Message Formatting]: https://discord.com/developers/docs/reference#message-formatting
/// [Discord Docs Changelog/Slash Command Mentions]: https://discord.com/developers/docs/change-log#slash-command-mentions
#[allow(missing_docs)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandMention {
Command {
id: Id<CommandMarker>,
name: String,
},

SubCommand {
id: Id<CommandMarker>,
name: String,
sub_command: String,
},

SubCommandGroup {
id: Id<CommandMarker>,
name: String,
sub_command: String,
sub_command_group: String,
},
}

#[cfg(test)]
mod tests {
use crate::timestamp::{Timestamp, TimestampStyle};

use super::{Mention, MentionFormat};
use super::{CommandMention, Mention, MentionFormat};
use static_assertions::assert_impl_all;
use std::fmt::{Debug, Display};
use twilight_model::id::marker::CommandMarker;
use twilight_model::{
channel::Channel,
guild::{Emoji, Member, Role},
Expand All @@ -208,6 +322,7 @@ mod tests {

assert_impl_all!(MentionFormat<()>: Clone, Copy, Debug, Eq, PartialEq, Send, Sync);
assert_impl_all!(MentionFormat<Id<ChannelMarker>>: Clone, Copy, Debug, Display, Eq, PartialEq, Send, Sync);
assert_impl_all!(MentionFormat<CommandMention>: Clone, Debug, Display, Eq, PartialEq, Send, Sync);
assert_impl_all!(MentionFormat<Id<EmojiMarker>>: Clone, Copy, Debug, Display, Eq, PartialEq, Send, Sync);
assert_impl_all!(MentionFormat<Id<RoleMarker>>: Clone, Copy, Debug, Display, Eq, PartialEq, Send, Sync);
assert_impl_all!(MentionFormat<Id<UserMarker>>: Clone, Copy, Debug, Display, Eq, PartialEq, Send, Sync);
Expand Down Expand Up @@ -240,6 +355,45 @@ mod tests {
);
}

#[test]
fn mention_format_command() {
assert_eq!(
"</name:123>",
MentionFormat(CommandMention::Command {
id: Id::<CommandMarker>::new(123),
name: "name".to_string()
})
.to_string()
);
}

#[test]
fn mention_format_sub_command() {
assert_eq!(
"</name subcommand:123>",
MentionFormat(CommandMention::SubCommand {
id: Id::<CommandMarker>::new(123),
name: "name".to_string(),
sub_command: "subcommand".to_string()
})
.to_string()
);
}

#[test]
fn mention_format_sub_command_group() {
assert_eq!(
"</name subcommand_group subcommand:123>",
MentionFormat(CommandMention::SubCommandGroup {
id: Id::<CommandMarker>::new(123),
name: "name".to_string(),
sub_command: "subcommand".to_string(),
sub_command_group: "subcommand_group".to_string()
})
.to_string()
);
}

#[test]
fn mention_format_emoji_id() {
assert_eq!(
Expand Down
22 changes: 22 additions & 0 deletions twilight-mention/src/parse/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Debug;
use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
Expand Down Expand Up @@ -51,6 +52,10 @@ impl Display for ParseMentionError<'_> {

f.write_str("') of mention is not a u64")
}
ParseMentionErrorType::ExtraneousPart { found } => {
f.write_str("found extraneous part ")?;
Debug::fmt(found, f)
}
ParseMentionErrorType::LeadingArrow { found } => {
f.write_str("expected to find a leading arrow ('<') but instead found ")?;

Expand Down Expand Up @@ -133,6 +138,11 @@ pub enum ParseMentionErrorType<'a> {
/// String that could not be parsed into a u64.
found: &'a str,
},
/// An extra part was found that shouldn't be present.
ExtraneousPart {
/// The extra part that was found.
found: &'a str,
},
/// Leading arrow (`<`) is not present.
LeadingArrow {
/// Character that was instead found where the leading arrow should be.
Expand Down Expand Up @@ -177,6 +187,7 @@ mod tests {
use std::{error::Error, fmt::Debug};

assert_fields!(ParseMentionErrorType::IdNotU64: found);
assert_fields!(ParseMentionErrorType::ExtraneousPart: found);
assert_fields!(ParseMentionErrorType::LeadingArrow: found);
assert_fields!(ParseMentionErrorType::Sigil: expected, found);
assert_fields!(ParseMentionErrorType::TimestampStyleInvalid: found);
Expand All @@ -196,6 +207,17 @@ mod tests {
}
.to_string(),
);

expected = "found extraneous part \"abc\"";
assert_eq!(
expected,
ParseMentionError {
kind: ParseMentionErrorType::ExtraneousPart { found: "abc" },
source: None
}
.to_string()
);

expected = "expected to find a leading arrow ('<') but instead found 'a'";
assert_eq!(
expected,
Expand Down
Loading

0 comments on commit 138dfa3

Please sign in to comment.