torvyn_types/
state.rs

1//! State machine types for flow lifecycle and resource ownership.
2//!
3//! Both [`FlowState`] and [`ResourceState`] include transition validation
4//! methods that enforce the legal transitions defined in the HLI documents.
5
6use std::fmt;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11// ---------------------------------------------------------------------------
12// InvalidTransition
13// ---------------------------------------------------------------------------
14
15/// Error returned when an illegal state transition is attempted.
16///
17/// # Examples
18/// ```
19/// use torvyn_types::{FlowState, InvalidTransition};
20///
21/// let result = FlowState::Running.transition_to(FlowState::Created);
22/// assert!(result.is_err());
23/// let err = result.unwrap_err();
24/// assert!(format!("{}", err).contains("Running"));
25/// assert!(format!("{}", err).contains("Created"));
26/// ```
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct InvalidTransition {
29    /// The state machine type (e.g., "FlowState", "ResourceState").
30    pub machine: &'static str,
31    /// The current state.
32    pub from: String,
33    /// The attempted target state.
34    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// ---------------------------------------------------------------------------
51// FlowState
52// ---------------------------------------------------------------------------
53
54/// Flow lifecycle state machine.
55///
56/// Per Doc 04, Section 10.1: 8 states with defined legal transitions.
57/// This is the label-only version in `torvyn-types`. The reactor crate
58/// provides an extended version with associated data (stats, error info).
59///
60/// State transition diagram:
61/// ```text
62///   Created -> Validated -> Instantiated -> Running -> Draining -> Completed
63///   Created -> Failed                                            -> Cancelled
64///   Validated -> Failed                                          -> Failed
65///   Running -> Draining -> Failed
66/// ```
67///
68/// # Examples
69/// ```
70/// use torvyn_types::FlowState;
71///
72/// let state = FlowState::Created;
73/// assert!(state.can_transition_to(&FlowState::Validated));
74/// assert!(!state.can_transition_to(&FlowState::Running));
75///
76/// let new_state = state.transition_to(FlowState::Validated).unwrap();
77/// assert_eq!(new_state, FlowState::Validated);
78/// ```
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81pub enum FlowState {
82    /// Flow definition has been submitted but not yet validated.
83    Created,
84    /// Contracts and capabilities have been validated.
85    Validated,
86    /// Components have been instantiated and streams are connected.
87    Instantiated,
88    /// The flow is actively processing stream elements.
89    Running,
90    /// The flow is draining remaining elements after a completion
91    /// or cancellation signal.
92    Draining,
93    /// The flow completed successfully.
94    Completed,
95    /// The flow was cancelled by operator or policy.
96    Cancelled,
97    /// The flow failed due to an unrecoverable error.
98    Failed,
99}
100
101impl FlowState {
102    /// Returns `true` if transitioning from `self` to `target` is legal.
103    ///
104    /// Legal transitions (per Doc 04, Section 10.2):
105    /// - Created -> Validated | Failed
106    /// - Validated -> Instantiated | Failed
107    /// - Instantiated -> Running
108    /// - Running -> Draining
109    /// - Draining -> Completed | Cancelled | Failed
110    ///
111    /// # WARM PATH — called per flow state change.
112    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    /// Attempt to transition from `self` to `target`.
128    ///
129    /// Returns `Ok(target)` if the transition is legal, or
130    /// `Err(InvalidTransition)` if it is not.
131    ///
132    /// # WARM PATH — called per flow state change.
133    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    /// Returns `true` if this state is terminal (no further transitions possible).
146    ///
147    /// # WARM PATH — checked for flow cleanup.
148    #[inline]
149    pub const fn is_terminal(&self) -> bool {
150        matches!(
151            self,
152            FlowState::Completed | FlowState::Cancelled | FlowState::Failed
153        )
154    }
155
156    /// Returns `true` if this state is active (Created through Running).
157    #[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// ---------------------------------------------------------------------------
186// ResourceState
187// ---------------------------------------------------------------------------
188
189/// Resource ownership state machine.
190///
191/// Per Doc 03, Section 3.1-3.2: tracks the lifecycle of host-managed resources
192/// (primarily buffers). Extended here with `Transit` and `Freed` states.
193///
194/// State transition diagram:
195/// ```text
196///   Pooled -> Owned -> Borrowed -> Owned (borrow released)
197///                   -> Leased   -> Owned (lease expired)
198///                   -> Transit  -> Owned (new owner)
199///                   -> Pooled   (released)
200///   Any -> Freed (forced cleanup or shutdown)
201/// ```
202///
203/// # Examples
204/// ```
205/// use torvyn_types::ResourceState;
206///
207/// let state = ResourceState::Pooled;
208/// assert!(state.can_transition_to(&ResourceState::Owned));
209/// assert!(!state.can_transition_to(&ResourceState::Borrowed));
210/// ```
211#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213pub enum ResourceState {
214    /// The resource is in a buffer pool, not in active use.
215    Pooled,
216    /// The resource is exclusively owned by one entity.
217    Owned,
218    /// The resource is owned but has outstanding read-only borrows.
219    Borrowed,
220    /// The resource is held under a time- or scope-bounded lease.
221    Leased,
222    /// The resource is in transit between owners (host holds temporarily).
223    Transit,
224    /// The resource has been freed and its slot may be reused.
225    Freed,
226}
227
228impl ResourceState {
229    /// Returns `true` if transitioning from `self` to `target` is legal.
230    ///
231    /// Legal transitions (per Doc 03, Section 3.2, extended):
232    /// - Pooled -> Owned (allocate)
233    /// - Owned -> Borrowed (borrow started)
234    /// - Owned -> Leased (lease granted)
235    /// - Owned -> Transit (transfer initiated)
236    /// - Owned -> Pooled (released to pool)
237    /// - Owned -> Freed (deallocated)
238    /// - Borrowed -> Owned (all borrows released)
239    /// - Borrowed -> Borrowed (additional borrow — same state)
240    /// - Leased -> Owned (lease expired/released)
241    /// - Transit -> Owned (transfer completed to new owner)
242    /// - Any -> Freed (forced cleanup: crash, shutdown)
243    ///
244    /// # HOT PATH — called per resource state change.
245    pub fn can_transition_to(&self, target: &ResourceState) -> bool {
246        // Any state can transition to Freed (forced cleanup)
247        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) // additional borrow
261            | (ResourceState::Leased, ResourceState::Owned)
262            | (ResourceState::Transit, ResourceState::Owned)
263        )
264    }
265
266    /// Attempt to transition from `self` to `target`.
267    ///
268    /// Returns `Ok(target)` if the transition is legal, or
269    /// `Err(InvalidTransition)` if it is not.
270    ///
271    /// # HOT PATH — called per resource state change.
272    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    /// Returns `true` if this state is terminal.
285    #[inline]
286    pub const fn is_terminal(&self) -> bool {
287        matches!(self, ResourceState::Freed)
288    }
289
290    /// Returns `true` if the resource is available for allocation from a pool.
291    #[inline]
292    pub const fn is_available(&self) -> bool {
293        matches!(self, ResourceState::Pooled)
294    }
295
296    /// Returns `true` if the resource has an active owner.
297    #[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// ---------------------------------------------------------------------------
323// Tests
324// ---------------------------------------------------------------------------
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    // === FlowState valid transitions ===
331
332    #[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    // === FlowState invalid transitions ===
381
382    #[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        // Must go through Draining first
396        assert!(!FlowState::Running.can_transition_to(&FlowState::Completed));
397    }
398
399    #[test]
400    fn test_flow_state_completed_to_running_invalid() {
401        // Terminal state — no transitions out
402        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        // Must go through Running first
420        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    // === FlowState complete transition matrix ===
433
434    #[test]
435    fn test_flow_state_complete_valid_transition_count() {
436        // There are exactly 9 valid transitions
437        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    // === ResourceState valid transitions ===
462
463    #[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        // Additional borrows stay in Borrowed state
499        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    // === ResourceState invalid transitions ===
532
533    #[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        // Must return to Owned first
546        assert!(!ResourceState::Borrowed.can_transition_to(&ResourceState::Pooled));
547    }
548
549    #[test]
550    fn test_resource_state_borrowed_to_transit_invalid() {
551        // Cannot transfer while borrows outstanding
552        assert!(!ResourceState::Borrowed.can_transition_to(&ResourceState::Transit));
553    }
554
555    #[test]
556    fn test_resource_state_leased_to_pooled_invalid() {
557        // Must return to Owned first
558        assert!(!ResourceState::Leased.can_transition_to(&ResourceState::Pooled));
559    }
560
561    #[test]
562    fn test_resource_state_transit_to_pooled_invalid() {
563        // Must become Owned by new entity first
564        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        // Freed can only go to Freed
571        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    // === InvalidTransition ===
593
594    #[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}