1use serde::{Deserialize, Serialize};
23
24use crate::{
25 error::{CatapultError, Result},
26 oda::OdaSlot,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
31pub enum OperationalPhase {
32 Assessment,
34 Selection,
36 Preparation,
38 Execution,
40 Sustainment,
42 Transition,
44}
45
46impl OperationalPhase {
47 #[must_use]
49 pub fn valid_transitions(self) -> &'static [OperationalPhase] {
50 use OperationalPhase::*;
51 match self {
52 Assessment => &[Selection],
53 Selection => &[Preparation, Assessment],
54 Preparation => &[Execution, Selection],
55 Execution => &[Sustainment],
56 Sustainment => &[Transition, Execution],
57 Transition => &[Assessment],
58 }
59 }
60
61 #[must_use]
63 pub fn can_transition_to(self, target: OperationalPhase) -> bool {
64 self.valid_transitions().contains(&target)
65 }
66
67 pub fn transition(self, target: OperationalPhase) -> Result<OperationalPhase> {
69 if self.can_transition_to(target) {
70 Ok(target)
71 } else {
72 Err(CatapultError::InvalidPhaseTransition {
73 from: self,
74 to: target,
75 })
76 }
77 }
78
79 #[must_use]
81 pub fn min_roster(self) -> &'static [OdaSlot] {
82 use OperationalPhase::*;
83 match self {
84 Assessment => &[],
85 Selection => &OdaSlot::FOUNDERS,
86 Preparation => &[
87 OdaSlot::HrPeopleOps1,
88 OdaSlot::DeepResearcher,
89 OdaSlot::VentureCommander,
90 OdaSlot::ProcessArchitect,
91 ],
92 Execution | Sustainment => &OdaSlot::ALL,
93 Transition => &[OdaSlot::VentureCommander, OdaSlot::OperationsDeputy],
94 }
95 }
96
97 pub const ALL: [OperationalPhase; 6] = [
99 Self::Assessment,
100 Self::Selection,
101 Self::Preparation,
102 Self::Execution,
103 Self::Sustainment,
104 Self::Transition,
105 ];
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn happy_path_forward() {
114 use OperationalPhase::*;
115 let path = [
116 Assessment,
117 Selection,
118 Preparation,
119 Execution,
120 Sustainment,
121 Transition,
122 ];
123 for w in path.windows(2) {
124 assert!(
125 w[0].can_transition_to(w[1]),
126 "{:?} should transition to {:?}",
127 w[0],
128 w[1]
129 );
130 }
131 }
132
133 #[test]
134 fn backward_transitions() {
135 use OperationalPhase::*;
136 assert!(Selection.can_transition_to(Assessment));
138 assert!(Preparation.can_transition_to(Selection));
140 assert!(Sustainment.can_transition_to(Execution));
142 assert!(Transition.can_transition_to(Assessment));
144 }
145
146 #[test]
147 fn invalid_transitions() {
148 use OperationalPhase::*;
149 assert!(!Assessment.can_transition_to(Execution));
150 assert!(!Assessment.can_transition_to(Transition));
151 assert!(!Execution.can_transition_to(Assessment));
152 assert!(!Sustainment.can_transition_to(Selection));
153 }
154
155 #[test]
156 fn transition_result() {
157 use OperationalPhase::*;
158 assert_eq!(Assessment.transition(Selection).unwrap(), Selection);
159 assert!(Assessment.transition(Execution).is_err());
160 }
161
162 #[test]
163 fn min_roster_assessment_empty() {
164 assert!(OperationalPhase::Assessment.min_roster().is_empty());
165 }
166
167 #[test]
168 fn min_roster_selection_founders() {
169 let roster = OperationalPhase::Selection.min_roster();
170 assert_eq!(roster.len(), 2);
171 assert!(roster.contains(&OdaSlot::HrPeopleOps1));
172 assert!(roster.contains(&OdaSlot::DeepResearcher));
173 }
174
175 #[test]
176 fn min_roster_execution_full() {
177 assert_eq!(OperationalPhase::Execution.min_roster().len(), 12);
178 }
179
180 #[test]
181 fn min_roster_transition_minimal() {
182 let roster = OperationalPhase::Transition.min_roster();
183 assert_eq!(roster.len(), 2);
184 assert!(roster.contains(&OdaSlot::VentureCommander));
185 assert!(roster.contains(&OdaSlot::OperationsDeputy));
186 }
187
188 #[test]
189 fn all_phases_count() {
190 assert_eq!(OperationalPhase::ALL.len(), 6);
191 }
192
193 #[test]
194 fn serde_roundtrip() {
195 for phase in &OperationalPhase::ALL {
196 let j = serde_json::to_string(phase).unwrap();
197 let rt: OperationalPhase = serde_json::from_str(&j).unwrap();
198 assert_eq!(&rt, phase);
199 }
200 }
201}