use std::collections::{HashMap, hash_map::Entry};
use thiserror::Error;
use crate::Check;
pub fn validate_batch(checks: &[Check<'_>], maximum: usize) -> Result<(), BatchError> {
if checks.len() > maximum {
return Err(BatchError::BatchTooLarge {
actual: checks.len(),
maximum,
});
}
let mut first_indices = HashMap::with_capacity(checks.len());
for (duplicate_index, check) in checks.iter().enumerate() {
match first_indices.entry(check.counter_key()) {
Entry::Vacant(entry) => {
entry.insert(duplicate_index);
}
Entry::Occupied(entry) => {
return Err(BatchError::DuplicateKey {
first_index: *entry.get(),
duplicate_index,
});
}
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum BatchError {
#[error("batch check at index {duplicate_index} duplicates the counter at index {first_index}")]
DuplicateKey {
first_index: usize,
duplicate_index: usize,
},
#[error("batch has {actual} checks but the configured maximum is {maximum}")]
BatchTooLarge {
actual: usize,
maximum: usize,
},
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{BatchError, validate_batch};
use crate::{Check, FixedWindowPolicy, PolicyId, ScopeId, SubjectKey};
fn policy(id: &str) -> FixedWindowPolicy {
FixedWindowPolicy::new(
PolicyId::new(id).unwrap(),
ScopeId::new("client").unwrap(),
10,
Duration::from_secs(60),
)
.unwrap()
}
fn subject(byte: u8) -> SubjectKey {
SubjectKey::from_digest([byte; 32])
}
#[test]
fn accepts_empty_and_distinct_batches() {
let first_policy = policy("auth.alpha");
let second_policy = policy("auth.beta");
let checks = [
Check::new(&first_policy, subject(1)),
Check::new(&second_policy, subject(1)),
Check::new(&first_policy, subject(2)),
];
assert_eq!(validate_batch(&[], 0), Ok(()));
assert_eq!(validate_batch(&checks, checks.len()), Ok(()));
}
#[test]
fn reports_the_first_duplicate_in_caller_order() {
let alpha = policy("auth.alpha");
let beta = policy("auth.beta");
let checks = [
Check::new(&beta, subject(1)),
Check::new(&beta, subject(1)),
Check::new(&alpha, subject(2)),
Check::new(&alpha, subject(2)),
];
assert_eq!(
validate_batch(&checks, checks.len()),
Err(BatchError::DuplicateKey {
first_index: 0,
duplicate_index: 1,
})
);
}
#[test]
fn batch_size_error_takes_precedence_over_duplicate_detection() {
let policy = policy("auth.alpha");
let checks = [
Check::new(&policy, subject(1)),
Check::new(&policy, subject(1)),
];
assert_eq!(
validate_batch(&checks, 1),
Err(BatchError::BatchTooLarge {
actual: 2,
maximum: 1,
})
);
}
}