use http::Method as HttpMethod;
use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Method {
Delete,
Get,
Patch,
Post,
Put,
}
impl Method {
#[must_use]
pub const fn to_http(self) -> HttpMethod {
match self {
Self::Delete => HttpMethod::DELETE,
Self::Get => HttpMethod::GET,
Self::Patch => HttpMethod::PATCH,
Self::Post => HttpMethod::POST,
Self::Put => HttpMethod::PUT,
}
}
}
#[derive(Debug)]
pub struct PathParseError {
kind: PathParseErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl PathParseError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &PathParseErrorType {
&self.kind
}
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(self) -> (PathParseErrorType, Option<Box<dyn Error + Send + Sync>>) {
(self.kind, self.source)
}
}
impl Display for PathParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
PathParseErrorType::IntegerParsing { .. } => {
f.write_str("An ID in a segment was invalid")
}
PathParseErrorType::MessageIdWithoutMethod { .. } => {
f.write_str("A message path was detected but the method wasn't given")
}
PathParseErrorType::NoMatch => f.write_str("There was no matched path"),
}
}
}
impl Error for PathParseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PathParseErrorType {
IntegerParsing,
MessageIdWithoutMethod {
channel_id: u64,
},
NoMatch,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Path {
ApplicationCommand(u64),
ApplicationCommandId(u64),
ApplicationGuildCommand(u64),
ApplicationGuildCommandId(u64),
ChannelsId(u64),
ChannelsIdFollowers(u64),
ChannelsIdInvites(u64),
ChannelsIdMessages(u64),
ChannelsIdMessagesBulkDelete(u64),
ChannelsIdMessagesId(Method, u64),
ChannelsIdMessagesIdCrosspost(u64),
ChannelsIdMessagesIdReactions(u64),
ChannelsIdMessagesIdReactionsUserIdType(u64),
ChannelsIdMessagesIdThreads(u64),
ChannelsIdPermissionsOverwriteId(u64),
ChannelsIdPins(u64),
ChannelsIdPinsMessageId(u64),
ChannelsIdRecipients(u64),
ChannelsIdThreadMembers(u64),
ChannelsIdThreadMembersId(u64),
ChannelsIdThreads(u64),
ChannelsIdTyping(u64),
ChannelsIdWebhooks(u64),
Gateway,
GatewayBot,
Guilds,
GuildsId(u64),
GuildsIdAuditLogs(u64),
GuildsIdAutoModerationRules(u64),
GuildsIdAutoModerationRulesId(u64),
GuildsIdBans(u64),
GuildsIdBansId(u64),
GuildsIdBansUserId(u64),
GuildsIdChannels(u64),
GuildsIdEmojis(u64),
GuildsIdEmojisId(u64),
GuildsIdIntegrations(u64),
GuildsIdIntegrationsId(u64),
GuildsIdIntegrationsIdSync(u64),
GuildsIdInvites(u64),
GuildsIdMembers(u64),
GuildsIdMembersId(u64),
GuildsIdMembersIdRolesId(u64),
GuildsIdMembersMeNick(u64),
GuildsIdMembersSearch(u64),
GuildsIdMfa(u64),
GuildsIdPreview(u64),
GuildsIdPrune(u64),
GuildsIdRegions(u64),
GuildsIdRoles(u64),
GuildsIdRolesId(u64),
GuildsIdScheduledEvents(u64),
GuildsIdScheduledEventsId(u64),
GuildsIdScheduledEventsIdUsers(u64),
GuildsIdStickers(u64),
GuildsIdTemplates(u64),
GuildsIdTemplatesCode(u64, String),
GuildsIdThreads(u64),
GuildsIdVanityUrl(u64),
GuildsIdVoiceStates(u64),
GuildsIdWebhooks(u64),
GuildsIdWelcomeScreen(u64),
GuildsIdWidget(u64),
GuildsIdWidgetJson(u64),
GuildsTemplatesCode(String),
InteractionCallback(u64),
InvitesCode,
OauthApplicationsMe,
StageInstances,
StickerPacks,
Stickers,
UsersId,
UsersIdChannels,
UsersIdConnections,
UsersIdGuilds,
UsersIdGuildsId,
UsersIdGuildsIdMember,
VoiceRegions,
WebhooksId(u64),
WebhooksIdToken(u64, String),
WebhooksIdTokenMessagesId(u64, String),
}
impl FromStr for Path {
type Err = PathParseError;
#[allow(clippy::enum_glob_use, clippy::too_many_lines)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Path::*;
fn parse_id(id: &str) -> Result<u64, PathParseError> {
id.parse().map_err(|source| PathParseError {
kind: PathParseErrorType::IntegerParsing,
source: Some(Box::new(source)),
})
}
let skip = usize::from(s.starts_with('/'));
let parts = s.split('/').skip(skip).collect::<Vec<&str>>();
Ok(match parts[..] {
["applications", id, "commands"] => ApplicationCommand(parse_id(id)?),
["applications", id, "commands", _] => ApplicationCommandId(parse_id(id)?),
["applications", id, "guilds", _, "commands"]
| ["applications", id, "guilds", _, "commands", "permissions"] => {
ApplicationGuildCommand(parse_id(id)?)
}
["applications", id, "guilds", _, "commands", _]
| ["applications", id, "guilds", _, "commands", _, "permissions"] => {
ApplicationGuildCommandId(parse_id(id)?)
}
["channels", id] => ChannelsId(parse_id(id)?),
["channels", id, "followers"] => ChannelsIdFollowers(parse_id(id)?),
["channels", id, "invites"] => ChannelsIdInvites(parse_id(id)?),
["channels", id, "messages"] => ChannelsIdMessages(parse_id(id)?),
["channels", id, "messages", "bulk-delete"] => {
ChannelsIdMessagesBulkDelete(parse_id(id)?)
}
["channels", id, "messages", _] => {
return Err(PathParseError {
kind: PathParseErrorType::MessageIdWithoutMethod {
channel_id: parse_id(id)?,
},
source: None,
});
}
["channels", id, "messages", _, "crosspost"] => {
ChannelsIdMessagesIdCrosspost(parse_id(id)?)
}
["channels", id, "messages", _, "reactions"]
| ["channels", id, "messages", _, "reactions", _] => {
ChannelsIdMessagesIdReactions(parse_id(id)?)
}
["channels", id, "messages", _, "reactions", _, _] => {
ChannelsIdMessagesIdReactionsUserIdType(parse_id(id)?)
}
["channels", id, "messages", _, "threads"] => {
ChannelsIdMessagesIdThreads(parse_id(id)?)
}
["channels", id, "permissions", _] => ChannelsIdPermissionsOverwriteId(parse_id(id)?),
["channels", id, "pins"] => ChannelsIdPins(parse_id(id)?),
["channels", id, "pins", _] => ChannelsIdPinsMessageId(parse_id(id)?),
["channels", id, "recipients"] | ["channels", id, "recipients", _] => {
ChannelsIdRecipients(parse_id(id)?)
}
["channels", id, "thread-members"] => ChannelsIdThreadMembers(parse_id(id)?),
["channels", id, "thread-members", _] => ChannelsIdThreadMembersId(parse_id(id)?),
["channels", id, "threads"] => ChannelsIdThreads(parse_id(id)?),
["channels", id, "typing"] => ChannelsIdTyping(parse_id(id)?),
["channels", id, "webhooks"] | ["channels", id, "webhooks", _] => {
ChannelsIdWebhooks(parse_id(id)?)
}
["gateway"] => Gateway,
["gateway", "bot"] => GatewayBot,
["guilds"] => Guilds,
["guilds", "templates", code] => GuildsTemplatesCode(code.to_string()),
["guilds", id] => GuildsId(parse_id(id)?),
["guilds", id, "audit-logs"] => GuildsIdAuditLogs(parse_id(id)?),
["guilds", id, "bans"] => GuildsIdBans(parse_id(id)?),
["guilds", id, "bans", _] => GuildsIdBansUserId(parse_id(id)?),
["guilds", id, "channels"] => GuildsIdChannels(parse_id(id)?),
["guilds", id, "emojis"] => GuildsIdEmojis(parse_id(id)?),
["guilds", id, "emojis", _] => GuildsIdEmojisId(parse_id(id)?),
["guilds", id, "integrations"] => GuildsIdIntegrations(parse_id(id)?),
["guilds", id, "integrations", _] => GuildsIdIntegrationsId(parse_id(id)?),
["guilds", id, "integrations", _, "sync"] => GuildsIdIntegrationsIdSync(parse_id(id)?),
["guilds", id, "invites"] => GuildsIdInvites(parse_id(id)?),
["guilds", id, "members"] => GuildsIdMembers(parse_id(id)?),
["guilds", id, "members", "search"] => GuildsIdMembersSearch(parse_id(id)?),
["guilds", id, "members", _] => GuildsIdMembersId(parse_id(id)?),
["guilds", id, "members", _, "roles", _] => GuildsIdMembersIdRolesId(parse_id(id)?),
["guilds", id, "members", "@me", "nick"] => GuildsIdMembersMeNick(parse_id(id)?),
["guilds", id, "preview"] => GuildsIdPreview(parse_id(id)?),
["guilds", id, "prune"] => GuildsIdPrune(parse_id(id)?),
["guilds", id, "regions"] => GuildsIdRegions(parse_id(id)?),
["guilds", id, "roles"] => GuildsIdRoles(parse_id(id)?),
["guilds", id, "roles", _] => GuildsIdRolesId(parse_id(id)?),
["guilds", id, "scheduled-events"] => GuildsIdScheduledEvents(parse_id(id)?),
["guilds", id, "scheduled-events", _] => GuildsIdScheduledEventsId(parse_id(id)?),
["guilds", id, "scheduled-events", _, "users"] => {
GuildsIdScheduledEventsIdUsers(parse_id(id)?)
}
["guilds", id, "stickers"] | ["guilds", id, "stickers", _] => {
GuildsIdStickers(parse_id(id)?)
}
["guilds", id, "templates"] => GuildsIdTemplates(parse_id(id)?),
["guilds", id, "templates", code] => {
GuildsIdTemplatesCode(parse_id(id)?, code.to_string())
}
["guilds", id, "threads", _] => GuildsIdThreads(parse_id(id)?),
["guilds", id, "vanity-url"] => GuildsIdVanityUrl(parse_id(id)?),
["guilds", id, "voice-states", _] => GuildsIdVoiceStates(parse_id(id)?),
["guilds", id, "welcome-screen"] => GuildsIdWelcomeScreen(parse_id(id)?),
["guilds", id, "webhooks"] => GuildsIdWebhooks(parse_id(id)?),
["guilds", id, "widget"] => GuildsIdWidget(parse_id(id)?),
["guilds", id, "widget.json"] => GuildsIdWidgetJson(parse_id(id)?),
["invites", _] => InvitesCode,
["interactions", id, _, "callback"] => InteractionCallback(parse_id(id)?),
["stage-instances", _] => StageInstances,
["sticker-packs"] => StickerPacks,
["stickers", _] => Stickers,
["oauth2", "applications", "@me"] => OauthApplicationsMe,
["users", _] => UsersId,
["users", _, "connections"] => UsersIdConnections,
["users", _, "channels"] => UsersIdChannels,
["users", _, "guilds"] => UsersIdGuilds,
["users", _, "guilds", _] => UsersIdGuildsId,
["users", _, "guilds", _, "member"] => UsersIdGuildsIdMember,
["voice", "regions"] => VoiceRegions,
["webhooks", id] => WebhooksId(parse_id(id)?),
["webhooks", id, token] => WebhooksIdToken(parse_id(id)?, token.to_string()),
["webhooks", id, token, "messages", _] => {
WebhooksIdTokenMessagesId(parse_id(id)?, token.to_string())
}
_ => {
return Err(PathParseError {
kind: PathParseErrorType::NoMatch,
source: None,
})
}
})
}
}
impl TryFrom<(Method, &str)> for Path {
type Error = PathParseError;
fn try_from((method, s): (Method, &str)) -> Result<Self, Self::Error> {
match Self::from_str(s) {
Ok(v) => Ok(v),
Err(why) => {
if let PathParseErrorType::MessageIdWithoutMethod { channel_id } = why.kind() {
Ok(Self::ChannelsIdMessagesId(method, *channel_id))
} else {
Err(why)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::{Path, PathParseError, PathParseErrorType};
use crate::request::Method;
use http::Method as HttpMethod;
use static_assertions::{assert_fields, assert_impl_all};
use std::{error::Error, fmt::Debug, hash::Hash, str::FromStr};
assert_fields!(PathParseErrorType::MessageIdWithoutMethod: channel_id);
assert_impl_all!(PathParseErrorType: Debug, Send, Sync);
assert_impl_all!(PathParseError: Error, Send, Sync);
assert_impl_all!(Path: Clone, Debug, Eq, Hash, PartialEq, Send, Sync);
#[test]
fn prefix_unimportant() -> Result<(), Box<dyn Error>> {
assert_eq!(Path::Guilds, Path::from_str("guilds")?);
assert_eq!(Path::Guilds, Path::from_str("/guilds")?);
Ok(())
}
#[test]
fn from_str() -> Result<(), Box<dyn Error>> {
assert_eq!(Path::ChannelsId(123), Path::from_str("/channels/123")?);
assert_eq!(Path::WebhooksId(123), Path::from_str("/webhooks/123")?);
assert_eq!(Path::InvitesCode, Path::from_str("/invites/abc")?);
Ok(())
}
#[test]
fn message_id() -> Result<(), Box<dyn Error>> {
assert!(matches!(
Path::from_str("channels/123/messages/456")
.unwrap_err()
.kind(),
PathParseErrorType::MessageIdWithoutMethod { channel_id: 123 },
));
assert_eq!(
Path::ChannelsIdMessagesId(Method::Get, 123),
Path::try_from((Method::Get, "/channels/123/messages/456"))?,
);
Ok(())
}
assert_impl_all!(Method: Clone, Copy, Debug, Eq, PartialEq);
#[test]
fn method_conversions() {
assert_eq!(HttpMethod::DELETE, Method::Delete.to_http());
assert_eq!(HttpMethod::GET, Method::Get.to_http());
assert_eq!(HttpMethod::PATCH, Method::Patch.to_http());
assert_eq!(HttpMethod::POST, Method::Post.to_http());
assert_eq!(HttpMethod::PUT, Method::Put.to_http());
}
}