Skip to main content

stateset_core/errors/
transition.rs

1//! Generic state transition error.
2//!
3//! Commerce entities move through well-defined state machines (e.g. an order goes
4//! from `Pending` → `Confirmed` → `Shipped` → `Delivered`). When an invalid
5//! transition is attempted, this type captures both the current and requested
6//! states in a type-safe way.
7
8use std::fmt;
9
10/// Error indicating an invalid state transition.
11///
12/// The type parameter `S` is the status enum (e.g. `OrderStatus`, `ReturnStatus`).
13/// This keeps transition errors strongly-typed while reusing a single generic
14/// error structure across all domain state machines.
15///
16/// # Example
17///
18/// ```rust
19/// use stateset_core::errors::StateTransitionError;
20///
21/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22/// enum Light { Red, Yellow, Green }
23///
24/// impl std::fmt::Display for Light {
25///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26///         write!(f, "{:?}", self)
27///     }
28/// }
29///
30/// let err = StateTransitionError::new(Light::Red, Light::Green);
31/// assert_eq!(err.to_string(), "invalid state transition from Red to Green");
32/// assert_eq!(err.from, Light::Red);
33/// assert_eq!(err.to, Light::Green);
34/// ```
35#[derive(Clone, Copy, PartialEq, Eq, Hash)]
36pub struct StateTransitionError<S> {
37    /// The current state.
38    pub from: S,
39    /// The requested (invalid) target state.
40    pub to: S,
41}
42
43impl<S> StateTransitionError<S> {
44    /// Create a new state transition error.
45    #[inline]
46    pub const fn new(from: S, to: S) -> Self {
47        Self { from, to }
48    }
49}
50
51impl<S: fmt::Display> fmt::Display for StateTransitionError<S> {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "invalid state transition from {} to {}", self.from, self.to)
54    }
55}
56
57impl<S: fmt::Debug> fmt::Debug for StateTransitionError<S> {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.debug_struct("StateTransitionError")
60            .field("from", &self.from)
61            .field("to", &self.to)
62            .finish()
63    }
64}
65
66impl<S: fmt::Debug + fmt::Display> std::error::Error for StateTransitionError<S> {}
67
68/// A mismatch between an expected value and the actual value found.
69///
70/// Inspired by reth's `GotExpected<T>` pattern — useful for version conflicts,
71/// quantity mismatches, and other "expected X but got Y" scenarios.
72///
73/// # Example
74///
75/// ```rust
76/// use stateset_core::errors::GotExpected;
77///
78/// let mismatch = GotExpected { got: 3, expected: 5 };
79/// assert_eq!(mismatch.to_string(), "expected 5, got 3");
80/// ```
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct GotExpected<T> {
83    /// The value that was actually found.
84    pub got: T,
85    /// The value that was expected.
86    pub expected: T,
87}
88
89impl<T> GotExpected<T> {
90    /// Create a new `GotExpected` mismatch.
91    #[inline]
92    pub const fn new(got: T, expected: T) -> Self {
93        Self { got, expected }
94    }
95}
96
97impl<T: fmt::Display> fmt::Display for GotExpected<T> {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "expected {}, got {}", self.expected, self.got)
100    }
101}
102
103impl<T: fmt::Debug + fmt::Display> std::error::Error for GotExpected<T> {}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
110    enum Status {
111        Pending,
112        Active,
113        Closed,
114    }
115
116    impl fmt::Display for Status {
117        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118            match self {
119                Self::Pending => write!(f, "pending"),
120                Self::Active => write!(f, "active"),
121                Self::Closed => write!(f, "closed"),
122            }
123        }
124    }
125
126    #[test]
127    fn transition_error_display() {
128        let err = StateTransitionError::new(Status::Pending, Status::Closed);
129        assert_eq!(err.to_string(), "invalid state transition from pending to closed");
130    }
131
132    #[test]
133    fn transition_error_debug() {
134        let err = StateTransitionError::new(Status::Active, Status::Pending);
135        let debug = format!("{:?}", err);
136        assert!(debug.contains("StateTransitionError"));
137        assert!(debug.contains("Active"));
138        assert!(debug.contains("Pending"));
139    }
140
141    #[test]
142    fn transition_error_fields() {
143        let err = StateTransitionError::new(Status::Active, Status::Closed);
144        assert_eq!(err.from, Status::Active);
145        assert_eq!(err.to, Status::Closed);
146    }
147
148    #[test]
149    fn got_expected_display() {
150        let ge = GotExpected { got: 3, expected: 5 };
151        assert_eq!(ge.to_string(), "expected 5, got 3");
152    }
153
154    #[test]
155    fn got_expected_with_strings() {
156        let ge = GotExpected { got: "foo".to_string(), expected: "bar".to_string() };
157        assert_eq!(ge.to_string(), "expected bar, got foo");
158    }
159}