use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Debug)]
pub struct AdmissionGate {
semaphores: HashMap<String, Arc<Semaphore>>,
}
impl AdmissionGate {
#[must_use]
pub fn new(providers: &[(String, usize)]) -> Self {
let semaphores = providers
.iter()
.filter(|(_, limit)| *limit > 0)
.map(|(name, limit)| (name.clone(), Arc::new(Semaphore::new(*limit))))
.collect();
Self { semaphores }
}
#[must_use]
pub fn try_acquire(&self, provider: &str) -> Option<OwnedSemaphorePermit> {
let sem = self.semaphores.get(provider)?;
Arc::clone(sem).try_acquire_owned().ok()
}
#[must_use]
pub fn has_gate(&self, provider: &str) -> bool {
self.semaphores.contains_key(provider)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permits_acquired_and_released() {
let gate = AdmissionGate::new(&[("quality".to_string(), 2)]);
let p1 = gate.try_acquire("quality");
let p2 = gate.try_acquire("quality");
assert!(p1.is_some());
assert!(p2.is_some());
let p3 = gate.try_acquire("quality");
assert!(p3.is_none());
drop(p1);
let p4 = gate.try_acquire("quality");
assert!(p4.is_some());
}
#[test]
fn try_acquire_returns_none_when_at_capacity() {
let gate = AdmissionGate::new(&[("limited".to_string(), 1)]);
let permit = gate.try_acquire("limited");
assert!(permit.is_some());
let second = gate.try_acquire("limited");
assert!(second.is_none(), "must return None at capacity");
}
#[test]
fn unknown_provider_returns_none() {
let gate = AdmissionGate::new(&[("known".to_string(), 3)]);
assert!(gate.try_acquire("unknown").is_none());
assert!(!gate.has_gate("unknown"));
}
#[test]
fn has_gate_reflects_configured_providers() {
let gate = AdmissionGate::new(&[("quality".to_string(), 3)]);
assert!(gate.has_gate("quality"));
assert!(!gate.has_gate("fast"));
}
#[test]
fn zero_limit_entry_is_ignored() {
let gate = AdmissionGate::new(&[("zero".to_string(), 0)]);
assert!(!gate.has_gate("zero"));
assert!(gate.try_acquire("zero").is_none());
}
#[test]
fn new_empty_providers_ok() {
let gate = AdmissionGate::new(&[]);
assert!(!gate.has_gate("any"));
}
#[test]
fn permit_released_after_drop_makes_slot_available() {
let gate = AdmissionGate::new(&[("q".to_string(), 1)]);
{
let _permit = gate.try_acquire("q").expect("should acquire");
assert!(gate.try_acquire("q").is_none(), "must be at capacity");
} assert!(
gate.try_acquire("q").is_some(),
"slot must be available after drop"
);
}
}