twilight_http_ratelimiting/
request.rs

1//! Request parameters for ratelimiting.
2//!
3//! This module contains the type definitions for parameters
4//! relevant for ratelimiting.
5//!
6//! The [`Ratelimiter`] uses [`Path`]s and [`Method`]s to store and associate
7//! buckets with endpoints.
8//!
9//! [`Ratelimiter`]: super::Ratelimiter
10
11use std::{
12    error::Error,
13    fmt::{Display, Formatter, Result as FmtResult},
14    str::FromStr,
15};
16
17/// HTTP request [method].
18///
19/// [method]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
20#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21#[non_exhaustive]
22pub enum Method {
23    /// Delete a resource.
24    Delete,
25    /// Retrieve a resource.
26    Get,
27    /// Update a resource.
28    Patch,
29    /// Create a resource.
30    Post,
31    /// Replace a resource.
32    Put,
33}
34
35impl Method {
36    /// Name of the method.
37    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/// Error returned when a [`Path`] could not be parsed from a string.
49#[derive(Debug)]
50pub struct PathParseError {
51    /// Detailed reason why this could not be parsed.
52    kind: PathParseErrorType,
53    /// Original error leading up to this one.
54    source: Option<Box<dyn Error + Send + Sync>>,
55}
56
57impl PathParseError {
58    /// Immutable reference to the type of error that occurred.
59    #[must_use = "retrieving the type has no effect if left unused"]
60    pub const fn kind(&self) -> &PathParseErrorType {
61        &self.kind
62    }
63
64    /// Consume the error, returning the source error if there is any.
65    #[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    /// Consume the error, returning the owned error type and the source error.
71    #[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/// Type of [`PathParseError`] specifying what failed to parse.
98#[derive(Debug)]
99#[non_exhaustive]
100pub enum PathParseErrorType {
101    /// The ID couldn't be parsed as an integer.
102    IntegerParsing,
103    /// When parsing into a [`Path::ChannelsIdMessagesId`] variant, the method
104    /// must also be specified via its `TryFrom` impl.
105    MessageIdWithoutMethod {
106        /// The ID of the channel.
107        channel_id: u64,
108    },
109    /// A static path for the provided path string wasn't found.
110    NoMatch,
111}
112
113/// An enum representing a path, most useful for ratelimiting implementations.
114// If adding to this enum, be sure to add to the `TryFrom` impl.
115#[derive(Clone, Debug, Eq, Hash, PartialEq)]
116#[non_exhaustive]
117pub enum Path {
118    /// Operating on global commands.
119    ApplicationCommand(u64),
120    /// Operating on a specific command.
121    ApplicationCommandId(u64),
122    /// Operating on application emojis.
123    ApplicationEmojis(u64),
124    /// Operating on a specific application emoji.
125    ApplicationEmoji(u64),
126    /// Operating on commands in a guild.
127    ApplicationGuildCommand(u64),
128    /// Operating on a specific command in a guild.
129    ApplicationGuildCommandId(u64),
130    /// Operating on current user application,
131    ApplicationsMe,
132    /// Operating on a channel.
133    ChannelsId(u64),
134    /// Operating on a channel's followers.
135    ChannelsIdFollowers(u64),
136    /// Operating on a channel's invites.
137    ChannelsIdInvites(u64),
138    /// Operating on a channel's messages.
139    ChannelsIdMessages(u64),
140    /// Operating on a channel's messages by bulk deleting.
141    ChannelsIdMessagesBulkDelete(u64),
142    /// Operating on an individual channel's message.
143    ChannelsIdMessagesId(Method, u64),
144    /// Crossposting an individual channel's message.
145    ChannelsIdMessagesIdCrosspost(u64),
146    /// Operating on an individual channel's message's reactions.
147    ChannelsIdMessagesIdReactions(u64),
148    /// Operating on an individual channel's message's reactions while
149    /// specifying the user ID and emoji type.
150    ChannelsIdMessagesIdReactionsUserIdType(u64),
151    /// Operating on an individual channel's message's threads.
152    ChannelsIdMessagesIdThreads(u64),
153    /// Operating on a channel's permission overwrites by ID.
154    ChannelsIdPermissionsOverwriteId(u64),
155    /// Operating on a channel's pins.
156    ChannelsIdPins(u64),
157    /// Operating on a channel's individual pinned message.
158    ChannelsIdPinsMessageId(u64),
159    /// Operating on a channel's polls.
160    ChannelsIdPolls(u64),
161    /// Operating on a group DM's recipients.
162    ChannelsIdRecipients(u64),
163    /// Operating on a thread's members.
164    ChannelsIdThreadMembers(u64),
165    /// Operating on a thread's member.
166    ChannelsIdThreadMembersId(u64),
167    /// Operating on a channel's threads.
168    ChannelsIdThreads(u64),
169    /// Operating on a channel's typing indicator.
170    ChannelsIdTyping(u64),
171    /// Operating on a channel's webhooks.
172    ChannelsIdWebhooks(u64),
173    /// Operating on an application's entitlements.
174    ApplicationIdEntitlements(u64),
175    /// Operating on an application's SKUs.
176    ApplicationIdSKUs(u64),
177    /// Operating with the gateway information.
178    Gateway,
179    /// Operating with the gateway information tailored to the current user.
180    GatewayBot,
181    /// Operating on the guild resource.
182    Guilds,
183    /// Operating on one of user's guilds.
184    GuildsId(u64),
185    /// Operating on a ban from one of the user's guilds.
186    GuildsIdAuditLogs(u64),
187    /// Operating on a guild's auto moderation rules.
188    GuildsIdAutoModerationRules(u64),
189    /// Operating on an auto moderation rule from  one of the user's guilds.
190    GuildsIdAutoModerationRulesId(u64),
191    /// Operating on one of the user's guilds' bans.
192    GuildsIdBans(u64),
193    /// Operating on a ban from one of the user's guilds.
194    GuildsIdBansId(u64),
195    /// Operating on specific member's ban from one of the user's guilds.
196    GuildsIdBansUserId(u64),
197    /// Operating on one of the user's guilds' channels.
198    GuildsIdChannels(u64),
199    /// Operating on one of the user's guilds' emojis.
200    GuildsIdEmojis(u64),
201    /// Operating on an emoji from one of the user's guilds.
202    GuildsIdEmojisId(u64),
203    /// Operating on one of the user's guilds' integrations.
204    GuildsIdIntegrations(u64),
205    /// Operating on an integration from one of the user's guilds.
206    GuildsIdIntegrationsId(u64),
207    /// Operating on an integration from one of the user's guilds by synchronizing it.
208    GuildsIdIntegrationsIdSync(u64),
209    /// Operating on one of the user's guilds' invites.
210    GuildsIdInvites(u64),
211    /// Operating on one of the user's guilds' members.
212    GuildsIdMembers(u64),
213    /// Operating on a member from one of the user's guilds.
214    GuildsIdMembersId(u64),
215    /// Operating on a role of a member from one of the user's guilds.
216    GuildsIdMembersIdRolesId(u64),
217    /// Operating on the user's nickname in one of the user's guilds.
218    GuildsIdMembersMeNick(u64),
219    /// Operating on one of the user's guilds' members by searching.
220    GuildsIdMembersSearch(u64),
221    /// Operating on one of the user's guilds' MFA level.
222    GuildsIdMfa(u64),
223    /// Operating on one of the user's guilds' onboarding.
224    GuildsIdOnboarding(u64),
225    /// Operating on one of the user's guilds' by previewing it.
226    GuildsIdPreview(u64),
227    /// Operating on one of the user's guilds' by pruning members.
228    GuildsIdPrune(u64),
229    /// Operating on the voice regions of one of the user's guilds.
230    GuildsIdRegions(u64),
231    /// Operating on the roles of one of the user's guilds.
232    GuildsIdRoles(u64),
233    /// Operating on a role of one of the user's guilds.
234    GuildsIdRolesId(u64),
235    /// Operating on the guild's scheduled events.
236    GuildsIdScheduledEvents(u64),
237    /// Operating on a particular guild's scheduled events.
238    GuildsIdScheduledEventsId(u64),
239    /// Operating on a particular guild's scheduled event users.
240    GuildsIdScheduledEventsIdUsers(u64),
241    /// Operating on one of the user's guilds' stickers.
242    GuildsIdStickers(u64),
243    /// Operating on one of the user's guilds' templates.
244    GuildsIdTemplates(u64),
245    /// Operating on a template from one of the user's guilds.
246    GuildsIdTemplatesCode(u64, String),
247    /// Operating on one of the user's guilds' threads.
248    GuildsIdThreads(u64),
249    /// Operating on one of the user's guilds' vanity URL.
250    GuildsIdVanityUrl(u64),
251    /// Operating on one of the user's guilds' voice states.
252    GuildsIdVoiceStates(u64),
253    /// Operating on one of the user's guilds' webhooks.
254    GuildsIdWebhooks(u64),
255    /// Operating on one of the user's guilds' welcome screen.
256    GuildsIdWelcomeScreen(u64),
257    /// Operating on one of the user's guild's widget settings.
258    GuildsIdWidget(u64),
259    /// Operating on one of the user's guild's widget.
260    GuildsIdWidgetJson(u64),
261    /// Operating on a guild template.
262    GuildsTemplatesCode(String),
263    /// Operating on an interaction's callback.
264    ///
265    /// This path is not bound to the application's global rate limit.
266    InteractionCallback(u64),
267    /// Operating on an invite.
268    InvitesCode,
269    /// Operating on the user's application information.
270    OauthApplicationsMe,
271    /// Operating on the current authorization's information.
272    OauthMe,
273    /// Operating on stage instances.
274    StageInstances,
275    /// Operating on sticker packs.
276    StickerPacks,
277    /// Operating on a sticker.
278    Stickers,
279    /// Operating on a sticker.
280    UsersId,
281    /// Operating on the user's private channels.
282    UsersIdChannels,
283    /// Operating on the user's connections.
284    UsersIdConnections,
285    /// Operating on the state of a guild that the user is in.
286    UsersIdGuilds,
287    /// Operating on the state of a guild that the user is in.
288    UsersIdGuildsId,
289    /// Operating on the state of a guild that the user, as a member, is in.
290    UsersIdGuildsIdMember,
291    /// Operating on the voice regions available to the current user.
292    VoiceRegions,
293    /// Operating on a webhook as a bot.
294    WebhooksId(u64),
295    /// Operating on a webhook as a webhook.
296    ///
297    /// When used with interactions, this path is not bound to the application's
298    /// global rate limit.
299    WebhooksIdToken(u64, String),
300    /// Operating on a message created by a webhook.
301    ///
302    /// When used with interactions, this path is not bound to the application's
303    /// global rate limit.
304    WebhooksIdTokenMessagesId(u64, String),
305}
306
307impl FromStr for Path {
308    type Err = PathParseError;
309
310    /// Parses a string into a path.
311    ///
312    /// The string *may* start with a slash (`/`), which will be ignored.
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// use std::str::FromStr;
318    /// use twilight_http_ratelimiting::Path;
319    ///
320    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
321    /// assert_eq!(Path::VoiceRegions, Path::from_str("/voice/regions")?);
322    /// assert_eq!(
323    ///     Path::ChannelsIdMessages(123),
324    ///     Path::from_str("channels/123/messages")?,
325    /// );
326    /// # Ok(()) }
327    /// ```
328    #[allow(clippy::enum_glob_use, clippy::too_many_lines)]
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        use Path::*;
331
332        /// Parse a string into a Discord ID.
333        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                // can not map to path without method since they have different ratelimits
368                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}