use std::future::Future;
use std::time::Duration;
use crate::store::error::StoreError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RateLimitKey(String);
impl RateLimitKey {
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self(key.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn fingerprint(&self) -> &str {
let end = self
.0
.char_indices()
.nth(8)
.map(|(i, _)| i)
.unwrap_or(self.0.len());
&self.0[..end]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RateLimitUnavailable {
#[default]
FailOpen,
FailClosed,
SoftDenyAfterThreshold(u32),
}
#[derive(Debug, Clone)]
pub struct RateLimitPolicy {
pub max_failures: u32,
pub window: Duration,
pub unavailable: RateLimitUnavailable,
}
impl RateLimitPolicy {
#[must_use]
pub fn default_invite() -> Self {
Self {
max_failures: 10,
window: Duration::from_secs(5 * 60),
unavailable: RateLimitUnavailable::FailOpen,
}
}
#[must_use]
pub fn is_exceeded(&self, failures: u32) -> bool {
failures >= self.max_failures
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitOutcome {
Allow,
Deny,
}
pub trait RateLimitStore {
fn check(
&self,
key: &RateLimitKey,
policy: &RateLimitPolicy,
) -> impl Future<Output = Result<RateLimitOutcome, StoreError>>;
fn record_failure(
&self,
key: &RateLimitKey,
policy: &RateLimitPolicy,
) -> impl Future<Output = Result<(), StoreError>>;
fn clear_failures(&self, key: &RateLimitKey) -> impl Future<Output = Result<(), StoreError>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_thresholds() {
let p = RateLimitPolicy::default_invite();
assert_eq!(p.max_failures, 10);
assert!(!p.is_exceeded(9));
assert!(p.is_exceeded(10));
assert!(p.is_exceeded(11));
}
#[test]
fn fingerprint_is_prefix_not_full_key() {
let k = RateLimitKey::new("abcdefghijklmnop");
assert_eq!(k.fingerprint(), "abcdefgh");
let short = RateLimitKey::new("ab");
assert_eq!(short.fingerprint(), "ab");
}
#[test]
fn key_roundtrips() {
let k = RateLimitKey::new("test-key");
assert_eq!(k.as_str(), "test-key");
}
}