use serde::{Deserialize as SerdeDeserialize, Serialize};
const API_EXHAUSTED_STATUS: &str = "api_exhausted";
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
pub struct CacheCollectionCompleteness {
pub status: String,
pub page_size: u32,
pub page_count: u32,
pub row_count: usize,
pub point_in_time_guaranteed: bool,
}
impl CacheCollectionCompleteness {
#[must_use]
pub fn api_exhausted(
page_size: u32,
page_count: u32,
row_count: usize,
point_in_time_guaranteed: bool,
) -> Self {
Self {
status: API_EXHAUSTED_STATUS.to_string(),
page_size,
page_count,
row_count,
point_in_time_guaranteed,
}
}
#[must_use]
pub fn is_api_exhausted(&self) -> bool {
self.status == API_EXHAUSTED_STATUS
}
}
pub fn validate_cache_collection_completeness(
completeness: &CacheCollectionCompleteness,
actual_row_count: usize,
) -> Result<(), String> {
if !completeness.is_api_exhausted() {
return Err(format!(
"completeness status is {}, expected {API_EXHAUSTED_STATUS}",
completeness.status
));
}
if completeness.page_size == 0 {
return Err("completeness page_size must be greater than zero".to_string());
}
if completeness.page_count == 0 {
return Err("completeness page_count must be greater than zero".to_string());
}
if completeness.row_count != actual_row_count {
return Err(format!(
"completeness row_count is {}, actual row count is {actual_row_count}",
completeness.row_count
));
}
let maximum_row_count = u64::from(completeness.page_size) * u64::from(completeness.page_count);
if actual_row_count as u128 > u128::from(maximum_row_count) {
return Err(format!(
"completeness row_count is {actual_row_count}, maximum for {} pages of size {} is {maximum_row_count}",
completeness.page_count, completeness.page_size
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completeness_requires_api_exhaustion_and_consistent_page_evidence() {
for page_size in 0..=2 {
for page_count in 0..=2 {
for declared_row_count in 0..=2 {
for actual_row_count in 0..=2 {
let completeness = CacheCollectionCompleteness::api_exhausted(
page_size,
page_count,
declared_row_count,
false,
);
let expected_valid = page_size > 0
&& page_count > 0
&& declared_row_count == actual_row_count
&& actual_row_count <= (page_size * page_count) as usize;
assert_eq!(
validate_cache_collection_completeness(&completeness, actual_row_count)
.is_ok(),
expected_valid,
"page_size={page_size}, page_count={page_count}, declared_row_count={declared_row_count}, actual_row_count={actual_row_count}"
);
}
}
}
}
let mut unknown = CacheCollectionCompleteness::api_exhausted(1, 1, 0, false);
unknown.status = "unknown".to_string();
assert_eq!(
validate_cache_collection_completeness(&unknown, 0),
Err("completeness status is unknown, expected api_exhausted".to_string())
);
assert_eq!(
serde_json::to_value(CacheCollectionCompleteness::api_exhausted(1, 1, 0, false))
.expect("serialize completeness")["status"],
"api_exhausted"
);
}
#[test]
fn completeness_enforces_page_capacity_without_narrow_multiplication() {
for (page_size, page_count, row_count, valid) in [
(2, 2, 4, true),
(2, 2, 5, false),
(1, 1, 0, true),
(u32::MAX, 2, 100, true),
(u32::MAX, u32::MAX, 0, true),
] {
let completeness =
CacheCollectionCompleteness::api_exhausted(page_size, page_count, row_count, false);
assert_eq!(
validate_cache_collection_completeness(&completeness, row_count).is_ok(),
valid
);
}
}
}