use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct QuotaStatus {
pub bytes_used_month: u64,
pub bytes_cap_month: u64,
pub bytes_used_week: u64,
pub bytes_cap_week: u64,
pub exports_this_hour: u32,
pub exports_cap_hour: u32,
pub historical_data_months: i32,
pub calls_per_minute: u32,
pub max_rows_per_request: u32,
pub vault_concurrency: u32,
}
impl QuotaStatus {
#[must_use]
pub fn bytes_remaining_month(&self) -> u64 {
self.bytes_cap_month.saturating_sub(self.bytes_used_month)
}
#[must_use]
pub fn bytes_remaining_week(&self) -> u64 {
self.bytes_cap_week.saturating_sub(self.bytes_used_week)
}
#[must_use]
pub fn exports_remaining_hour(&self) -> u32 {
self.exports_cap_hour.saturating_sub(self.exports_this_hour)
}
#[must_use]
pub fn historical_limit_months(&self) -> Option<u32> {
u32::try_from(self.historical_data_months).ok()
}
#[must_use]
pub fn is_byte_allowance_exhausted(&self) -> bool {
self.bytes_remaining_month() == 0 || self.bytes_remaining_week() == 0
}
#[must_use]
pub fn is_export_allowance_exhausted(&self) -> bool {
self.exports_remaining_hour() == 0
}
#[must_use]
pub fn is_exhausted(&self) -> bool {
self.is_byte_allowance_exhausted() || self.is_export_allowance_exhausted()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
const USAGE_RESPONSE: &str = r#"{
"bytes_used_month": 5962,
"bytes_cap_month": 53687091200,
"bytes_used_week": 5962,
"bytes_cap_week": 16106127360,
"exports_this_hour": 0,
"exports_cap_hour": 5,
"historical_data_months": -1,
"calls_per_minute": 200,
"max_rows_per_request": 5000,
"vault_concurrency": 2
}"#;
fn parsed() -> QuotaStatus {
serde_json::from_str(USAGE_RESPONSE).unwrap()
}
#[test]
fn deserializes_the_real_usage_response() {
let status = parsed();
assert_eq!(status.bytes_used_month, 5962);
assert_eq!(status.bytes_cap_month, 53_687_091_200);
assert_eq!(status.bytes_used_week, 5962);
assert_eq!(status.bytes_cap_week, 16_106_127_360);
assert_eq!(status.exports_this_hour, 0);
assert_eq!(status.exports_cap_hour, 5);
assert_eq!(status.calls_per_minute, 200);
assert_eq!(status.max_rows_per_request, 5000);
assert_eq!(status.vault_concurrency, 2);
}
#[test]
fn unknown_fields_are_ignored_rather_than_rejected() {
let with_extra = r#"{
"bytes_used_month": 1, "bytes_cap_month": 2,
"bytes_used_week": 3, "bytes_cap_week": 4,
"exports_this_hour": 5, "exports_cap_hour": 6,
"historical_data_months": 7, "calls_per_minute": 8,
"max_rows_per_request": 9, "vault_concurrency": 10,
"tokens_used_day": 11
}"#;
assert_eq!(
serde_json::from_str::<QuotaStatus>(with_extra)
.unwrap()
.bytes_used_month,
1
);
}
#[test]
fn unlimited_history_is_none_not_a_negative_count() {
assert_eq!(parsed().historical_limit_months(), None);
}
#[test]
fn a_bounded_history_limit_is_reported_as_a_count() {
let mut status = parsed();
status.historical_data_months = 24;
assert_eq!(status.historical_limit_months(), Some(24));
}
#[test]
fn remaining_saturates_instead_of_underflowing() {
let mut status = parsed();
status.bytes_used_month = status.bytes_cap_month + 1;
status.bytes_used_week = status.bytes_cap_week + 1;
status.exports_this_hour = status.exports_cap_hour + 1;
assert_eq!(status.bytes_remaining_month(), 0);
assert_eq!(status.bytes_remaining_week(), 0);
assert_eq!(status.exports_remaining_hour(), 0);
assert!(status.is_exhausted());
}
#[test]
fn a_fresh_allowance_is_not_exhausted() {
let status = parsed();
assert_eq!(status.bytes_remaining_month(), 53_687_091_200 - 5962);
assert_eq!(status.exports_remaining_hour(), 5);
assert!(!status.is_exhausted());
}
#[test]
fn static_limits_alone_never_mark_the_allowance_exhausted() {
let mut status = parsed();
status.calls_per_minute = 0;
status.max_rows_per_request = 0;
status.vault_concurrency = 0;
assert!(!status.is_exhausted());
}
}