use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Requirement {
Advisory,
DecisionRequired,
Required,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum Evaluation {
Satisfied,
NotObserved {
reason: String,
},
Unsatisfied {
reason: String,
},
}
impl Evaluation {
#[must_use]
pub const fn is_satisfied(&self) -> bool {
matches!(self, Self::Satisfied)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Readiness {
Ready,
NeedsDecision,
Blocked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Precondition {
pub id: String,
pub statement: String,
pub requirement: Requirement,
pub evaluation: Evaluation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_by: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_refs: Vec<String>,
}
impl Precondition {
#[must_use]
pub const fn verdict(&self) -> Readiness {
if self.evaluation.is_satisfied() {
return Readiness::Ready;
}
match self.requirement {
Requirement::Advisory => Readiness::Ready,
Requirement::DecisionRequired => Readiness::NeedsDecision,
Requirement::Required => Readiness::Blocked,
}
}
}
#[must_use]
pub fn readiness(preconditions: &[Precondition]) -> Readiness {
preconditions
.iter()
.map(Precondition::verdict)
.max()
.unwrap_or(Readiness::Ready)
}
#[cfg(test)]
mod tests {
use super::*;
fn precondition(requirement: Requirement, evaluation: Evaluation) -> Precondition {
Precondition {
id: "one".to_string(),
statement: "one thing holds".to_string(),
requirement,
evaluation,
resolved_by: None,
evidence_refs: Vec::new(),
}
}
fn unsatisfied() -> Evaluation {
Evaluation::Unsatisfied {
reason: "it does not".to_string(),
}
}
fn not_observed() -> Evaluation {
Evaluation::NotObserved {
reason: "nobody could tell".to_string(),
}
}
#[test]
fn readiness_is_the_worst_precondition() {
use Requirement::{Advisory, DecisionRequired, Required};
let cases: &[(Requirement, Evaluation, Readiness)] = &[
(Advisory, Evaluation::Satisfied, Readiness::Ready),
(Advisory, unsatisfied(), Readiness::Ready),
(Advisory, not_observed(), Readiness::Ready),
(DecisionRequired, Evaluation::Satisfied, Readiness::Ready),
(DecisionRequired, unsatisfied(), Readiness::NeedsDecision),
(DecisionRequired, not_observed(), Readiness::NeedsDecision),
(Required, Evaluation::Satisfied, Readiness::Ready),
(Required, unsatisfied(), Readiness::Blocked),
(Required, not_observed(), Readiness::Blocked),
];
for (requirement, evaluation, expected) in cases {
let held = precondition(*requirement, evaluation.clone());
assert_eq!(held.verdict(), *expected, "{requirement:?} {evaluation:?}");
}
}
#[test]
fn one_blocked_precondition_blocks_the_plan_whatever_its_position() {
let ready = precondition(Requirement::Advisory, Evaluation::Satisfied);
let waiting = precondition(Requirement::DecisionRequired, unsatisfied());
let blocked = precondition(Requirement::Required, unsatisfied());
assert_eq!(readiness(&[]), Readiness::Ready);
assert_eq!(readiness(std::slice::from_ref(&ready)), Readiness::Ready);
assert_eq!(
readiness(&[ready.clone(), waiting.clone()]),
Readiness::NeedsDecision
);
assert_eq!(
readiness(&[blocked.clone(), ready.clone(), waiting.clone()]),
Readiness::Blocked
);
assert_eq!(readiness(&[waiting, ready, blocked]), Readiness::Blocked);
}
#[test]
fn a_gap_is_honest_and_is_not_permission() {
let held = precondition(Requirement::Required, not_observed());
assert_eq!(held.verdict(), Readiness::Blocked);
}
}