Skip to content

Commit

Permalink
Redesign the Parse trait and add support for most applicable model …
Browse files Browse the repository at this point in the history
…types (#1380)

This commit redesigns and renames the `Parse` trait - which is now called `ArgumentConvert` - according to the ideas from #1327. This is not a breaking change, since a dummy version of the `Parse` trait is still present which internally delegates to `ArgumentConvert`.

Additionally, there is now `ArgumentConvert` support for most applicable model types:
- `Channel`
- `GuildChannel`
- `ChannelCategory`
- `Emoji`
- `GuildChannel`
- `Member` (already supported)
- `Message` (already supported)
- `Role`
- `User`

I oriented myself at [discord.py's converters.][0]

[0]: https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#discord-converters
  • Loading branch information
kangalio committed Aug 9, 2021
1 parent ea8ec29 commit eb14984
Show file tree
Hide file tree
Showing 11 changed files with 825 additions and 191 deletions.
45 changes: 45 additions & 0 deletions src/utils/argument_convert/_template.rs
@@ -0,0 +1,45 @@
use super::ArgumentConvert;
use crate::{model::prelude::*, prelude::*};

/// Error that can be returned from [`PLACEHOLDER::convert`].
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum PLACEHOLDERParseError {
}

impl std::error::Error for PLACEHOLDERParseError {}

impl std::fmt::Display for PLACEHOLDERParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
}
}
}

/// Look up a [`PLACEHOLDER`] by a string case-insensitively.
///
/// Requires the cache feature to be enabled.
///
/// The lookup strategy is as follows (in order):
/// 1. Lookup by PLACEHOLDER
/// 2. [Lookup by PLACEHOLDER](`crate::utils::parse_PLACEHOLDER`).
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for PLACEHOLDER {
type Err = PLACEHOLDERParseError;

async fn convert(
ctx: &Context,
guild_id: Option<GuildId>,
_channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
let lookup_by_PLACEHOLDER = || PLACEHOLDER;

lookup_by_PLACEHOLDER()
.or_else(lookup_by_PLACEHOLDER)
.or_else(lookup_by_PLACEHOLDER)
.cloned()
.ok_or(PLACEHOLDERParseError::NotFoundOrMalformed)
}
}
209 changes: 209 additions & 0 deletions src/utils/argument_convert/channel.rs
@@ -0,0 +1,209 @@
use super::ArgumentConvert;
use crate::{model::prelude::*, prelude::*};

/// Error that can be returned from [`Channel::convert`].
#[non_exhaustive]
#[derive(Debug)]
pub enum ChannelParseError {
/// When channel retrieval via HTTP failed
Http(SerenityError),
/// The provided channel string failed to parse, or the parsed result cannot be found in the
/// cache.
NotFoundOrMalformed,
}

impl std::error::Error for ChannelParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Http(e) => Some(e),
Self::NotFoundOrMalformed => None,
}
}
}

impl std::fmt::Display for ChannelParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http(_) => write!(f, "Failed to request channel via HTTP"),
Self::NotFoundOrMalformed => write!(f, "Channel not found or unknown format"),
}
}
}

fn channel_belongs_to_guild(channel: &Channel, guild: GuildId) -> bool {
match channel {
Channel::Guild(channel) => channel.guild_id == guild,
Channel::Category(channel) => channel.guild_id == guild,
Channel::Private(_channel) => false,
}
}

async fn lookup_channel_global(ctx: &Context, s: &str) -> Result<Channel, ChannelParseError> {
if let Some(channel_id) = s.parse::<u64>().ok().or_else(|| crate::utils::parse_channel(s)) {
return ChannelId(channel_id).to_channel(ctx).await.map_err(ChannelParseError::Http);
}

let channels = ctx.cache.channels.read().await;
if let Some(channel) =
channels.values().find(|channel| channel.name.eq_ignore_ascii_case(s)).cloned()
{
return Ok(Channel::Guild(channel));
}

Err(ChannelParseError::NotFoundOrMalformed)
}

/// Look up a Channel by a string case-insensitively.
///
/// Lookup are done via local guild. If in DMs, the global cache is used instead.
///
/// The cache feature needs to be enabled.
///
/// The lookup strategy is as follows (in order):
/// 1. Lookup by ID.
/// 2. [Lookup by mention](`crate::utils::parse_channel`).
/// 3. Lookup by name.
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for Channel {
type Err = ChannelParseError;

async fn convert(
ctx: &Context,
guild_id: Option<GuildId>,
_channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
let channel = lookup_channel_global(ctx, s).await?;

// Don't yield for other guilds' channels
if let Some(guild_id) = guild_id {
if !channel_belongs_to_guild(&channel, guild_id) {
return Err(ChannelParseError::NotFoundOrMalformed);
}
};

Ok(channel)
}
}

/// Error that can be returned from [`GuildChannel::convert`].
#[non_exhaustive]
#[derive(Debug)]
pub enum GuildChannelParseError {
/// When channel retrieval via HTTP failed
Http(SerenityError),
/// The provided channel string failed to parse, or the parsed result cannot be found in the
/// cache.
NotFoundOrMalformed,
/// When the referenced channel is not a guild channel
NotAGuildChannel,
}

impl std::error::Error for GuildChannelParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Http(e) => Some(e),
Self::NotFoundOrMalformed => None,
Self::NotAGuildChannel => None,
}
}
}

impl std::fmt::Display for GuildChannelParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http(_) => write!(f, "Failed to request channel via HTTP"),
Self::NotFoundOrMalformed => write!(f, "Channel not found or unknown format"),
Self::NotAGuildChannel => write!(f, "Channel is not a guild channel"),
}
}
}

