use super::outcome::{Indeterminacy, Outcome, Quality};
use super::policy::{AnalysisPolicy, Fingerprint, PolicyField};
use super::provenance::Provenance;
use crate::diagnostic::Diagnostic;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
#[non_exhaustive]
pub enum Status {
#[default]
Complete,
Partial,
Ambiguous,
Indeterminate,
}
impl Status {
#[must_use]
pub const fn is_usable(self) -> bool {
!matches!(self, Self::Indeterminate)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Coverage {
pub intended: u32,
pub used: u32,
pub missing: u32,
pub ambiguous: u32,
}
impl Coverage {
#[must_use]
pub const fn complete(atoms: u32) -> Self {
Self {
intended: atoms,
used: atoms,
missing: 0,
ambiguous: 0,
}
}
#[must_use]
pub fn fraction(self) -> f64 {
if self.intended == 0 {
1.0
} else {
f64::from(self.used) / f64::from(self.intended)
}
}
#[must_use]
pub const fn is_complete(self) -> bool {
self.missing == 0 && self.ambiguous == 0 && self.used == self.intended
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
#[non_exhaustive]
pub enum Impact {
#[default]
Unmeasured,
Inert,
Measured(MeasuredImpact),
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct MeasuredImpact {
pub metric: &'static str,
pub change: f64,
pub comparisons: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum AssumptionSource {
Explicit,
ProfileDefault,
Inferred,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Assumption {
pub field: PolicyField,
pub value: Box<str>,
pub source: AssumptionSource,
pub impact: Impact,
}
impl Assumption {
#[must_use]
pub fn new(
field: PolicyField,
value: impl Into<Box<str>>,
source: AssumptionSource,
impact: Impact,
) -> Self {
Self {
field,
value: value.into(),
source,
impact,
}
}
#[must_use]
pub fn is_silent_and_material(&self, threshold: f64) -> bool {
self.source == AssumptionSource::ProfileDefault
&& matches!(self.impact, Impact::Measured(measured) if measured.change >= threshold)
}
}
#[derive(Clone, Debug)]
pub struct Analysis<T> {
outcome: Outcome<T>,
quality: Quality,
pub coverage: Coverage,
pub warnings: Vec<Diagnostic>,
pub assumptions: Vec<Assumption>,
pub provenance: Provenance,
atom_origin: Option<Vec<u32>>,
}
impl<T> Analysis<T> {
#[must_use]
pub fn determinate(
value: T,
quality: Quality,
coverage: Coverage,
policy: &AnalysisPolicy,
) -> Self {
Self::from_parts(
Outcome::Determinate(value),
quality,
coverage,
Vec::new(),
Vec::new(),
Provenance::new(policy),
)
}
#[must_use]
pub fn complete(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
Self::determinate(value, Quality::Complete, coverage, policy)
}
#[must_use]
pub fn partial(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
Self::determinate(value, Quality::Partial, coverage, policy)
}
#[must_use]
pub fn ambiguous(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
Self::determinate(value, Quality::Ambiguous, coverage, policy)
}
#[must_use]
pub fn indeterminate(
reason: Indeterminacy,
coverage: Coverage,
policy: &AnalysisPolicy,
) -> Self {
Self::from_parts(
Outcome::Indeterminate(reason),
Quality::default(),
coverage,
Vec::new(),
Vec::new(),
Provenance::new(policy),
)
}
#[must_use]
pub const fn from_parts(
outcome: Outcome<T>,
quality: Quality,
coverage: Coverage,
warnings: Vec<Diagnostic>,
assumptions: Vec<Assumption>,
provenance: Provenance,
) -> Self {
Self {
outcome,
quality,
coverage,
warnings,
assumptions,
provenance,
atom_origin: None,
}
}
#[must_use]
pub fn with_atom_origin(mut self, origin: Vec<u32>) -> Self {
self.atom_origin = Some(origin);
self
}
#[must_use]
pub fn atom_origin(&self) -> Option<&[u32]> {
self.atom_origin.as_deref()
}
#[must_use]
pub fn into_parts(
self,
) -> (
Outcome<T>,
Quality,
Coverage,
Vec<Diagnostic>,
Vec<Assumption>,
Provenance,
) {
(
self.outcome,
self.quality,
self.coverage,
self.warnings,
self.assumptions,
self.provenance,
)
}
#[must_use]
pub const fn outcome(&self) -> &Outcome<T> {
&self.outcome
}
#[must_use]
pub fn into_outcome(self) -> Outcome<T> {
self.outcome
}
#[must_use]
pub const fn value(&self) -> Option<&T> {
self.outcome.value()
}
#[must_use]
pub const fn indeterminacy(&self) -> Option<&Indeterminacy> {
self.outcome.indeterminacy()
}
pub fn into_result(self) -> Result<T, Indeterminacy> {
self.outcome.into_result()
}
#[must_use]
pub const fn status(&self) -> Status {
match self.outcome {
Outcome::Determinate(_) => match self.quality {
Quality::Complete => Status::Complete,
Quality::Partial => Status::Partial,
Quality::Ambiguous => Status::Ambiguous,
},
Outcome::Indeterminate(_) => Status::Indeterminate,
}
}
#[must_use]
pub const fn quality(&self) -> Option<Quality> {
match self.outcome {
Outcome::Determinate(_) => Some(self.quality),
Outcome::Indeterminate(_) => None,
}
}
pub fn degrade(&mut self, quality: Quality) {
self.quality = self.quality.worst(quality);
}
#[must_use]
pub fn with_warning(mut self, warning: Diagnostic) -> Self {
self.warnings.push(warning);
self
}
#[must_use]
pub fn with_assumption(mut self, assumption: Assumption) -> Self {
self.assumptions.push(assumption);
self
}
#[must_use]
pub fn policy_fingerprint(&self) -> Fingerprint {
self.provenance.policy_fingerprint
}
pub fn silent_assumptions(&self, threshold: f64) -> impl Iterator<Item = &Assumption> {
self.assumptions
.iter()
.filter(move |assumption| assumption.is_silent_and_material(threshold))
}
pub fn map<U>(self, transform: impl FnOnce(T) -> U) -> Analysis<U> {
Analysis {
outcome: self.outcome.map(transform),
quality: self.quality,
coverage: self.coverage,
warnings: self.warnings,
assumptions: self.assumptions,
provenance: self.provenance,
atom_origin: self.atom_origin,
}
}
}
#[cfg(test)]
#[path = "analysis_tests.rs"]
mod tests;