use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::refs::{AvailabilityKey, ScopeRef, TargetRef};
use super::verdict::{Availability, AvailabilityReason, AvailabilityState, DecidedBy};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DiscoverySource {
ProviderListing,
ProviderProbe,
CatalogueRecord,
OperatorAssertion,
}
impl DiscoverySource {
pub const ALL: &'static [Self] = &[
Self::ProviderListing,
Self::ProviderProbe,
Self::CatalogueRecord,
Self::OperatorAssertion,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::ProviderListing => "provider_listing",
Self::ProviderProbe => "provider_probe",
Self::CatalogueRecord => "catalogue_record",
Self::OperatorAssertion => "operator_assertion",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DiscoveryCompleteness {
Complete,
Partial,
Unsupported,
Unreliable,
}
impl DiscoveryCompleteness {
pub const ALL: &'static [Self] = &[
Self::Complete,
Self::Partial,
Self::Unsupported,
Self::Unreliable,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Complete => "complete",
Self::Partial => "partial",
Self::Unsupported => "unsupported",
Self::Unreliable => "unreliable",
}
}
pub const fn is_complete(self) -> bool {
matches!(self, Self::Complete)
}
const fn reason(self) -> AvailabilityReason {
match self {
Self::Complete => AvailabilityReason::Observed,
Self::Partial => AvailabilityReason::DiscoveryIncomplete,
Self::Unsupported => AvailabilityReason::DiscoveryUnsupported,
Self::Unreliable => AvailabilityReason::DiscoveryUnreliable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DiscoveryResult {
Present,
Absent,
Indeterminate,
}
impl DiscoveryResult {
pub const ALL: &'static [Self] = &[Self::Present, Self::Absent, Self::Indeterminate];
pub const fn as_str(self) -> &'static str {
match self {
Self::Present => "present",
Self::Absent => "absent",
Self::Indeterminate => "indeterminate",
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct DiscoveryObservation {
pub scope: ScopeRef,
pub target: TargetRef,
pub result: DiscoveryResult,
pub completeness: DiscoveryCompleteness,
pub source: DiscoverySource,
pub observed_at: SystemTime,
pub expires_at: Option<SystemTime>,
pub detail: Option<String>,
}
impl fmt::Debug for DiscoveryObservation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DiscoveryObservation")
.field("scope", &self.scope)
.field("target", &self.target)
.field("result", &self.result)
.field("completeness", &self.completeness)
.field("source", &self.source)
.field("observed_at", &self.observed_at)
.field("expires_at", &self.expires_at)
.field(
"detail",
&self.detail.as_ref().map(|_| "<redacted>").unwrap_or("none"),
)
.finish()
}
}
fn to_stored_resolution(instant: SystemTime) -> SystemTime {
match instant.duration_since(UNIX_EPOCH) {
Ok(since) => UNIX_EPOCH + Duration::from_micros(since.as_micros() as u64),
Err(_) => instant,
}
}
impl DiscoveryObservation {
pub fn new(
scope: ScopeRef,
target: TargetRef,
result: DiscoveryResult,
completeness: DiscoveryCompleteness,
source: DiscoverySource,
observed_at: SystemTime,
) -> Self {
Self {
scope,
target,
result,
completeness,
source,
observed_at: to_stored_resolution(observed_at),
expires_at: None,
detail: None,
}
}
#[must_use]
pub fn expiring_at(mut self, expires_at: SystemTime) -> Self {
self.expires_at = Some(to_stored_resolution(expires_at));
self
}
#[must_use]
pub fn detailed(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub fn without_detail(mut self) -> Self {
self.detail = None;
self
}
pub fn key(&self) -> AvailabilityKey {
AvailabilityKey::new(self.scope, self.target.clone())
}
pub fn is_same_look(&self, other: &Self) -> bool {
self.scope == other.scope
&& self.target == other.target
&& self.result == other.result
&& self.completeness == other.completeness
&& self.source == other.source
&& self.observed_at == other.observed_at
&& self.expires_at == other.expires_at
}
pub const fn is_definitive(&self) -> bool {
self.completeness.is_complete() && !matches!(self.result, DiscoveryResult::Indeterminate)
}
pub const fn is_positive(&self) -> bool {
self.completeness.is_complete() && matches!(self.result, DiscoveryResult::Present)
}
pub fn is_expired(&self, now: SystemTime) -> bool {
self.expires_at.is_some_and(|expires_at| now >= expires_at)
}
pub fn verdict(&self, now: SystemTime, last_known_good: bool) -> Availability {
let expired = self.is_expired(now);
let (state, reason) = match (self.result, self.completeness.is_complete(), expired) {
(_, false, _) => (AvailabilityState::Unknown, self.completeness.reason()),
(DiscoveryResult::Present, true, false) => (
AvailabilityState::Available,
if last_known_good {
AvailabilityReason::LastKnownGood
} else {
AvailabilityReason::Observed
},
),
(DiscoveryResult::Present, true, true) => (
AvailabilityState::Stale,
AvailabilityReason::EvidenceExpired,
),
(DiscoveryResult::Absent, true, false) => (
AvailabilityState::Denied,
AvailabilityReason::DiscoveryAbsent,
),
(DiscoveryResult::Absent, true, true) => (
AvailabilityState::Unknown,
AvailabilityReason::EvidenceExpired,
),
(DiscoveryResult::Indeterminate, true, _) => (
AvailabilityState::Unknown,
AvailabilityReason::DiscoveryUnreliable,
),
};
Availability::decided(state, reason, DecidedBy::Discovery).with_evidence(
self.observed_at,
self.expires_at,
self.source,
last_known_good,
)
}
}