1use serde::Serialize;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum Requirement {
17 Advisory,
20 DecisionRequired,
23 Required,
25}
26
27impl Requirement {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::Advisory => "advisory",
33 Self::DecisionRequired => "decision-required",
34 Self::Required => "required",
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41#[serde(tag = "state", rename_all = "kebab-case")]
42pub enum Evaluation {
43 Satisfied,
45 NotObserved {
47 reason: String,
49 },
50 Unsatisfied {
52 reason: String,
54 },
55}
56
57impl Evaluation {
58 #[must_use]
60 pub const fn word(&self) -> &'static str {
61 match self {
62 Self::Satisfied => "satisfied",
63 Self::NotObserved { .. } => "not-observed",
64 Self::Unsatisfied { .. } => "unsatisfied",
65 }
66 }
67
68 #[must_use]
70 pub const fn holds(&self) -> bool {
71 matches!(self, Self::Satisfied)
72 }
73}
74
75#[derive(Debug, Clone, Serialize)]
78pub struct Precondition {
79 pub id: String,
81 pub requirement: Requirement,
83 pub evaluation: Evaluation,
85 #[serde(skip_serializing_if = "Option::is_none")]
88 pub decision: Option<String>,
89 pub evidence_refs: Vec<String>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
95#[serde(rename_all = "kebab-case")]
96pub enum Readiness {
97 Ready,
99 NeedsDecision,
101 Blocked,
103}
104
105impl Readiness {
106 #[must_use]
108 pub const fn as_str(self) -> &'static str {
109 match self {
110 Self::Ready => "ready",
111 Self::NeedsDecision => "needs-decision",
112 Self::Blocked => "blocked",
113 }
114 }
115}
116
117#[must_use]
123pub fn derive(preconditions: &[Precondition]) -> Readiness {
124 let mut readiness = Readiness::Ready;
125 for precondition in preconditions {
126 if precondition.evaluation.holds() {
127 continue;
128 }
129 match precondition.requirement {
130 Requirement::Required => return Readiness::Blocked,
131 Requirement::DecisionRequired => readiness = Readiness::NeedsDecision,
132 Requirement::Advisory => {}
133 }
134 }
135 readiness
136}
137
138#[cfg(test)]
139mod tests {
140 use super::{Evaluation, Precondition, Readiness, Requirement, derive};
141
142 fn precondition(requirement: Requirement, evaluation: Evaluation) -> Precondition {
143 Precondition {
144 id: "one".into(),
145 requirement,
146 evaluation,
147 decision: None,
148 evidence_refs: Vec::new(),
149 }
150 }
151
152 #[test]
155 fn readiness_is_the_worst_precondition() {
156 let not_observed = || Evaluation::NotObserved {
157 reason: "unread".into(),
158 };
159 let unsatisfied = || Evaluation::Unsatisfied {
160 reason: "found otherwise".into(),
161 };
162 let table: Vec<(Requirement, Evaluation, Readiness)> = vec![
163 (
164 Requirement::Advisory,
165 Evaluation::Satisfied,
166 Readiness::Ready,
167 ),
168 (Requirement::Advisory, not_observed(), Readiness::Ready),
169 (Requirement::Advisory, unsatisfied(), Readiness::Ready),
170 (
171 Requirement::DecisionRequired,
172 Evaluation::Satisfied,
173 Readiness::Ready,
174 ),
175 (
176 Requirement::DecisionRequired,
177 not_observed(),
178 Readiness::NeedsDecision,
179 ),
180 (
181 Requirement::DecisionRequired,
182 unsatisfied(),
183 Readiness::NeedsDecision,
184 ),
185 (
186 Requirement::Required,
187 Evaluation::Satisfied,
188 Readiness::Ready,
189 ),
190 (Requirement::Required, not_observed(), Readiness::Blocked),
191 (Requirement::Required, unsatisfied(), Readiness::Blocked),
192 ];
193 for (requirement, evaluation, expected) in table {
194 let got = derive(&[precondition(requirement, evaluation.clone())]);
195 assert_eq!(got, expected, "{requirement:?} {evaluation:?}");
196 }
197 assert_eq!(derive(&[]), Readiness::Ready);
198 let mixed = [
199 precondition(Requirement::Advisory, unsatisfied()),
200 precondition(Requirement::DecisionRequired, not_observed()),
201 precondition(Requirement::Required, Evaluation::Satisfied),
202 ];
203 assert_eq!(derive(&mixed), Readiness::NeedsDecision);
204 let blocked = [
205 precondition(Requirement::DecisionRequired, not_observed()),
206 precondition(Requirement::Required, unsatisfied()),
207 ];
208 assert_eq!(derive(&blocked), Readiness::Blocked);
209 }
210
211 #[test]
212 fn the_words_are_the_wire_form() {
213 for (requirement, word) in [
214 (Requirement::Advisory, "advisory"),
215 (Requirement::DecisionRequired, "decision-required"),
216 (Requirement::Required, "required"),
217 ] {
218 assert_eq!(requirement.as_str(), word);
219 assert_eq!(
220 serde_json::to_string(&requirement).expect("serializes"),
221 format!("\"{word}\"")
222 );
223 }
224 for (readiness, word) in [
225 (Readiness::Ready, "ready"),
226 (Readiness::NeedsDecision, "needs-decision"),
227 (Readiness::Blocked, "blocked"),
228 ] {
229 assert_eq!(readiness.as_str(), word);
230 assert_eq!(
231 serde_json::to_string(&readiness).expect("serializes"),
232 format!("\"{word}\"")
233 );
234 }
235 assert_eq!(
236 serde_json::to_string(&Evaluation::NotObserved {
237 reason: "unread".into()
238 })
239 .expect("serializes"),
240 r#"{"state":"not-observed","reason":"unread"}"#
241 );
242 }
243}