1use std::fmt;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct InvalidTransition {
29 pub machine: &'static str,
31 pub from: String,
33 pub to: String,
35}
36
37impl fmt::Display for InvalidTransition {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(
40 f,
41 "invalid {} transition: '{}' \u{2192} '{}' is not permitted. \
42 Check the state machine documentation for valid transitions.",
43 self.machine, self.from, self.to
44 )
45 }
46}
47
48impl std::error::Error for InvalidTransition {}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81pub enum FlowState {
82 Created,
84 Validated,
86 Instantiated,
88 Running,
90 Draining,
93 Completed,
95 Cancelled,
97 Failed,
99}
100
101impl FlowState {
102 pub fn can_transition_to(&self, target: &FlowState) -> bool {
113 matches!(
114 (self, target),
115 (FlowState::Created, FlowState::Validated)
116 | (FlowState::Created, FlowState::Failed)
117 | (FlowState::Validated, FlowState::Instantiated)
118 | (FlowState::Validated, FlowState::Failed)
119 | (FlowState::Instantiated, FlowState::Running)
120 | (FlowState::Running, FlowState::Draining)
121 | (FlowState::Draining, FlowState::Completed)
122 | (FlowState::Draining, FlowState::Cancelled)
123 | (FlowState::Draining, FlowState::Failed)
124 )
125 }
126
127 pub fn transition_to(self, target: FlowState) -> Result<FlowState, InvalidTransition> {
134 if self.can_transition_to(&target) {
135 Ok(target)
136 } else {
137 Err(InvalidTransition {
138 machine: "FlowState",
139 from: format!("{:?}", self),
140 to: format!("{:?}", target),
141 })
142 }
143 }
144
145 #[inline]
149 pub const fn is_terminal(&self) -> bool {
150 matches!(
151 self,
152 FlowState::Completed | FlowState::Cancelled | FlowState::Failed
153 )
154 }
155
156 #[inline]
158 pub const fn is_active(&self) -> bool {
159 matches!(
160 self,
161 FlowState::Created
162 | FlowState::Validated
163 | FlowState::Instantiated
164 | FlowState::Running
165 | FlowState::Draining
166 )
167 }
168}
169
170impl fmt::Display for FlowState {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 FlowState::Created => write!(f, "Created"),
174 FlowState::Validated => write!(f, "Validated"),
175 FlowState::Instantiated => write!(f, "Instantiated"),
176 FlowState::Running => write!(f, "Running"),
177 FlowState::Draining => write!(f, "Draining"),
178 FlowState::Completed => write!(f, "Completed"),
179 FlowState::Cancelled => write!(f, "Cancelled"),
180 FlowState::Failed => write!(f, "Failed"),
181 }
182 }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213pub enum ResourceState {
214 Pooled,
216 Owned,
218 Borrowed,
220 Leased,
222 Transit,
224 Freed,
226}
227
228impl ResourceState {
229 pub fn can_transition_to(&self, target: &ResourceState) -> bool {
246 if *target == ResourceState::Freed {
248 return true;
249 }
250
251 matches!(
252 (self, target),
253 (ResourceState::Pooled, ResourceState::Owned)
254 | (ResourceState::Owned, ResourceState::Borrowed)
255 | (ResourceState::Owned, ResourceState::Leased)
256 | (ResourceState::Owned, ResourceState::Transit)
257 | (ResourceState::Owned, ResourceState::Pooled)
258 | (ResourceState::Owned, ResourceState::Freed)
259 | (ResourceState::Borrowed, ResourceState::Owned)
260 | (ResourceState::Borrowed, ResourceState::Borrowed) | (ResourceState::Leased, ResourceState::Owned)
262 | (ResourceState::Transit, ResourceState::Owned)
263 )
264 }
265
266 pub fn transition_to(self, target: ResourceState) -> Result<ResourceState, InvalidTransition> {
273 if self.can_transition_to(&target) {
274 Ok(target)
275 } else {
276 Err(InvalidTransition {
277 machine: "ResourceState",
278 from: format!("{:?}", self),
279 to: format!("{:?}", target),
280 })
281 }
282 }
283
284 #[inline]
286 pub const fn is_terminal(&self) -> bool {
287 matches!(self, ResourceState::Freed)
288 }
289
290 #[inline]
292 pub const fn is_available(&self) -> bool {
293 matches!(self, ResourceState::Pooled)
294 }
295
296 #[inline]
298 pub const fn is_active(&self) -> bool {
299 matches!(
300 self,
301 ResourceState::Owned
302 | ResourceState::Borrowed
303 | ResourceState::Leased
304 | ResourceState::Transit
305 )
306 }
307}
308
309impl fmt::Display for ResourceState {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 match self {
312 ResourceState::Pooled => write!(f, "Pooled"),
313 ResourceState::Owned => write!(f, "Owned"),
314 ResourceState::Borrowed => write!(f, "Borrowed"),
315 ResourceState::Leased => write!(f, "Leased"),
316 ResourceState::Transit => write!(f, "Transit"),
317 ResourceState::Freed => write!(f, "Freed"),
318 }
319 }
320}
321
322#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
333 fn test_flow_state_created_to_validated() {
334 assert!(FlowState::Created.can_transition_to(&FlowState::Validated));
335 assert!(FlowState::Created
336 .transition_to(FlowState::Validated)
337 .is_ok());
338 }
339
340 #[test]
341 fn test_flow_state_created_to_failed() {
342 assert!(FlowState::Created.can_transition_to(&FlowState::Failed));
343 }
344
345 #[test]
346 fn test_flow_state_validated_to_instantiated() {
347 assert!(FlowState::Validated.can_transition_to(&FlowState::Instantiated));
348 }
349
350 #[test]
351 fn test_flow_state_validated_to_failed() {
352 assert!(FlowState::Validated.can_transition_to(&FlowState::Failed));
353 }
354
355 #[test]
356 fn test_flow_state_instantiated_to_running() {
357 assert!(FlowState::Instantiated.can_transition_to(&FlowState::Running));
358 }
359
360 #[test]
361 fn test_flow_state_running_to_draining() {
362 assert!(FlowState::Running.can_transition_to(&FlowState::Draining));
363 }
364
365 #[test]
366 fn test_flow_state_draining_to_completed() {
367 assert!(FlowState::Draining.can_transition_to(&FlowState::Completed));
368 }
369
370 #[test]
371 fn test_flow_state_draining_to_cancelled() {
372 assert!(FlowState::Draining.can_transition_to(&FlowState::Cancelled));
373 }
374
375 #[test]
376 fn test_flow_state_draining_to_failed() {
377 assert!(FlowState::Draining.can_transition_to(&FlowState::Failed));
378 }
379
380 #[test]
383 fn test_flow_state_created_to_running_invalid() {
384 assert!(!FlowState::Created.can_transition_to(&FlowState::Running));
385 let result = FlowState::Created.transition_to(FlowState::Running);
386 assert!(result.is_err());
387 let err = result.unwrap_err();
388 assert_eq!(err.machine, "FlowState");
389 assert_eq!(err.from, "Created");
390 assert_eq!(err.to, "Running");
391 }
392
393 #[test]
394 fn test_flow_state_running_to_completed_invalid() {
395 assert!(!FlowState::Running.can_transition_to(&FlowState::Completed));
397 }
398
399 #[test]
400 fn test_flow_state_completed_to_running_invalid() {
401 assert!(!FlowState::Completed.can_transition_to(&FlowState::Running));
403 }
404
405 #[test]
406 fn test_flow_state_failed_is_terminal() {
407 assert!(FlowState::Failed.is_terminal());
408 assert!(!FlowState::Failed.can_transition_to(&FlowState::Created));
409 assert!(!FlowState::Failed.can_transition_to(&FlowState::Running));
410 }
411
412 #[test]
413 fn test_flow_state_cancelled_is_terminal() {
414 assert!(FlowState::Cancelled.is_terminal());
415 }
416
417 #[test]
418 fn test_flow_state_instantiated_to_draining_invalid() {
419 assert!(!FlowState::Instantiated.can_transition_to(&FlowState::Draining));
421 }
422
423 #[test]
424 fn test_flow_state_is_active() {
425 assert!(FlowState::Created.is_active());
426 assert!(FlowState::Running.is_active());
427 assert!(FlowState::Draining.is_active());
428 assert!(!FlowState::Completed.is_active());
429 assert!(!FlowState::Failed.is_active());
430 }
431
432 #[test]
435 fn test_flow_state_complete_valid_transition_count() {
436 let states = [
438 FlowState::Created,
439 FlowState::Validated,
440 FlowState::Instantiated,
441 FlowState::Running,
442 FlowState::Draining,
443 FlowState::Completed,
444 FlowState::Cancelled,
445 FlowState::Failed,
446 ];
447 let mut valid_count = 0;
448 for from in &states {
449 for to in &states {
450 if from.can_transition_to(to) {
451 valid_count += 1;
452 }
453 }
454 }
455 assert_eq!(
456 valid_count, 9,
457 "expected exactly 9 valid FlowState transitions"
458 );
459 }
460
461 #[test]
464 fn test_resource_state_pooled_to_owned() {
465 assert!(ResourceState::Pooled.can_transition_to(&ResourceState::Owned));
466 assert!(ResourceState::Pooled
467 .transition_to(ResourceState::Owned)
468 .is_ok());
469 }
470
471 #[test]
472 fn test_resource_state_owned_to_borrowed() {
473 assert!(ResourceState::Owned.can_transition_to(&ResourceState::Borrowed));
474 }
475
476 #[test]
477 fn test_resource_state_owned_to_leased() {
478 assert!(ResourceState::Owned.can_transition_to(&ResourceState::Leased));
479 }
480
481 #[test]
482 fn test_resource_state_owned_to_transit() {
483 assert!(ResourceState::Owned.can_transition_to(&ResourceState::Transit));
484 }
485
486 #[test]
487 fn test_resource_state_owned_to_pooled() {
488 assert!(ResourceState::Owned.can_transition_to(&ResourceState::Pooled));
489 }
490
491 #[test]
492 fn test_resource_state_borrowed_to_owned() {
493 assert!(ResourceState::Borrowed.can_transition_to(&ResourceState::Owned));
494 }
495
496 #[test]
497 fn test_resource_state_borrowed_to_borrowed() {
498 assert!(ResourceState::Borrowed.can_transition_to(&ResourceState::Borrowed));
500 }
501
502 #[test]
503 fn test_resource_state_leased_to_owned() {
504 assert!(ResourceState::Leased.can_transition_to(&ResourceState::Owned));
505 }
506
507 #[test]
508 fn test_resource_state_transit_to_owned() {
509 assert!(ResourceState::Transit.can_transition_to(&ResourceState::Owned));
510 }
511
512 #[test]
513 fn test_resource_state_any_to_freed() {
514 let states = [
515 ResourceState::Pooled,
516 ResourceState::Owned,
517 ResourceState::Borrowed,
518 ResourceState::Leased,
519 ResourceState::Transit,
520 ResourceState::Freed,
521 ];
522 for state in &states {
523 assert!(
524 state.can_transition_to(&ResourceState::Freed),
525 "{:?} should be able to transition to Freed",
526 state
527 );
528 }
529 }
530
531 #[test]
534 fn test_resource_state_pooled_to_borrowed_invalid() {
535 assert!(!ResourceState::Pooled.can_transition_to(&ResourceState::Borrowed));
536 }
537
538 #[test]
539 fn test_resource_state_pooled_to_leased_invalid() {
540 assert!(!ResourceState::Pooled.can_transition_to(&ResourceState::Leased));
541 }
542
543 #[test]
544 fn test_resource_state_borrowed_to_pooled_invalid() {
545 assert!(!ResourceState::Borrowed.can_transition_to(&ResourceState::Pooled));
547 }
548
549 #[test]
550 fn test_resource_state_borrowed_to_transit_invalid() {
551 assert!(!ResourceState::Borrowed.can_transition_to(&ResourceState::Transit));
553 }
554
555 #[test]
556 fn test_resource_state_leased_to_pooled_invalid() {
557 assert!(!ResourceState::Leased.can_transition_to(&ResourceState::Pooled));
559 }
560
561 #[test]
562 fn test_resource_state_transit_to_pooled_invalid() {
563 assert!(!ResourceState::Transit.can_transition_to(&ResourceState::Pooled));
565 }
566
567 #[test]
568 fn test_resource_state_freed_is_terminal() {
569 assert!(ResourceState::Freed.is_terminal());
570 assert!(!ResourceState::Freed.can_transition_to(&ResourceState::Pooled));
572 assert!(!ResourceState::Freed.can_transition_to(&ResourceState::Owned));
573 }
574
575 #[test]
576 fn test_resource_state_is_available() {
577 assert!(ResourceState::Pooled.is_available());
578 assert!(!ResourceState::Owned.is_available());
579 assert!(!ResourceState::Freed.is_available());
580 }
581
582 #[test]
583 fn test_resource_state_is_active() {
584 assert!(!ResourceState::Pooled.is_active());
585 assert!(ResourceState::Owned.is_active());
586 assert!(ResourceState::Borrowed.is_active());
587 assert!(ResourceState::Leased.is_active());
588 assert!(ResourceState::Transit.is_active());
589 assert!(!ResourceState::Freed.is_active());
590 }
591
592 #[test]
595 fn test_invalid_transition_display_is_actionable() {
596 let err = InvalidTransition {
597 machine: "FlowState",
598 from: "Running".into(),
599 to: "Created".into(),
600 };
601 let msg = format!("{err}");
602 assert!(msg.contains("FlowState"));
603 assert!(msg.contains("Running"));
604 assert!(msg.contains("Created"));
605 assert!(msg.contains("not permitted"));
606 }
607}