Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Redesign the
Parse
trait and add support for most applicable model …
…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
Showing
11 changed files
with
825 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Oops, something went wrong.