1use std::{
12 error::Error,
13 fmt::{Display, Formatter, Result as FmtResult},
14 str::FromStr,
15};
16
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21#[non_exhaustive]
22pub enum Method {
23 Delete,
25 Get,
27 Patch,
29 Post,
31 Put,
33}
34
35impl Method {
36 pub const fn name(self) -> &'static str {
38 match self {
39 Method::Delete => "DELETE",
40 Method::Get => "GET",
41 Method::Patch => "PATCH",
42 Method::Post => "POST",
43 Method::Put => "PUT",
44 }
45 }
46}
47
48#[derive(Debug)]
50pub struct PathParseError {
51 kind: PathParseErrorType,
53 source: Option<Box<dyn Error + Send + Sync>>,
55}
56
57impl PathParseError {
58 #[must_use = "retrieving the type has no effect if left unused"]
60 pub const fn kind(&self) -> &PathParseErrorType {
61 &self.kind
62 }
63
64 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
66 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
67 self.source
68 }
69
70 #[must_use = "consuming the error into its parts has no effect if left unused"]
72 pub fn into_parts(self) -> (PathParseErrorType, Option<Box<dyn Error + Send + Sync>>) {
73 (self.kind, self.source)
74 }
75}
76
77impl Display for PathParseError {
78 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
79 match &self.kind {
80 PathParseErrorType::IntegerParsing { .. } => {
81 f.write_str("An ID in a segment was invalid")
82 }
83 PathParseErrorType::MessageIdWithoutMethod { .. } => {
84 f.write_str("A message path was detected but the method wasn't given")
85 }
86 PathParseErrorType::NoMatch => f.write_str("There was no matched path"),
87 }
88 }
89}
90
91impl Error for PathParseError {
92 fn source(&self) -> Option<&(dyn Error + 'static)> {
93 self.source
94 .as_ref()
95 .map(|source| &**source as &(dyn Error + 'static))
96 }
97}
98
99#[derive(Debug)]
101#[non_exhaustive]
102pub enum PathParseErrorType {
103 IntegerParsing,
105 MessageIdWithoutMethod {
108 channel_id: u64,
110 },
111 NoMatch,
113}
114
115#[derive(Clone, Debug, Eq, Hash, PartialEq)]
118#[non_exhaustive]
119pub enum Path {
120 ApplicationCommand(u64),
122 ApplicationCommandId(u64),
124 ApplicationEmojis(u64),
126 ApplicationEmoji(u64),
128 ApplicationGuildCommand(u64),
130 ApplicationGuildCommandId(u64),
132 ApplicationsMe,
134 ChannelsId(u64),
136 ChannelsIdFollowers(u64),
138 ChannelsIdInvites(u64),
140 ChannelsIdMessages(u64),
142 ChannelsIdMessagesBulkDelete(u64),
144 ChannelsIdMessagesId(Method, u64),
146 ChannelsIdMessagesIdCrosspost(u64),
148 ChannelsIdMessagesIdReactions(u64),
150 ChannelsIdMessagesIdReactionsUserIdType(u64),
153 ChannelsIdMessagesIdThreads(u64),
155 ChannelsIdPermissionsOverwriteId(u64),
157 ChannelsIdPins(u64),
159 ChannelsIdPinsMessageId(u64),
161 ChannelsIdPolls(u64),
163 ChannelsIdRecipients(u64),
165 ChannelsIdThreadMembers(u64),
167 ChannelsIdThreadMembersId(u64),
169 ChannelsIdThreads(u64),
171 ChannelsIdTyping(u64),
173 ChannelsIdWebhooks(u64),
175 ApplicationIdEntitlements(u64),
177 ApplicationIdSKUs(u64),
179 Gateway,
181 GatewayBot,
183 Guilds,
185 GuildsId(u64),
187 GuildsIdAuditLogs(u64),
189 GuildsIdAutoModerationRules(u64),
191 GuildsIdAutoModerationRulesId(u64),
193 GuildsIdBans(u64),
195 GuildsIdBansId(u64),
197 GuildsIdBansUserId(u64),
199 GuildsIdChannels(u64),
201 GuildsIdEmojis(u64),
203 GuildsIdEmojisId(u64),
205 GuildsIdIntegrations(u64),
207 GuildsIdIntegrationsId(u64),
209 GuildsIdIntegrationsIdSync(u64),
211 GuildsIdInvites(u64),
213 GuildsIdMembers(u64),
215 GuildsIdMembersId(u64),
217 GuildsIdMembersIdRolesId(u64),
219 GuildsIdMembersMeNick(u64),
221 GuildsIdMembersSearch(u64),
223 GuildsIdMfa(u64),
225 GuildsIdOnboarding(u64),
227 GuildsIdPreview(u64),
229 GuildsIdPrune(u64),
231 GuildsIdRegions(u64),
233 GuildsIdRoles(u64),
235 GuildsIdRolesId(u64),
237 GuildsIdScheduledEvents(u64),
239 GuildsIdScheduledEventsId(u64),
241 GuildsIdScheduledEventsIdUsers(u64),
243 GuildsIdStickers(u64),
245 GuildsIdTemplates(u64),
247 GuildsIdTemplatesCode(u64, String),
249 GuildsIdThreads(u64),
251 GuildsIdVanityUrl(u64),
253 GuildsIdVoiceStates(u64),
255 GuildsIdWebhooks(u64),
257 GuildsIdWelcomeScreen(u64),
259 GuildsIdWidget(u64),
261 GuildsIdWidgetJson(u64),
263 GuildsTemplatesCode(String),
265 InteractionCallback(u64),
269 InvitesCode,
271 OauthApplicationsMe,
273 OauthMe,
275 StageInstances,
277 StickerPacks,
279 Stickers,
281 UsersId,
283 UsersIdChannels,
285 UsersIdConnections,
287 UsersIdGuilds,
289 UsersIdGuildsId,
291 UsersIdGuildsIdMember,
293 VoiceRegions,
295 WebhooksId(u64),
297 WebhooksIdToken(u64, String),
302 WebhooksIdTokenMessagesId(u64, String),
307}
308
309impl FromStr for Path {
310 type Err = PathParseError;
311
312 #[allow(clippy::enum_glob_use, clippy::too_many_lines)]
331 fn from_str(s: &str) -> Result<Self, Self::Err> {
332 use Path::*;
333
334 fn parse_id(id: &str) -> Result<u64, PathParseError> {
336 id.parse().map_err(|source| PathParseError {
337 kind: PathParseErrorType::IntegerParsing,
338 source: Some(Box::new(source)),
339 })
340 }
341
342 let skip = usize::from(s.starts_with('/'));
343
344 let parts = s.split('/').skip(skip).collect::<Vec<&str>>();
345
346 Ok(match parts[..] {
347 ["applications", "@me"] => ApplicationsMe,
348 ["applications", id, "commands"] => ApplicationCommand(parse_id(id)?),
349 ["applications", id, "commands", _] => ApplicationCommandId(parse_id(id)?),
350 ["applications", id, "entitlements"] => ApplicationIdEntitlements(parse_id(id)?),
351 ["applications", id, "emojis"] => ApplicationEmojis(parse_id(id)?),
352 ["applications", id, "guilds", _, "commands"]
353 | ["applications", id, "guilds", _, "commands", "permissions"] => {
354 ApplicationGuildCommand(parse_id(id)?)
355 }
356 ["applications", id, "guilds", _, "commands", _]
357 | ["applications", id, "guilds", _, "commands", _, "permissions"] => {
358 ApplicationGuildCommandId(parse_id(id)?)
359 }
360 ["applications", id, "skus"] => ApplicationIdSKUs(parse_id(id)?),
361 ["channels", id] => ChannelsId(parse_id(id)?),
362 ["channels", id, "followers"] => ChannelsIdFollowers(parse_id(id)?),
363 ["channels", id, "invites"] => ChannelsIdInvites(parse_id(id)?),
364 ["channels", id, "messages"] => ChannelsIdMessages(parse_id(id)?),
365 ["channels", id, "messages", "bulk-delete"] => {
366 ChannelsIdMessagesBulkDelete(parse_id(id)?)
367 }
368 ["channels", id, "messages", _] => {
369 return Err(PathParseError {
371 kind: PathParseErrorType::MessageIdWithoutMethod {
372 channel_id: parse_id(id)?,
373 },
374 source: None,
375 });
376 }
377 ["channels", id, "messages", _, "crosspost"] => {
378 ChannelsIdMessagesIdCrosspost(parse_id(id)?)
379 }
380 ["channels", id, "messages", _, "reactions"]
381 | ["channels", id, "messages", _, "reactions", _] => {
382 ChannelsIdMessagesIdReactions(parse_id(id)?)
383 }
384 ["channels", id, "messages", _, "reactions", _, _] => {
385 ChannelsIdMessagesIdReactionsUserIdType(parse_id(id)?)
386 }
387 ["channels", id, "messages", _, "threads"] => {
388 ChannelsIdMessagesIdThreads(parse_id(id)?)
389 }
390 ["channels", id, "permissions", _] => ChannelsIdPermissionsOverwriteId(parse_id(id)?),
391 ["channels", id, "pins"] => ChannelsIdPins(parse_id(id)?),
392 ["channels", id, "pins", _] => ChannelsIdPinsMessageId(parse_id(id)?),
393 ["channels", id, "recipients"] | ["channels", id, "recipients", _] => {
394 ChannelsIdRecipients(parse_id(id)?)
395 }
396 ["channels", id, "thread-members"] => ChannelsIdThreadMembers(parse_id(id)?),
397 ["channels", id, "thread-members", _] => ChannelsIdThreadMembersId(parse_id(id)?),
398 ["channels", id, "threads"] => ChannelsIdThreads(parse_id(id)?),
399 ["channels", id, "typing"] => ChannelsIdTyping(parse_id(id)?),
400 ["channels", id, "webhooks"] | ["channels", id, "webhooks", _] => {
401 ChannelsIdWebhooks(parse_id(id)?)
402 }
403 ["gateway"] => Gateway,
404 ["gateway", "bot"] => GatewayBot,
405 ["guilds"] => Guilds,
406 ["guilds", "templates", code] => GuildsTemplatesCode(code.to_string()),
407 ["guilds", id] => GuildsId(parse_id(id)?),
408 ["guilds", id, "audit-logs"] => GuildsIdAuditLogs(parse_id(id)?),
409 ["guilds", id, "auto-moderation", "rules"] => {
410 GuildsIdAutoModerationRules(parse_id(id)?)
411 }
412 ["guilds", id, "auto-moderation", "rules", _] => {
413 GuildsIdAutoModerationRulesId(parse_id(id)?)
414 }
415 ["guilds", id, "bans"] => GuildsIdBans(parse_id(id)?),
416 ["guilds", id, "bans", _] => GuildsIdBansUserId(parse_id(id)?),
417 ["guilds", id, "channels"] => GuildsIdChannels(parse_id(id)?),
418 ["guilds", id, "emojis"] => GuildsIdEmojis(parse_id(id)?),
419 ["guilds", id, "emojis", _] => GuildsIdEmojisId(parse_id(id)?),
420 ["guilds", id, "integrations"] => GuildsIdIntegrations(parse_id(id)?),
421 ["guilds", id, "integrations", _] => GuildsIdIntegrationsId(parse_id(id)?),
422 ["guilds", id, "integrations", _, "sync"] => GuildsIdIntegrationsIdSync(parse_id(id)?),
423 ["guilds", id, "invites"] => GuildsIdInvites(parse_id(id)?),
424 ["guilds", id, "members"] => GuildsIdMembers(parse_id(id)?),
425 ["guilds", id, "members", "search"] => GuildsIdMembersSearch(parse_id(id)?),
426 ["guilds", id, "members", _] => GuildsIdMembersId(parse_id(id)?),
427 ["guilds", id, "members", _, "roles", _] => GuildsIdMembersIdRolesId(parse_id(id)?),
428 ["guilds", id, "members", "@me", "nick"] => GuildsIdMembersMeNick(parse_id(id)?),
429 ["guilds", id, "onboarding"] => GuildsIdOnboarding(parse_id(id)?),
430 ["guilds", id, "preview"] => GuildsIdPreview(parse_id(id)?),
431 ["guilds", id, "prune"] => GuildsIdPrune(parse_id(id)?),
432 ["guilds", id, "regions"] => GuildsIdRegions(parse_id(id)?),
433 ["guilds", id, "roles"] => GuildsIdRoles(parse_id(id)?),
434 ["guilds", id, "roles", _] => GuildsIdRolesId(parse_id(id)?),
435 ["guilds", id, "scheduled-events"] => GuildsIdScheduledEvents(parse_id(id)?),
436 ["guilds", id, "scheduled-events", _] => GuildsIdScheduledEventsId(parse_id(id)?),
437 ["guilds", id, "scheduled-events", _, "users"] => {
438 GuildsIdScheduledEventsIdUsers(parse_id(id)?)
439 }
440 ["guilds", id, "stickers"] | ["guilds", id, "stickers", _] => {
441 GuildsIdStickers(parse_id(id)?)
442 }
443 ["guilds", id, "templates"] => GuildsIdTemplates(parse_id(id)?),
444 ["guilds", id, "templates", code] => {
445 GuildsIdTemplatesCode(parse_id(id)?, code.to_string())
446 }
447 ["guilds", id, "threads", _] => GuildsIdThreads(parse_id(id)?),
448 ["guilds", id, "vanity-url"] => GuildsIdVanityUrl(parse_id(id)?),
449 ["guilds", id, "voice-states", _] => GuildsIdVoiceStates(parse_id(id)?),
450 ["guilds", id, "welcome-screen"] => GuildsIdWelcomeScreen(parse_id(id)?),
451 ["guilds", id, "webhooks"] => GuildsIdWebhooks(parse_id(id)?),
452 ["guilds", id, "widget"] => GuildsIdWidget(parse_id(id)?),
453 ["guilds", id, "widget.json"] => GuildsIdWidgetJson(parse_id(id)?),
454 ["invites", _] => InvitesCode,
455 ["interactions", id, _, "callback"] => InteractionCallback(parse_id(id)?),
456 ["stage-instances", _] => StageInstances,
457 ["sticker-packs"] => StickerPacks,
458 ["stickers", _] => Stickers,
459 ["oauth2", "applications", "@me"] => OauthApplicationsMe,
460 ["oauth2", "@me"] => OauthMe,
461 ["users", _] => UsersId,
462 ["users", _, "connections"] => UsersIdConnections,
463 ["users", _, "channels"] => UsersIdChannels,
464 ["users", _, "guilds"] => UsersIdGuilds,
465 ["users", _, "guilds", _] => UsersIdGuildsId,
466 ["users", _, "guilds", _, "member"] => UsersIdGuildsIdMember,
467 ["voice", "regions"] => VoiceRegions,
468 ["webhooks", id] => WebhooksId(parse_id(id)?),
469 ["webhooks", id, token] => WebhooksIdToken(parse_id(id)?, token.to_string()),
470 ["webhooks", id, token, "messages", _] => {
471 WebhooksIdTokenMessagesId(parse_id(id)?, token.to_string())
472 }
473 _ => {
474 return Err(PathParseError {
475 kind: PathParseErrorType::NoMatch,
476 source: None,
477 })
478 }
479 })
480 }
481}
482
483impl TryFrom<(Method, &str)> for Path {
484 type Error = PathParseError;
485
486 fn try_from((method, s): (Method, &str)) -> Result<Self, Self::Error> {
487 match Self::from_str(s) {
488 Ok(v) => Ok(v),
489 Err(why) => {
490 if let PathParseErrorType::MessageIdWithoutMethod { channel_id } = why.kind() {
491 Ok(Self::ChannelsIdMessagesId(method, *channel_id))
492 } else {
493 Err(why)
494 }
495 }
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::{Path, PathParseError, PathParseErrorType};
503 use crate::request::Method;
504 use static_assertions::{assert_fields, assert_impl_all};
505 use std::{error::Error, fmt::Debug, hash::Hash, str::FromStr};
506
507 assert_fields!(PathParseErrorType::MessageIdWithoutMethod: channel_id);
508 assert_impl_all!(PathParseErrorType: Debug, Send, Sync);
509 assert_impl_all!(PathParseError: Error, Send, Sync);
510 assert_impl_all!(Path: Clone, Debug, Eq, Hash, PartialEq, Send, Sync);
511
512 #[test]
513 fn prefix_unimportant() -> Result<(), Box<dyn Error>> {
514 assert_eq!(Path::Guilds, Path::from_str("guilds")?);
515 assert_eq!(Path::Guilds, Path::from_str("/guilds")?);
516
517 Ok(())
518 }
519
520 #[test]
521 fn from_str() -> Result<(), Box<dyn Error>> {
522 assert_eq!(Path::ChannelsId(123), Path::from_str("/channels/123")?);
523 assert_eq!(Path::WebhooksId(123), Path::from_str("/webhooks/123")?);
524 assert_eq!(Path::InvitesCode, Path::from_str("/invites/abc")?);
525
526 Ok(())
527 }
528
529 #[test]
530 fn message_id() -> Result<(), Box<dyn Error>> {
531 assert!(matches!(
532 Path::from_str("channels/123/messages/456")
533 .unwrap_err()
534 .kind(),
535 PathParseErrorType::MessageIdWithoutMethod { channel_id: 123 },
536 ));
537 assert_eq!(
538 Path::ChannelsIdMessagesId(Method::Get, 123),
539 Path::try_from((Method::Get, "/channels/123/messages/456"))?,
540 );
541
542 Ok(())
543 }
544
545 assert_impl_all!(Method: Clone, Copy, Debug, Eq, PartialEq);
546
547 #[test]
548 fn method_conversions() {
549 assert_eq!("DELETE", Method::Delete.name());
550 assert_eq!("GET", Method::Get.name());
551 assert_eq!("PATCH", Method::Patch.name());
552 assert_eq!("POST", Method::Post.name());
553 assert_eq!("PUT", Method::Put.name());
554 }
555}