use std::collections::BTreeMap;
use std::time::SystemTime;
use super::dimensions::{
CataloguePresence, Enablement, Entitlement, PolicyDecision, RuntimeHealth,
};
use super::discovery::DiscoveryObservation;
use super::refs::{AvailabilityKey, CredentialRef, ScopeRef, TargetRef};
use super::verdict::{Availability, AvailabilityReason, AvailabilityState, DecidedBy};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AvailabilityRecord {
pub presence: CataloguePresence,
pub enablement: Enablement,
pub entitlement: Entitlement,
pub policy: PolicyDecision,
pub runtime: RuntimeHealth,
pub credential: Option<CredentialRef>,
pub discovery: Option<DiscoveryObservation>,
pub last_known_good: Option<DiscoveryObservation>,
pub definitive_at: Option<SystemTime>,
}
impl Default for AvailabilityRecord {
fn default() -> Self {
Self {
presence: CataloguePresence::Absent,
enablement: Enablement::NotEnabled,
entitlement: Entitlement::Unknown,
policy: PolicyDecision::Indeterminate,
runtime: RuntimeHealth::Unobserved,
credential: None,
discovery: None,
last_known_good: None,
definitive_at: None,
}
}
}
impl AvailabilityRecord {
pub fn enabled() -> Self {
Self {
presence: CataloguePresence::Present,
enablement: Enablement::Enabled,
policy: PolicyDecision::Permitted,
..Self::default()
}
}
fn evidence(&self) -> Option<(&DiscoveryObservation, bool)> {
match (&self.discovery, &self.last_known_good) {
(Some(current), Some(retained))
if current.is_definitive() && current.observed_at >= retained.observed_at =>
{
Some((current, false))
}
(Some(current), None) if current.is_definitive() => Some((current, false)),
(_, Some(retained)) => Some((retained, true)),
(Some(current), None) => Some((current, false)),
(None, None) => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AvailabilityIndex {
records: BTreeMap<AvailabilityKey, AvailabilityRecord>,
}
impl AvailabilityIndex {
pub fn empty() -> Self {
Self::default()
}
pub fn builder() -> AvailabilityIndexBuilder {
AvailabilityIndexBuilder::default()
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn record(&self, key: &AvailabilityKey) -> Option<&AvailabilityRecord> {
self.records.get(key)
}
pub fn evaluate(&self, key: &AvailabilityKey, now: SystemTime) -> Availability {
let Some(record) = self.records.get(key) else {
return Availability::no_record();
};
Self::evaluate_record(record, now)
}
fn evaluate_record(record: &AvailabilityRecord, now: SystemTime) -> Availability {
match record.presence {
CataloguePresence::Absent => {
return Availability::decided(
AvailabilityState::Unavailable,
AvailabilityReason::NotInCatalogue,
DecidedBy::Catalogue,
);
}
CataloguePresence::Withdrawn => {
return Availability::decided(
AvailabilityState::Unavailable,
AvailabilityReason::WithdrawnFromCatalogue,
DecidedBy::Catalogue,
);
}
CataloguePresence::Present => {}
}
if record.policy == PolicyDecision::Denied {
return Availability::decided(
AvailabilityState::Denied,
AvailabilityReason::PolicyDenied,
DecidedBy::Policy,
);
}
if !record.enablement.is_enabled() {
return Availability::decided(
AvailabilityState::Denied,
AvailabilityReason::NotEnabled,
DecidedBy::Enablement,
);
}
if record.policy == PolicyDecision::Indeterminate {
return Availability::decided(
AvailabilityState::Unknown,
AvailabilityReason::PolicyIndeterminate,
DecidedBy::Policy,
);
}
match record.entitlement {
Entitlement::Missing => {
return Availability::decided(
AvailabilityState::Denied,
AvailabilityReason::EntitlementMissing,
DecidedBy::Entitlement,
);
}
Entitlement::Revoked => {
return Availability::decided(
AvailabilityState::Denied,
AvailabilityReason::EntitlementRevoked,
DecidedBy::Entitlement,
);
}
Entitlement::Unknown => {
return Availability::decided(
AvailabilityState::Unknown,
AvailabilityReason::EntitlementUnknown,
DecidedBy::Entitlement,
);
}
Entitlement::Granted => {}
}
if record.runtime == RuntimeHealth::Unavailable {
return Availability::decided(
AvailabilityState::Unavailable,
AvailabilityReason::RuntimeUnavailable,
DecidedBy::Runtime,
);
}
let evidence = match record.evidence() {
Some((observation, last_known_good)) => observation.verdict(now, last_known_good),
None => Availability::decided(
AvailabilityState::Unknown,
AvailabilityReason::NoEvidence,
DecidedBy::Discovery,
),
};
if record.runtime == RuntimeHealth::Impaired
&& evidence.state.permits_attempt()
&& evidence.state.certainty() > AvailabilityState::Unknown.certainty()
{
return evidence
.lowered_to_unknown(AvailabilityReason::RuntimeImpaired, DecidedBy::Runtime);
}
evidence
}
pub fn evaluate_scope(
&self,
scope: &ScopeRef,
now: SystemTime,
) -> Vec<(TargetRef, Availability)> {
self.records
.iter()
.filter(|(key, _)| key.scope == *scope)
.map(|(key, record)| (key.target.clone(), Self::evaluate_record(record, now)))
.collect()
}
pub fn evaluate_all(&self, now: SystemTime) -> Vec<(AvailabilityKey, Availability)> {
self.records
.iter()
.map(|(key, record)| (key.clone(), Self::evaluate_record(record, now)))
.collect()
}
}
#[derive(Debug, Clone, Default)]
pub struct AvailabilityIndexBuilder {
records: BTreeMap<AvailabilityKey, AvailabilityRecord>,
superseded: usize,
misfiled: usize,
}
impl AvailabilityIndexBuilder {
pub fn from_index(index: &AvailabilityIndex) -> Self {
Self {
records: index.records.clone(),
superseded: 0,
misfiled: 0,
}
}
#[must_use]
pub fn record(mut self, key: AvailabilityKey, record: AvailabilityRecord) -> Self {
let owned = |evidence: &Option<DiscoveryObservation>| {
evidence.clone().filter(|look| look.key() == key)
};
let retained = owned(&record.last_known_good);
let current = owned(&record.discovery);
let misfiled = [
(&record.last_known_good, &retained),
(&record.discovery, ¤t),
]
.iter()
.filter(|(declared, kept)| declared.is_some() && kept.is_none())
.count();
self.misfiled += misfiled;
let declared_conclusion = (misfiled == 0).then_some(record.definitive_at).flatten();
let entry = self.records.entry(key).or_default();
*entry = AvailabilityRecord {
discovery: entry.discovery.clone(),
last_known_good: entry.last_known_good.clone(),
definitive_at: entry.definitive_at,
..record
};
let mut already_judged = None;
if let Some(current) = current {
if Self::holds(&entry.discovery, ¤t) {
already_judged = Some(current);
} else {
already_judged = Some(current.clone());
if !Self::admit(entry, current) {
self.superseded += 1;
}
}
}
if let Some(retained) = retained
&& !already_judged
.as_ref()
.is_some_and(|judged| judged.is_same_look(&retained))
&& !Self::holds(&entry.last_known_good, &retained)
&& !Self::retain(entry, &retained)
{
self.superseded += 1;
}
if let Some(declared) = declared_conclusion {
entry.definitive_at = Some(
entry
.definitive_at
.map_or(declared, |held| held.max(declared)),
);
let discredited = |held: &Option<DiscoveryObservation>| {
held.as_ref().is_some_and(|look| {
look.is_positive() && Some(look.observed_at) < entry.definitive_at
})
};
if discredited(&entry.last_known_good) {
entry.last_known_good = None;
}
if discredited(&entry.discovery) {
entry.discovery = None;
}
}
self
}
#[must_use]
pub fn observe(mut self, observation: DiscoveryObservation) -> Self {
let entry = self.records.entry(observation.key()).or_default();
let held = Self::holds(&entry.discovery, &observation)
|| Self::holds(&entry.last_known_good, &observation);
if !held && !Self::admit(entry, observation) {
self.superseded += 1;
}
self
}
fn holds(slot: &Option<DiscoveryObservation>, look: &DiscoveryObservation) -> bool {
slot.as_ref().is_some_and(|held| held.is_same_look(look))
}
fn admit(entry: &mut AvailabilityRecord, observation: DiscoveryObservation) -> bool {
let overturns_conclusion = Self::retain(entry, &observation);
let newest_held = entry
.discovery
.iter()
.chain(entry.last_known_good.iter())
.map(|held| held.observed_at)
.chain(entry.definitive_at)
.max();
if !overturns_conclusion || newest_held.is_some_and(|held| held > observation.observed_at) {
return false;
}
entry.discovery = Some(observation);
true
}
fn retain(entry: &mut AvailabilityRecord, observation: &DiscoveryObservation) -> bool {
let overturns_conclusion = Self::overturns(entry.definitive_at, observation);
if observation.is_definitive() && overturns_conclusion {
entry.last_known_good = observation.is_positive().then(|| observation.clone());
entry.definitive_at = Some(observation.observed_at);
}
overturns_conclusion
}
fn overturns(concluded: Option<SystemTime>, observation: &DiscoveryObservation) -> bool {
concluded.is_none_or(|held| {
if observation.is_definitive() && !observation.is_positive() {
observation.observed_at >= held
} else {
observation.observed_at > held
}
})
}
pub fn superseded(&self) -> usize {
self.superseded
}
pub fn misfiled(&self) -> usize {
self.misfiled
}
pub fn build(self) -> AvailabilityIndex {
AvailabilityIndex {
records: self.records,
}
}
}