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