1mod run_fsm;
15mod step_fsm;
16
17pub use run_fsm::{RunEvent, RunFsm};
18pub use step_fsm::{StepEvent, StepFsm};
19
20use std::fmt;
21
22use chrono::{DateTime, Utc};
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct Transition<S, E> {
44 pub from: S,
46 pub to: S,
48 pub event: E,
50 pub at: DateTime<Utc>,
52}
53
54#[derive(Debug, Clone)]
68pub struct TransitionError<S: fmt::Display, E: fmt::Display> {
69 pub from: S,
71 pub event: E,
73}
74
75impl<S: fmt::Display, E: fmt::Display> fmt::Display for TransitionError<S, E> {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(
78 f,
79 "invalid transition: event '{}' not allowed in state '{}'",
80 self.event, self.from
81 )
82 }
83}
84
85impl<S: fmt::Debug + fmt::Display, E: fmt::Debug + fmt::Display> std::error::Error
86 for TransitionError<S, E>
87{
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use chrono::Utc;
94
95 #[test]
96 fn transition_captures_state_change() {
97 let now = Utc::now();
98 let t: Transition<String, String> = Transition {
99 from: "Pending".to_string(),
100 to: "Running".to_string(),
101 event: "picked_up".to_string(),
102 at: now,
103 };
104
105 assert_eq!(t.from, "Pending");
106 assert_eq!(t.to, "Running");
107 assert_eq!(t.event, "picked_up");
108 assert_eq!(t.at, now);
109 }
110
111 #[test]
112 fn transition_with_different_types() {
113 let now = Utc::now();
114 #[derive(Debug, Clone, PartialEq)]
115 enum State {
116 Pending,
117 Running,
118 Completed,
119 }
120
121 #[derive(Debug, Clone)]
122 enum Event {
123 Started,
124 Finished,
125 }
126
127 let t = Transition {
128 from: State::Pending,
129 to: State::Running,
130 event: Event::Started,
131 at: now,
132 };
133
134 assert_eq!(t.from, State::Pending);
135 assert_eq!(t.to, State::Running);
136
137 let t2 = Transition {
138 from: State::Running,
139 to: State::Completed,
140 event: Event::Finished,
141 at: now,
142 };
143
144 assert_eq!(t2.from, State::Running);
145 assert_eq!(t2.to, State::Completed);
146 }
147
148 #[test]
149 fn transition_serializes_to_json() {
150 let now = Utc::now();
151 let t: Transition<String, String> = Transition {
152 from: "A".to_string(),
153 to: "B".to_string(),
154 event: "go".to_string(),
155 at: now,
156 };
157
158 let json = serde_json::to_value(&t).expect("serialize");
159 assert_eq!(json["from"], "A");
160 assert_eq!(json["to"], "B");
161 assert_eq!(json["event"], "go");
162 }
163
164 #[test]
165 fn transition_deserializes_from_json() {
166 let json_str =
167 r#"{"from":"pending","to":"running","event":"picked_up","at":"2025-01-01T00:00:00Z"}"#;
168 let t: Transition<String, String> = serde_json::from_str(json_str).expect("deserialize");
169
170 assert_eq!(t.from, "pending");
171 assert_eq!(t.to, "running");
172 assert_eq!(t.event, "picked_up");
173 }
174
175 #[test]
176 fn transition_error_formats_message() {
177 let err: TransitionError<String, String> = TransitionError {
178 from: "Completed".to_string(),
179 event: "picked_up".to_string(),
180 };
181
182 let msg = err.to_string();
183 assert!(msg.contains("Completed"));
184 assert!(msg.contains("picked_up"));
185 assert!(msg.contains("invalid transition"));
186 }
187
188 #[test]
189 fn transition_error_message_is_informative() {
190 let err: TransitionError<&str, &str> = TransitionError {
191 from: "Failed",
192 event: "retry",
193 };
194
195 let msg = err.to_string();
196 assert!(msg.contains("Failed"));
197 assert!(msg.contains("retry"));
198 }
199
200 #[test]
201 fn transition_error_implements_error_trait() {
202 use std::error::Error;
203
204 let err: TransitionError<String, String> = TransitionError {
205 from: "Running".to_string(),
206 event: "start".to_string(),
207 };
208
209 let error_ref: &dyn Error = &err;
210 assert!(!error_ref.to_string().is_empty());
211 }
212
213 #[test]
214 fn transition_error_clone() {
215 let err = TransitionError {
216 from: "Pending".to_string(),
217 event: "resume".to_string(),
218 };
219
220 let cloned = err.clone();
221 assert_eq!(cloned.from, err.from);
222 assert_eq!(cloned.event, err.event);
223 }
224
225 #[test]
226 fn transition_clone_preserves_all_fields() {
227 let now = Utc::now();
228 let t1 = Transition {
229 from: "A".to_string(),
230 to: "B".to_string(),
231 event: "E".to_string(),
232 at: now,
233 };
234
235 let t2 = t1.clone();
236 assert_eq!(t1, t2);
237 assert_eq!(t1.at, t2.at);
238 }
239
240 #[test]
241 fn transition_debug_output_contains_fields() {
242 let t: Transition<String, String> = Transition {
243 from: "x".to_string(),
244 to: "y".to_string(),
245 event: "z".to_string(),
246 at: Utc::now(),
247 };
248
249 let debug = format!("{:?}", t);
250 assert!(debug.contains("x"));
251 assert!(debug.contains("y"));
252 assert!(debug.contains("z"));
253 }
254}