use std::future::Future;
use std::pin::Pin;
#[derive(Debug, thiserror::Error)]
pub enum ForgeError {
#[error("Rate limit exceeded: {limit} per {window_seconds}s")]
RateLimited {
limit: u64,
window_seconds: u64,
},
#[error("Identifier banned: {reason}")]
Banned {
reason: String,
},
#[error("Rate limiter internal error: {message}")]
Internal {
message: String,
},
}
impl ForgeError {
#[must_use]
pub fn internal(error: impl std::fmt::Display) -> Self {
Self::Internal {
message: error.to_string(),
}
}
}
pub trait ForgeRateLimiter: Send + Sync {
#[allow(
clippy::type_complexity,
reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type; mirrors AsyncAutoBuilder::build"
)]
fn check<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<bool, ForgeError>> + Send + 'a>>;
#[allow(
clippy::type_complexity,
reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type; mirrors AsyncAutoBuilder::build"
)]
fn record<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), ForgeError>> + Send + 'a>>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
struct MockForgeRateLimiter {
allow: bool,
}
impl ForgeRateLimiter for MockForgeRateLimiter {
fn check<'a>(
&'a self,
_key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<bool, ForgeError>> + Send + 'a>> {
Box::pin(async move { Ok(self.allow) })
}
fn record<'a>(
&'a self,
_key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), ForgeError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
}
#[tokio::test]
async fn forge_rate_limiter_check_returns_pin_box_future() {
let limiter = MockForgeRateLimiter { allow: true };
let result: Result<bool, ForgeError> = limiter.check("key").await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn forge_rate_limiter_record_returns_pin_box_future() {
let limiter = MockForgeRateLimiter { allow: true };
let result: Result<(), ForgeError> = limiter.record("key").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn forge_rate_limiter_is_object_safe() {
let limiter: Arc<dyn ForgeRateLimiter + Send + Sync> =
Arc::new(MockForgeRateLimiter { allow: false });
let allowed: bool = limiter.check("k").await.unwrap();
assert!(!allowed);
}
#[test]
fn forge_rate_limiter_requires_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MockForgeRateLimiter>();
}
#[test]
fn forge_error_implements_std_error() {
fn assert_error<E: std::error::Error + Send + 'static>() {}
assert_error::<ForgeError>();
}
#[test]
fn forge_error_display() {
let err = ForgeError::RateLimited {
limit: 100,
window_seconds: 60,
};
let msg = err.to_string();
assert!(msg.contains("100"));
assert!(msg.contains("60"));
}
}