use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Requirement {
Advisory,
DecisionRequired,
Required,
}
impl Requirement {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Advisory => "advisory",
Self::DecisionRequired => "decision-required",
Self::Required => "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 word(&self) -> &'static str {
match self {
Self::Satisfied => "satisfied",
Self::NotObserved { .. } => "not-observed",
Self::Unsatisfied { .. } => "unsatisfied",
}
}
#[must_use]
pub const fn holds(&self) -> bool {
matches!(self, Self::Satisfied)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Precondition {
pub id: String,
pub requirement: Requirement,
pub evaluation: Evaluation,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<String>,
pub evidence_refs: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Readiness {
Ready,
NeedsDecision,
Blocked,
}
impl Readiness {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ready => "ready",
Self::NeedsDecision => "needs-decision",
Self::Blocked => "blocked",
}
}
}
#[must_use]
pub fn derive(preconditions: &[Precondition]) -> Readiness {
let mut readiness = Readiness::Ready;
for precondition in preconditions {
if precondition.evaluation.holds() {
continue;
}
match precondition.requirement {
Requirement::Required => return Readiness::Blocked,
Requirement::DecisionRequired => readiness = Readiness::NeedsDecision,
Requirement::Advisory => {}
}
}
readiness
}
#[cfg(test)]
mod tests {
use super::{Evaluation, Precondition, Readiness, Requirement, derive};
fn precondition(requirement: Requirement, evaluation: Evaluation) -> Precondition {
Precondition {
id: "one".into(),
requirement,
evaluation,
decision: None,
evidence_refs: Vec::new(),
}
}
#[test]
fn readiness_is_the_worst_precondition() {
let not_observed = || Evaluation::NotObserved {
reason: "unread".into(),
};
let unsatisfied = || Evaluation::Unsatisfied {
reason: "found otherwise".into(),
};
let table: Vec<(Requirement, Evaluation, Readiness)> = vec![
(
Requirement::Advisory,
Evaluation::Satisfied,
Readiness::Ready,
),
(Requirement::Advisory, not_observed(), Readiness::Ready),
(Requirement::Advisory, unsatisfied(), Readiness::Ready),
(
Requirement::DecisionRequired,
Evaluation::Satisfied,
Readiness::Ready,
),
(
Requirement::DecisionRequired,
not_observed(),
Readiness::NeedsDecision,
),
(
Requirement::DecisionRequired,
unsatisfied(),
Readiness::NeedsDecision,
),
(
Requirement::Required,
Evaluation::Satisfied,
Readiness::Ready,
),
(Requirement::Required, not_observed(), Readiness::Blocked),
(Requirement::Required, unsatisfied(), Readiness::Blocked),
];
for (requirement, evaluation, expected) in table {
let got = derive(&[precondition(requirement, evaluation.clone())]);
assert_eq!(got, expected, "{requirement:?} {evaluation:?}");
}
assert_eq!(derive(&[]), Readiness::Ready);
let mixed = [
precondition(Requirement::Advisory, unsatisfied()),
precondition(Requirement::DecisionRequired, not_observed()),
precondition(Requirement::Required, Evaluation::Satisfied),
];
assert_eq!(derive(&mixed), Readiness::NeedsDecision);
let blocked = [
precondition(Requirement::DecisionRequired, not_observed()),
precondition(Requirement::Required, unsatisfied()),
];
assert_eq!(derive(&blocked), Readiness::Blocked);
}
#[test]
fn the_words_are_the_wire_form() {
for (requirement, word) in [
(Requirement::Advisory, "advisory"),
(Requirement::DecisionRequired, "decision-required"),
(Requirement::Required, "required"),
] {
assert_eq!(requirement.as_str(), word);
assert_eq!(
serde_json::to_string(&requirement).expect("serializes"),
format!("\"{word}\"")
);
}
for (readiness, word) in [
(Readiness::Ready, "ready"),
(Readiness::NeedsDecision, "needs-decision"),
(Readiness::Blocked, "blocked"),
] {
assert_eq!(readiness.as_str(), word);
assert_eq!(
serde_json::to_string(&readiness).expect("serializes"),
format!("\"{word}\"")
);
}
assert_eq!(
serde_json::to_string(&Evaluation::NotObserved {
reason: "unread".into()
})
.expect("serializes"),
r#"{"state":"not-observed","reason":"unread"}"#
);
}
}