use std::collections::BTreeMap;
use jiff::Timestamp;
use crate::record::AccessRecord;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hold {
until: Timestamp,
released: Option<Timestamp>,
}
impl Hold {
#[must_use]
pub const fn new(until: Timestamp) -> Self {
Self {
until,
released: None,
}
}
#[must_use]
pub const fn expires(&self) -> Timestamp {
self.until
}
#[must_use]
pub const fn released(&self) -> Option<Timestamp> {
self.released
}
#[must_use]
pub fn ends(&self) -> Timestamp {
self.released
.map_or(self.until, |released| released.min(self.until))
}
#[must_use]
pub fn withholds(&self, now: Timestamp) -> bool {
now < self.ends()
}
pub fn release(&mut self, at: Timestamp) {
self.released = Some(self.released.map_or(at, |released| released.min(at)));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Reader {
Person,
Professional,
Supervisory,
}
impl Reader {
#[must_use]
pub const fn is_person(self) -> bool {
matches!(self, Self::Person)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum HoldError {
#[error("the resource is not held from the person")]
NotHeld,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Holds {
by_resource: BTreeMap<String, Hold>,
}
impl Holds {
pub fn hold(&mut self, resource: impl Into<String>, until: Timestamp) {
let fresh = Hold::new(until);
self.by_resource
.entry(resource.into())
.and_modify(|hold| {
*hold = match hold.released {
None => Hold::new(hold.until.max(until)),
Some(_) => fresh,
};
})
.or_insert(fresh);
}
pub fn release(&mut self, resource: &str, at: Timestamp) -> Result<(), HoldError> {
let hold = self
.by_resource
.get_mut(resource)
.ok_or(HoldError::NotHeld)?;
hold.release(at);
Ok(())
}
#[must_use]
pub fn get(&self, resource: &str) -> Option<&Hold> {
self.by_resource.get(resource)
}
#[must_use]
pub fn withholds(&self, resource: &str, now: Timestamp) -> bool {
let object = resource.split_once("::").map(|(object, _)| object);
[Some(resource), object]
.into_iter()
.flatten()
.filter_map(|id| self.by_resource.get(id))
.any(|hold| hold.withholds(now))
}
#[must_use]
pub fn shows(&self, reader: Reader, record: &AccessRecord, now: Timestamp) -> bool {
if !reader.is_person() || self.by_resource.is_empty() {
return true;
}
let cut = record.categories.evidence_counts().versions > record.categories.versions().len()
|| record.origins.iter().any(|origin| {
origin.categories.as_ref().is_some_and(|categories| {
categories.evidence_counts().versions > categories.versions().len()
})
});
if cut && self.by_resource.values().any(|hold| hold.withholds(now)) {
return false;
}
!named(record).any(|resource| self.withholds(resource, now))
}
pub fn trail<'a>(
&'a self,
reader: Reader,
records: impl IntoIterator<Item = &'a AccessRecord> + 'a,
now: Timestamp,
) -> impl Iterator<Item = &'a AccessRecord> + 'a {
records
.into_iter()
.filter(move |record| self.shows(reader, record, now))
}
}
fn named(record: &AccessRecord) -> impl Iterator<Item = &str> {
let origins = record.origins.iter().flat_map(|origin| {
let versions = origin
.categories
.iter()
.flat_map(|categories| categories.versions().iter().map(String::as_str));
origin
.version
.as_ref()
.and_then(|version| version.named())
.into_iter()
.chain(versions)
});
record
.request
.resource
.as_deref()
.into_iter()
.chain(record.categories.versions().iter().map(String::as_str))
.chain(origins)
}