#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Limits {
pub max_units: Option<u64>,
pub max_spend_micros: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Usage {
pub units: u64,
pub spend_micros: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitDecision {
Allowed(Usage),
Rejected(LimitReason),
}
impl LimitDecision {
#[must_use]
pub const fn is_allowed(self) -> bool {
matches!(self, Self::Allowed(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitReason {
QuotaExceeded,
SpendCapExceeded,
ArithmeticOverflow,
}
#[must_use]
pub fn assess_limits(
current: Usage,
requested_units: u64,
unit_price_micros: u64,
limits: Limits,
) -> LimitDecision {
let Some(units) = current.units.checked_add(requested_units) else {
return LimitDecision::Rejected(LimitReason::ArithmeticOverflow);
};
if limits.max_units.is_some_and(|limit| units > limit) {
return LimitDecision::Rejected(LimitReason::QuotaExceeded);
}
let Some(incremental_spend) = requested_units.checked_mul(unit_price_micros) else {
return LimitDecision::Rejected(LimitReason::ArithmeticOverflow);
};
let Some(spend_micros) = current.spend_micros.checked_add(incremental_spend) else {
return LimitDecision::Rejected(LimitReason::ArithmeticOverflow);
};
if limits
.max_spend_micros
.is_some_and(|limit| spend_micros > limit)
{
return LimitDecision::Rejected(LimitReason::SpendCapExceeded);
}
LimitDecision::Allowed(Usage {
units,
spend_micros,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_boundaries_are_allowed() {
let decision = assess_limits(
Usage {
units: 8,
spend_micros: 800,
},
2,
100,
Limits {
max_units: Some(10),
max_spend_micros: Some(1_000),
},
);
assert_eq!(
decision,
LimitDecision::Allowed(Usage {
units: 10,
spend_micros: 1_000
})
);
}
#[test]
fn quota_is_checked_before_spend() {
let decision = assess_limits(
Usage::default(),
11,
1_000,
Limits {
max_units: Some(10),
max_spend_micros: Some(1),
},
);
assert_eq!(
decision,
LimitDecision::Rejected(LimitReason::QuotaExceeded)
);
}
#[test]
fn spend_cap_rejects_the_first_unit_over() {
let decision = assess_limits(
Usage {
units: 2,
spend_micros: 200,
},
1,
101,
Limits {
max_units: None,
max_spend_micros: Some(300),
},
);
assert_eq!(
decision,
LimitDecision::Rejected(LimitReason::SpendCapExceeded)
);
}
#[test]
fn arithmetic_overflow_never_wraps_into_an_allowance() {
assert_eq!(
assess_limits(Usage::default(), u64::MAX, 2, Limits::default()),
LimitDecision::Rejected(LimitReason::ArithmeticOverflow)
);
assert_eq!(
assess_limits(
Usage {
units: u64::MAX,
spend_micros: 0
},
1,
0,
Limits::default()
),
LimitDecision::Rejected(LimitReason::ArithmeticOverflow)
);
}
}