1#[doc(hidden)]
6pub use paste::paste;
7
8#[macro_export]
96macro_rules! create_state_machine {
97 (
98 @impl
99 [$cfg:meta]
100 [$(#[$($machine_attrs:tt)*])*]
101 $vis:vis $machine:ident {
102 states: [ $( $state:ident ),+ $(,)? ],
103 default: $default:ident,
104 transitions: [
105 $(
106 $action:ident: [ $( $from:ident ),+ $(,)? ] => $to:ident
107 ),+ $(,)?
108 ],
109 }
110 ) => {
111 $crate::state_machine::paste! {
112 #[cfg($cfg)]
113 $(#[$($machine_attrs)*])*
114 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
115 $vis enum $machine {
116 $( $state, )+
117 }
118
119 #[cfg($cfg)]
120 impl ::std::default::Default for $machine {
121 fn default() -> Self {
122 Self::$default
123 }
124 }
125
126 #[cfg($cfg)]
127 impl ::std::fmt::Display for $machine {
128 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
129 f.write_str(self.name())
130 }
131 }
132
133 #[cfg($cfg)]
134 #[allow(dead_code)]
135 impl $machine {
136 $vis const STATES: &'static [&'static str] = &[
137 $( stringify!($state), )+
138 ];
139
140 $vis const TRANSITIONS: &'static [(&'static str, &'static str)] = &[
141 $(
142 $( (stringify!($from), stringify!($to)), )+
143 )+
144 ];
145
146 #[must_use]
147 $vis fn name(self) -> &'static str {
148 match self {
149 $( Self::$state => stringify!($state), )+
150 }
151 }
152
153 #[must_use]
154 $vis fn states() -> &'static [&'static str] {
155 Self::STATES
156 }
157
158 #[must_use]
159 $vis fn transitions() -> &'static [(&'static str, &'static str)] {
160 Self::TRANSITIONS
161 }
162
163 #[must_use]
164 #[allow(unreachable_patterns)]
165 $vis fn transition_allowed(from: Self, to: Self) -> bool {
166 matches!(
167 (from, to),
168 $(
169 $( (Self::$from, Self::$to) )|+
170 )|+
171 )
172 }
173
174 $vis fn apply<TTransition>(&mut self) -> bool
175 where
176 TTransition: [< $machine Transition >],
177 {
178 if !TTransition::FROM.contains(self)
179 || !Self::transition_allowed(*self, TTransition::TO)
180 {
181 return false;
182 }
183
184 *self = TTransition::TO;
185 true
186 }
187
188 #[must_use]
189 $vis fn as_str(self) -> &'static str {
190 self.name()
191 }
192
193 #[must_use]
194 $vis fn mermaid() -> ::std::string::String {
195 let mut diagram = ::std::string::String::from("stateDiagram-v2\n");
196 diagram.push_str(" [*] --> ");
197 diagram.push_str(stringify!($default));
198 diagram.push('\n');
199
200 $(
201 $(
202 diagram.push_str(" ");
203 diagram.push_str(stringify!($from));
204 diagram.push_str(" --> ");
205 diagram.push_str(stringify!($to));
206 diagram.push_str(": ");
207 diagram.push_str(stringify!($action));
208 diagram.push('\n');
209 )+
210 )+
211
212 diagram
213 }
214 }
215
216 $(
217 #[cfg($cfg)]
218 #[allow(non_camel_case_types)]
219 $vis struct [< $machine $action:camel Transition >];
220 )+
221
222 #[cfg($cfg)]
223 mod [< $machine:snake _transition_seal >] {
224 pub trait Sealed {}
225 }
226
227 #[cfg($cfg)]
228 #[allow(private_bounds)]
229 $vis trait [< $machine Transition >]: [< $machine:snake _transition_seal >]::Sealed {
230 const FROM: &'static [$machine];
231 const TO: $machine;
232 }
233
234 $(
235 #[cfg($cfg)]
236 impl [< $machine:snake _transition_seal >]::Sealed
237 for [< $machine $action:camel Transition >]
238 {
239 }
240
241 #[cfg($cfg)]
242 impl [< $machine Transition >] for [< $machine $action:camel Transition >] {
243 const FROM: &'static [$machine] = &[
244 $( $machine::$from, )+
245 ];
246 const TO: $machine = $machine::$to;
247 }
248 )+
249
250 $(
251 #[cfg($cfg)]
252 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
253 $vis struct [< $machine $state >];
254 )+
255
256 #[cfg($cfg)]
257 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
258 $vis struct [< $machine Typestate >]<S> {
259 state: S,
260 }
261
262 #[cfg($cfg)]
263 #[allow(dead_code)]
264 impl<S> [< $machine Typestate >]<S> {
265 #[must_use]
266 $vis const fn new(state: S) -> Self {
267 Self {
268 state,
269 }
270 }
271
272 #[must_use]
273 $vis fn state(&self) -> &S {
274 &self.state
275 }
276
277 #[must_use]
278 $vis fn into_state(self) -> S {
279 self.state
280 }
281 }
282
283 #[cfg($cfg)]
284 impl ::std::default::Default for [< $machine Typestate >]<[< $machine $default >]> {
285 fn default() -> Self {
286 Self::new([< $machine $default >])
287 }
288 }
289
290 #[cfg($cfg)]
291 const _: () = {
292 $(
293 let _ = [< $machine $state >];
294 let _ = [< $machine Typestate >]::new([< $machine $state >]);
295 )+
296
297 $(
298 let _ = [< $machine $action:camel Transition >];
299 )+
300 };
301
302 $(
303 $(
304 #[cfg($cfg)]
305 #[allow(dead_code, non_snake_case)]
306 impl [< $machine Typestate >]<[< $machine $from >]> {
307 #[must_use]
308 $vis fn $action(self) -> [< $machine Typestate >]<[< $machine $to >]> {
309 [< $machine Typestate >]::new([< $machine $to >])
310 }
311 }
312 )+
313 )+
314 }
315 };
316
317 (
318 @parse
319 [$($cfgs:meta),*]
320 [$($machine_attrs:tt)*]
321 #[cfg($cfg:meta)]
322 $($rest:tt)*
323 ) => {
324 $crate::create_state_machine!(
325 @parse
326 [$($cfgs,)* $cfg]
327 [$($machine_attrs)*]
328 $($rest)*
329 );
330 };
331
332 (
333 @parse
334 [$($cfgs:meta),*]
335 [$($machine_attrs:tt)*]
336 #[cfg_attr($($cfg_attr:tt)*)]
337 $($rest:tt)*
338 ) => {
339 ::std::compile_error!(
340 "`create_state_machine!` does not support `cfg_attr`; use explicit `cfg` attributes instead"
341 );
342 };
343
344 (
345 @parse
346 [$($cfgs:meta),*]
347 [$($machine_attrs:tt)*]
348 #[$($machine_attr:tt)*]
349 $($rest:tt)*
350 ) => {
351 $crate::create_state_machine!(
352 @parse
353 [$($cfgs),*]
354 [$($machine_attrs)* #[$($machine_attr)*]]
355 $($rest)*
356 );
357 };
358
359 (
360 @parse
361 [$($cfgs:meta),*]
362 [$($machine_attrs:tt)*]
363 $vis:vis $machine:ident {
364 states: [ $( $state:ident ),+ $(,)? ],
365 default: $default:ident,
366 transitions: [
367 $(
368 $action:ident: [ $( $from:ident ),+ $(,)? ] => $to:ident
369 ),+ $(,)?
370 ],
371 }
372 ) => {
373 $crate::create_state_machine!(
374 @impl
375 [all($($cfgs),*)]
376 [$($machine_attrs)*]
377 $vis $machine {
378 states: [ $( $state ),+ ],
379 default: $default,
380 transitions: [
381 $(
382 $action: [ $( $from ),+ ] => $to
383 ),+
384 ],
385 }
386 );
387 };
388
389 ($($input:tt)*) => {
390 $crate::create_state_machine!(@parse [] [] $($input)*);
391 };
392}
393
394#[cfg(test)]
395mod tests {
396 create_state_machine!(
397 TestMachine {
398 states: [Idle, Busy, Fault],
399 default: Idle,
400 transitions: [
401 start: [Idle] => Busy,
402 fail: [Idle, Busy] => Fault,
403 finish: [Busy] => Idle,
404 reset: [Fault] => Idle,
405 ],
406 }
407 );
408
409 create_state_machine!(
410 pub(crate) DoorMachine {
411 states: [Closed, Open, Locked],
412 default: Closed,
413 transitions: [
414 open: [Closed] => Open,
415 close: [Open] => Closed,
416 lock: [Closed] => Locked,
417 reset: [Open, Locked] => Closed,
418 ],
419 }
420 );
421
422 create_state_machine!(
423 pub(crate) DeviceMachine {
424 states: [Reset, Ready],
425 default: Reset,
426 transitions: [
427 reset: [Ready] => Reset,
428 ready: [Reset] => Ready,
429 ],
430 }
431 );
432
433 #[test]
434 fn generated_state_machine_exports_metadata() {
435 assert_eq!(TestMachine::states(), &["Idle", "Busy", "Fault"]);
436 assert_eq!(TestMachine::default(), TestMachine::Idle);
437 assert_eq!(TestMachine::Busy.name(), "Busy");
438 assert_eq!(TestMachine::Fault.as_str(), "Fault");
439 assert_eq!(TestMachine::Idle.to_string(), "Idle");
440 assert_eq!(
441 TestMachine::transitions(),
442 &[
443 ("Idle", "Busy"),
444 ("Idle", "Fault"),
445 ("Busy", "Fault"),
446 ("Busy", "Idle"),
447 ("Fault", "Idle"),
448 ]
449 );
450 }
451
452 #[test]
453 fn generated_state_machine_checks_transition_matrix() {
454 assert!(TestMachine::transition_allowed(
455 TestMachine::Idle,
456 TestMachine::Busy
457 ));
458 assert!(TestMachine::transition_allowed(
459 TestMachine::Idle,
460 TestMachine::Fault
461 ));
462 assert!(TestMachine::transition_allowed(
463 TestMachine::Busy,
464 TestMachine::Fault
465 ));
466 assert!(TestMachine::transition_allowed(
467 TestMachine::Busy,
468 TestMachine::Idle
469 ));
470 assert!(TestMachine::transition_allowed(
471 TestMachine::Fault,
472 TestMachine::Idle
473 ));
474
475 assert!(!TestMachine::transition_allowed(
476 TestMachine::Fault,
477 TestMachine::Busy
478 ));
479 assert!(!TestMachine::transition_allowed(
480 TestMachine::Idle,
481 TestMachine::Idle
482 ));
483 assert!(!TestMachine::transition_allowed(
484 TestMachine::Busy,
485 TestMachine::Busy
486 ));
487 }
488
489 #[test]
490 fn generated_transition_markers_support_runtime_state_updates() {
491 let mut state = TestMachine::Idle;
492
493 assert!(!state.apply::<TestMachineFinishTransition>());
494 assert_eq!(state, TestMachine::Idle);
495
496 assert!(state.apply::<TestMachineStartTransition>());
497 assert_eq!(state, TestMachine::Busy);
498
499 assert!(state.apply::<TestMachineFailTransition>());
500 assert_eq!(state, TestMachine::Fault);
501
502 assert!(state.apply::<TestMachineResetTransition>());
503 assert_eq!(state, TestMachine::Idle);
504 }
505
506 #[test]
507 fn generated_transition_markers_export_source_and_target_metadata() {
508 assert_eq!(TestMachineStartTransition::FROM, &[TestMachine::Idle]);
509 assert_eq!(TestMachineStartTransition::TO, TestMachine::Busy);
510
511 assert_eq!(
512 TestMachineFailTransition::FROM,
513 &[TestMachine::Idle, TestMachine::Busy]
514 );
515 assert_eq!(TestMachineFailTransition::TO, TestMachine::Fault);
516 }
517
518 #[test]
519 fn generated_typestate_helpers_chain_valid_transitions() {
520 fn expect_idle(_: TestMachineTypestate<TestMachineIdle>) {}
521 fn expect_busy(_: TestMachineTypestate<TestMachineBusy>) {}
522 fn expect_fault(_: TestMachineTypestate<TestMachineFault>) {}
523
524 let idle = TestMachineTypestate::default();
525 assert_eq!(*idle.state(), TestMachineIdle);
526
527 let busy = idle.start();
528 expect_busy(busy);
529
530 let busy = TestMachineTypestate::new(TestMachineBusy);
531 let fault = busy.fail();
532 expect_fault(fault);
533
534 let fault = TestMachineTypestate::new(TestMachineFault);
535 let idle = fault.reset();
536 assert_eq!(idle.into_state(), TestMachineIdle);
537 expect_idle(idle);
538 }
539
540 #[test]
541 fn generated_names_do_not_collide_when_events_repeat_across_machines() {
542 assert_eq!(
543 DoorMachineResetTransition::FROM,
544 &[DoorMachine::Open, DoorMachine::Locked]
545 );
546 assert_eq!(DoorMachineResetTransition::TO, DoorMachine::Closed);
547 assert_eq!(DeviceMachineResetTransition::FROM, &[DeviceMachine::Ready]);
548 assert_eq!(DeviceMachineResetTransition::TO, DeviceMachine::Reset);
549 assert!(DeviceMachine::transition_allowed(
550 DeviceMachine::Ready,
551 DeviceMachine::Reset
552 ));
553 }
554
555 #[test]
556 fn generated_public_visibility_exports_machine_api() {
557 assert_eq!(
558 DoorMachine::transitions(),
559 &[
560 ("Closed", "Open"),
561 ("Open", "Closed"),
562 ("Closed", "Locked"),
563 ("Open", "Closed"),
564 ("Locked", "Closed"),
565 ]
566 );
567 assert_eq!(DoorMachine::default(), DoorMachine::Closed);
568 assert_eq!(DoorMachine::Locked.to_string(), "Locked");
569 }
570
571 #[test]
572 fn generated_mermaid_diagram_uses_default_state_and_action_labels() {
573 assert_eq!(
574 TestMachine::mermaid(),
575 concat!(
576 "stateDiagram-v2\n",
577 " [*] --> Idle\n",
578 " Idle --> Busy: start\n",
579 " Idle --> Fault: fail\n",
580 " Busy --> Fault: fail\n",
581 " Busy --> Idle: finish\n",
582 " Fault --> Idle: reset\n",
583 )
584 );
585 }
586
587 #[test]
588 fn generated_mermaid_diagram_expands_shared_events_in_declaration_order() {
589 assert_eq!(
590 DoorMachine::mermaid(),
591 concat!(
592 "stateDiagram-v2\n",
593 " [*] --> Closed\n",
594 " Closed --> Open: open\n",
595 " Open --> Closed: close\n",
596 " Closed --> Locked: lock\n",
597 " Open --> Closed: reset\n",
598 " Locked --> Closed: reset\n",
599 )
600 );
601 }
602}