use crate::decision::Decision;
use crate::quota::Quota;
pub trait Policy: Send + Sync + 'static {
fn token_cost(&self, _quota: &Quota) -> u64 {
1
}
fn on_response(&self, _status_code: u16, _decision: &Decision) -> i64 {
0
}
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Default)]
pub struct DefaultPolicy;
impl DefaultPolicy {
pub fn new() -> Self {
Self
}
}
impl Policy for DefaultPolicy {
fn name(&self) -> &'static str {
"default"
}
}
#[derive(Debug, Clone)]
pub struct PenaltyPolicy {
pub client_error_multiplier: u64,
pub server_error_multiplier: u64,
}
impl PenaltyPolicy {
pub fn new(multiplier: u64) -> Self {
Self {
client_error_multiplier: multiplier,
server_error_multiplier: multiplier,
}
}
pub fn with_multipliers(client_error: u64, server_error: u64) -> Self {
Self {
client_error_multiplier: client_error,
server_error_multiplier: server_error,
}
}
}
impl Default for PenaltyPolicy {
fn default() -> Self {
Self::new(2)
}
}
impl Policy for PenaltyPolicy {
fn on_response(&self, status_code: u16, _decision: &Decision) -> i64 {
match status_code {
400..=499 => -((self.client_error_multiplier - 1) as i64),
500..=599 => -((self.server_error_multiplier - 1) as i64),
_ => 0,
}
}
fn name(&self) -> &'static str {
"penalty"
}
}
#[derive(Debug, Clone)]
pub struct CreditPolicy {
pub refund_not_modified: bool,
pub refund_no_content: bool,
}
impl CreditPolicy {
pub fn new() -> Self {
Self {
refund_not_modified: true,
refund_no_content: false,
}
}
pub fn with_no_content(mut self) -> Self {
self.refund_no_content = true;
self
}
}
impl Default for CreditPolicy {
fn default() -> Self {
Self::new()
}
}
impl Policy for CreditPolicy {
fn on_response(&self, status_code: u16, _decision: &Decision) -> i64 {
if status_code == 304 && self.refund_not_modified {
return 1;
}
if status_code == 204 && self.refund_no_content {
return 1;
}
0
}
fn name(&self) -> &'static str {
"credit"
}
}
#[derive(Default)]
pub struct CompositePolicy {
policies: Vec<Box<dyn Policy>>,
}
impl CompositePolicy {
pub fn new() -> Self {
Self {
policies: Vec::new(),
}
}
pub fn with<P: Policy>(mut self, policy: P) -> Self {
self.policies.push(Box::new(policy));
self
}
}
impl Policy for CompositePolicy {
fn token_cost(&self, quota: &Quota) -> u64 {
self.policies
.iter()
.map(|p| p.token_cost(quota))
.max()
.unwrap_or(1)
}
fn on_response(&self, status_code: u16, decision: &Decision) -> i64 {
self.policies
.iter()
.map(|p| p.on_response(status_code, decision))
.sum()
}
fn name(&self) -> &'static str {
"composite"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy() {
let policy = DefaultPolicy::new();
let quota = Quota::per_minute(100);
assert_eq!(policy.token_cost("a), 1);
assert_eq!(policy.name(), "default");
}
#[test]
fn test_penalty_policy() {
let policy = PenaltyPolicy::new(3);
let _quota = Quota::per_minute(100);
let decision = crate::decision::Decision::allowed(
crate::decision::RateLimitInfo::new(100, 99, std::time::Instant::now(), std::time::Instant::now()),
);
assert_eq!(policy.on_response(200, &decision), 0);
assert_eq!(policy.on_response(404, &decision), -2);
assert_eq!(policy.on_response(500, &decision), -2);
}
#[test]
fn test_credit_policy() {
let policy = CreditPolicy::new().with_no_content();
let _quota = Quota::per_minute(100);
let decision = crate::decision::Decision::allowed(
crate::decision::RateLimitInfo::new(100, 99, std::time::Instant::now(), std::time::Instant::now()),
);
assert_eq!(policy.on_response(304, &decision), 1);
assert_eq!(policy.on_response(204, &decision), 1);
assert_eq!(policy.on_response(200, &decision), 0);
}
#[test]
fn test_composite_policy() {
let policy = CompositePolicy::new()
.with(PenaltyPolicy::new(2))
.with(CreditPolicy::new());
let decision = crate::decision::Decision::allowed(
crate::decision::RateLimitInfo::new(100, 99, std::time::Instant::now(), std::time::Instant::now()),
);
assert_eq!(policy.on_response(404, &decision), -1); assert_eq!(policy.on_response(304, &decision), 1); }
}