/// Look up a GuildChannel by a string case-insensitively.
///
/// Lookup is done by the global cache, hence the cache feature needs to be enabled.
///
/// For more information, see the ArgumentConvert implementation for [`Channel`]
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for GuildChannel {
type Err = GuildChannelParseError;

async fn convert(
ctx: &Context,
guild_id: Option<GuildId>,
channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
match Channel::convert(ctx, guild_id, channel_id, s).await {
Ok(Channel::Guild(channel)) => Ok(channel),
Ok(_) => Err(GuildChannelParseError::NotAGuildChannel),
Err(ChannelParseError::Http(e)) => Err(GuildChannelParseError::Http(e)),
Err(ChannelParseError::NotFoundOrMalformed) => {
Err(GuildChannelParseError::NotFoundOrMalformed)
},
}
}
}

/// Error that can be returned from [`ChannelCategory::convert`].
#[non_exhaustive]
#[derive(Debug)]
pub enum ChannelCategoryParseError {
/// When channel retrieval via HTTP failed
Http(SerenityError),
/// The provided channel string failed to parse, or the parsed result cannot be found in the
/// cache.
NotFoundOrMalformed,
/// When the referenced channel is not a channel category
NotAChannelCategory,
}

impl std::error::Error for ChannelCategoryParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Http(e) => Some(e),
Self::NotFoundOrMalformed => None,
Self::NotAChannelCategory => None,
}
}
}

impl std::fmt::Display for ChannelCategoryParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http(_) => write!(f, "Failed to request channel via HTTP"),
Self::NotFoundOrMalformed => write!(f, "Channel not found or unknown format"),
Self::NotAChannelCategory => write!(f, "Channel is not a channel category"),
}
}
}

/// Look up a ChannelCategory by a string case-insensitively.
///
/// Lookup is done by the global cache, hence the cache feature needs to be enabled.
///
/// For more information, see the ArgumentConvert implementation for [`Channel`]
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for ChannelCategory {
type Err = ChannelCategoryParseError;

async fn convert(
ctx: &Context,
guild_id: Option<GuildId>,
channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
match Channel::convert(ctx, guild_id, channel_id, s).await {
Ok(Channel::Category(channel)) => Ok(channel),
// TODO: accomodate issue #1352 somehow
Ok(_) => Err(ChannelCategoryParseError::NotAChannelCategory),
Err(ChannelParseError::Http(e)) => Err(ChannelCategoryParseError::Http(e)),
Err(ChannelParseError::NotFoundOrMalformed) => {
Err(ChannelCategoryParseError::NotFoundOrMalformed)
},
}
}
}
63 changes: 63 additions & 0 deletions src/utils/argument_convert/emoji.rs
@@ -0,0 +1,63 @@
use super::ArgumentConvert;
use crate::{model::prelude::*, prelude::*};

/// Error that can be returned from [`Emoji::convert`].
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum EmojiParseError {
/// The provided emoji string failed to parse, or the parsed result cannot be found in the
/// cache.
NotFoundOrMalformed,
}

impl std::error::Error for EmojiParseError {}

impl std::fmt::Display for EmojiParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFoundOrMalformed => write!(f, "Emoji not found or unknown format"),
}
}
}

/// Look up a [`Emoji`].
///
/// Requires the cache feature to be enabled.
///
/// The lookup strategy is as follows (in order):
/// 1. Lookup by ID.
/// 2. [Lookup by extracting ID from the emoji](`crate::utils::parse_emoji`).
/// 3. Lookup by name.
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for Emoji {
type Err = EmojiParseError;

async fn convert(
ctx: &Context,
_guild_id: Option<GuildId>,
_channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
let guilds = ctx.cache.guilds.read().await;

let direct_id = s.parse::<u64>().ok().map(EmojiId);
let id_from_mention = crate::utils::parse_emoji(s).map(|e| e.id);

if let Some(emoji_id) = direct_id.or(id_from_mention) {
if let Some(emoji) = guilds.values().find_map(|guild| guild.emojis.get(&emoji_id)) {
return Ok(emoji.clone());
}
}

if let Some(emoji) = guilds
.values()
.flat_map(|guild| guild.emojis.values())
.find(|emoji| emoji.name.eq_ignore_ascii_case(s))
{
return Ok(emoji.clone());
}

Err(EmojiParseError::NotFoundOrMalformed)
}
}
45 changes: 45 additions & 0 deletions src/utils/argument_convert/guild.rs
@@ -0,0 +1,45 @@
use super::ArgumentConvert;
use crate::{model::prelude::*, prelude::*};

/// Error that can be returned from [`Guild::convert`].
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum GuildParseError {
/// The provided guild string failed to parse, or the parsed result cannot be found in the
/// cache.
NotFoundOrMalformed,
}

impl std::error::Error for GuildParseError {}

impl std::fmt::Display for GuildParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFoundOrMalformed => write!(f, "Guild not found or unknown format"),
}
}
}

/// Look up a Guild, either by ID or by a string case-insensitively.
///
/// Requires the cache feature to be enabled.
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl ArgumentConvert for Guild {
type Err = GuildParseError;

async fn convert(
ctx: &Context,
_guild_id: Option<GuildId>,
_channel_id: Option<ChannelId>,
s: &str,
) -> Result<Self, Self::Err> {
let guilds = ctx.cache.guilds.read().await;

let lookup_by_id = || guilds.get(&GuildId(s.parse().ok()?));

let lookup_by_name = || guilds.values().find(|guild| guild.name.eq_ignore_ascii_case(s));

lookup_by_id().or_else(lookup_by_name).cloned().ok_or(GuildParseError::NotFoundOrMalformed)
}
}

0 comments on commit eb14984

Please sign in to comment.