meerkat_runtime/
runtime_state.rs1use serde::{Deserialize, Serialize};
6
7#[allow(dead_code)]
8fn can_transition(from: &RuntimeState, next: &RuntimeState) -> bool {
9 use RuntimeState::{
10 Attached, Destroyed, Idle, Initializing, Recovering, Retired, Running, Stopped,
11 };
12
13 matches!(
14 (from, next),
15 (Initializing, Idle | Stopped | Destroyed)
16 | (
17 Idle,
18 Attached | Running | Retired | Recovering | Stopped | Destroyed
19 )
20 | (
21 Attached,
22 Running | Idle | Retired | Recovering | Stopped | Destroyed
23 )
24 | (
25 Running,
26 Idle | Attached | Recovering | Retired | Stopped | Destroyed
27 )
28 | (Recovering, Idle | Attached | Running | Stopped | Destroyed)
29 | (Retired, Running | Stopped | Destroyed)
30 )
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36#[non_exhaustive]
37pub enum RuntimeState {
38 Initializing,
40 Idle,
42 Attached,
44 Running,
46 Recovering,
48 Retired,
50 Stopped,
52 Destroyed,
54}
55
56impl RuntimeState {
57 pub fn is_terminal(&self) -> bool {
59 matches!(self, Self::Stopped | Self::Destroyed)
60 }
61
62 pub fn can_accept_input(&self) -> bool {
64 matches!(self, Self::Idle | Self::Attached | Self::Running)
65 }
66
67 pub fn can_process_queue(&self) -> bool {
69 matches!(self, Self::Idle | Self::Attached | Self::Retired)
70 }
71
72 pub fn is_attached(&self) -> bool {
74 matches!(self, Self::Attached)
75 }
76
77 pub fn is_idle_or_attached(&self) -> bool {
79 matches!(self, Self::Idle | Self::Attached)
80 }
81}
82
83impl std::fmt::Display for RuntimeState {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::Initializing => write!(f, "initializing"),
87 Self::Idle => write!(f, "idle"),
88 Self::Attached => write!(f, "attached"),
89 Self::Running => write!(f, "running"),
90 Self::Recovering => write!(f, "recovering"),
91 Self::Retired => write!(f, "retired"),
92 Self::Stopped => write!(f, "stopped"),
93 Self::Destroyed => write!(f, "destroyed"),
94 }
95 }
96}
97
98#[derive(Debug, Clone, thiserror::Error)]
100#[error("Invalid runtime state transition: {from} -> {to}")]
101pub struct RuntimeStateTransitionError {
102 pub from: RuntimeState,
103 pub to: RuntimeState,
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn terminal_states() {
113 assert!(RuntimeState::Stopped.is_terminal());
114 assert!(RuntimeState::Destroyed.is_terminal());
115 assert!(!RuntimeState::Initializing.is_terminal());
116 assert!(!RuntimeState::Idle.is_terminal());
117 assert!(!RuntimeState::Attached.is_terminal());
118 assert!(!RuntimeState::Running.is_terminal());
119 assert!(!RuntimeState::Recovering.is_terminal());
120 assert!(!RuntimeState::Retired.is_terminal());
121 }
122
123 #[test]
124 fn input_and_queue_capabilities() {
125 assert!(RuntimeState::Idle.can_accept_input());
126 assert!(RuntimeState::Attached.can_accept_input());
127 assert!(RuntimeState::Running.can_accept_input());
128 assert!(!RuntimeState::Retired.can_accept_input());
129
130 assert!(RuntimeState::Idle.can_process_queue());
131 assert!(RuntimeState::Attached.can_process_queue());
132 assert!(RuntimeState::Retired.can_process_queue());
133 assert!(!RuntimeState::Running.can_process_queue());
134 }
135
136 #[test]
137 fn attachment_predicates() {
138 assert!(RuntimeState::Attached.is_attached());
139 assert!(RuntimeState::Idle.is_idle_or_attached());
140 assert!(RuntimeState::Attached.is_idle_or_attached());
141 assert!(!RuntimeState::Running.is_idle_or_attached());
142 }
143
144 #[test]
145 fn transition_table_matches_spec_examples() {
146 assert!(can_transition(
147 &RuntimeState::Initializing,
148 &RuntimeState::Idle
149 ));
150 assert!(can_transition(&RuntimeState::Idle, &RuntimeState::Attached));
151 assert!(can_transition(
152 &RuntimeState::Attached,
153 &RuntimeState::Running
154 ));
155 assert!(can_transition(
156 &RuntimeState::Running,
157 &RuntimeState::Retired
158 ));
159 assert!(can_transition(
160 &RuntimeState::Retired,
161 &RuntimeState::Stopped
162 ));
163
164 assert!(!can_transition(&RuntimeState::Stopped, &RuntimeState::Idle));
165 assert!(!can_transition(
166 &RuntimeState::Destroyed,
167 &RuntimeState::Running
168 ));
169 assert!(!can_transition(&RuntimeState::Retired, &RuntimeState::Idle));
170 }
171
172 #[test]
173 fn transition_failure_shape_matches_runtime_error() {
174 let result = if can_transition(&RuntimeState::Stopped, &RuntimeState::Idle) {
175 Ok(())
176 } else {
177 Err(RuntimeStateTransitionError {
178 from: RuntimeState::Stopped,
179 to: RuntimeState::Idle,
180 })
181 };
182
183 assert!(result.is_err());
184 assert!(matches!(
185 result.unwrap_err(),
186 RuntimeStateTransitionError {
187 from: RuntimeState::Stopped,
188 to: RuntimeState::Idle
189 }
190 ));
191 }
192
193 #[test]
194 fn serde_roundtrip_all_states() {
195 for state in [
196 RuntimeState::Initializing,
197 RuntimeState::Idle,
198 RuntimeState::Attached,
199 RuntimeState::Running,
200 RuntimeState::Recovering,
201 RuntimeState::Retired,
202 RuntimeState::Stopped,
203 RuntimeState::Destroyed,
204 ] {
205 let json = serde_json::to_value(state).unwrap();
206 let parsed: RuntimeState = serde_json::from_value(json).unwrap();
207 assert_eq!(state, parsed);
208 }
209 }
210
211 #[test]
212 fn display() {
213 assert_eq!(RuntimeState::Idle.to_string(), "idle");
214 assert_eq!(RuntimeState::Attached.to_string(), "attached");
215 assert_eq!(RuntimeState::Running.to_string(), "running");
216 assert_eq!(RuntimeState::Destroyed.to_string(), "destroyed");
217 }
218}