route_ratelimit/
types.rs

1//! Core types for rate limit configuration.
2
3use http::Method;
4use reqwest::Request;
5use std::time::Duration;
6
7/// Behavior when a rate limit is exceeded.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ThrottleBehavior {
10    /// Delay the request until the rate limit window allows it.
11    #[default]
12    Delay,
13    /// Return an error immediately.
14    Error,
15}
16
17/// A single rate limit configuration.
18#[derive(Debug, Clone)]
19pub struct RateLimit {
20    /// Maximum number of requests allowed in the window.
21    pub requests: u32,
22    /// Time window for the rate limit.
23    pub window: Duration,
24}
25
26impl RateLimit {
27    /// Create a new rate limit.
28    ///
29    /// # Panics
30    ///
31    /// Panics if:
32    /// - `requests` is 0
33    /// - `window` is zero
34    /// - `window` exceeds `u64::MAX` nanoseconds (~585 years)
35    pub fn new(requests: u32, window: Duration) -> Self {
36        assert!(requests > 0, "requests must be greater than 0");
37        assert!(!window.is_zero(), "window must be greater than 0");
38        assert!(
39            window.as_nanos() <= u64::MAX as u128,
40            "window must not exceed u64::MAX nanoseconds (~585 years)"
41        );
42        Self { requests, window }
43    }
44
45    /// Calculate the emission interval (time between requests).
46    #[inline]
47    pub(crate) fn emission_interval(&self) -> Duration {
48        self.window / self.requests
49    }
50}
51
52/// A route definition that matches requests and applies rate limits.
53#[derive(Debug, Clone)]
54pub struct Route {
55    /// Optional host to match (e.g., "api.example.com").
56    pub host: Option<String>,
57    /// Optional HTTP method to match.
58    pub method: Option<Method>,
59    /// Path prefix to match (e.g., "/order"). Empty matches all paths.
60    pub path_prefix: String,
61    /// Rate limits to apply (all must pass).
62    pub limits: Vec<RateLimit>,
63    /// Behavior when rate limit is exceeded.
64    pub on_limit: ThrottleBehavior,
65}
66
67impl Route {
68    /// Returns `true` if this route has no filters (matches all requests).
69    ///
70    /// A catch-all route has no host, no method, and no path prefix constraints.
71    #[cfg(feature = "tracing")]
72    #[inline]
73    pub(crate) fn is_catch_all(&self) -> bool {
74        self.host.is_none() && self.method.is_none() && self.path_prefix.is_empty()
75    }
76
77    /// Check if this route matches a request.
78    #[inline]
79    pub(crate) fn matches(&self, req: &Request) -> bool {
80        // Check host
81        if let Some(ref host) = self.host {
82            if let Some(req_host) = req.url().host_str() {
83                if req_host != host {
84                    return false;
85                }
86            } else {
87                return false;
88            }
89        }
90
91        // Check method
92        if let Some(ref method) = self.method {
93            if req.method() != method {
94                return false;
95            }
96        }
97
98        // Check path prefix
99        // Path prefix matching uses path segment boundaries:
100        // - "/order" matches "/order", "/order/", "/order/123"
101        // - "/order" does NOT match "/orders" or "/order-test"
102        if !self.path_prefix.is_empty() {
103            let path = req.url().path();
104            if !path.starts_with(&self.path_prefix) {
105                return false;
106            }
107            // Ensure we're matching at a path segment boundary
108            let remaining = &path[self.path_prefix.len()..];
109            if !remaining.is_empty() && !remaining.starts_with('/') {
110                return false;
111            }
112        }
113
114        true
115    }
116}
117
118/// Unique key for a route's rate limit state.
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub(crate) struct RouteKey {
121    pub route_index: usize,
122    pub limit_index: usize,
123}