skp_ratelimit/
extensions.rs1use crate::decision::Decision;
18use crate::quota::Quota;
19
20#[derive(Debug, Clone)]
24pub struct RateLimitExt {
25 pub key: String,
27 pub quota: Quota,
29 pub decision: Decision,
31 pub allowed: bool,
33 pub remaining: u64,
35 pub limit: u64,
37 pub reset_seconds: u64,
39}
40
41impl RateLimitExt {
42 pub fn new(key: impl Into<String>, quota: Quota, decision: Decision) -> Self {
44 let info = decision.info();
45 Self {
46 key: key.into(),
47 allowed: decision.is_allowed(),
48 remaining: info.remaining,
49 limit: info.limit,
50 reset_seconds: info.reset_seconds(),
51 quota,
52 decision,
53 }
54 }
55
56 pub fn is_allowed(&self) -> bool {
58 self.allowed
59 }
60
61 pub fn is_denied(&self) -> bool {
63 !self.allowed
64 }
65}
66
67#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
71pub struct RateLimitResponse {
72 pub allowed: bool,
74 pub limit: u64,
76 pub remaining: u64,
78 pub reset_in_seconds: u64,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub retry_after_seconds: Option<u64>,
83}
84
85impl From<&RateLimitExt> for RateLimitResponse {
86 fn from(ext: &RateLimitExt) -> Self {
87 Self {
88 allowed: ext.allowed,
89 limit: ext.limit,
90 remaining: ext.remaining,
91 reset_in_seconds: ext.reset_seconds,
92 retry_after_seconds: ext
93 .decision
94 .info()
95 .retry_after
96 .map(|d| d.as_secs()),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::decision::RateLimitInfo;
105 use std::time::{Duration, Instant};
106
107 #[test]
108 fn test_rate_limit_ext() {
109 let info = RateLimitInfo::new(100, 50, Instant::now() + Duration::from_secs(60), Instant::now());
110 let decision = Decision::allowed(info);
111 let quota = Quota::per_minute(100);
112
113 let ext = RateLimitExt::new("user:123", quota, decision);
114
115 assert!(ext.is_allowed());
116 assert!(!ext.is_denied());
117 assert_eq!(ext.remaining, 50);
118 assert_eq!(ext.limit, 100);
119 }
120
121 #[test]
122 fn test_rate_limit_response_serialization() {
123 let info = RateLimitInfo::new(100, 0, Instant::now() + Duration::from_secs(30), Instant::now())
124 .with_retry_after(Duration::from_secs(30));
125 let decision = Decision::denied(info);
126 let quota = Quota::per_minute(100);
127
128 let ext = RateLimitExt::new("user:123", quota, decision);
129 let response: RateLimitResponse = (&ext).into();
130
131 assert!(!response.allowed);
132 assert_eq!(response.limit, 100);
133 assert_eq!(response.remaining, 0);
134 assert!(response.retry_after_seconds.is_some());
135 }
136}