1pub trait MachineIntrospection {
3 type StateId: Copy + Eq + core::hash::Hash + 'static;
5
6 type TransitionId: Copy + Eq + core::hash::Hash + 'static;
8
9 const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId>;
11}
12
13#[derive(Clone, Copy)]
16pub struct TransitionInventory<S: 'static, T: 'static> {
17 get: fn() -> &'static [TransitionDescriptor<S, T>],
18}
19
20impl<S, T> TransitionInventory<S, T> {
21 pub const fn new(get: fn() -> &'static [TransitionDescriptor<S, T>]) -> Self {
23 Self { get }
24 }
25
26 pub fn as_slice(&self) -> &'static [TransitionDescriptor<S, T>] {
28 (self.get)()
29 }
30}
31
32impl<S, T> core::ops::Deref for TransitionInventory<S, T> {
33 type Target = [TransitionDescriptor<S, T>];
34
35 fn deref(&self) -> &Self::Target {
36 self.as_slice()
37 }
38}
39
40impl<S, T> core::fmt::Debug for TransitionInventory<S, T> {
41 fn fmt(
42 &self,
43 formatter: &mut core::fmt::Formatter<'_>,
44 ) -> core::result::Result<(), core::fmt::Error> {
45 formatter.debug_tuple("TransitionInventory").finish()
46 }
47}
48
49impl<S, T> core::cmp::PartialEq for TransitionInventory<S, T> {
50 fn eq(&self, other: &Self) -> bool {
51 core::ptr::eq(self.as_slice(), other.as_slice())
52 }
53}
54
55impl<S, T> core::cmp::Eq for TransitionInventory<S, T> {}
56
57#[derive(Clone, Copy)]
60pub struct TransitionPresentationInventory<T: 'static, M: 'static = ()> {
61 get: fn() -> &'static [TransitionPresentation<T, M>],
62}
63
64impl<T, M> TransitionPresentationInventory<T, M> {
65 pub const fn new(get: fn() -> &'static [TransitionPresentation<T, M>]) -> Self {
67 Self { get }
68 }
69
70 pub fn as_slice(&self) -> &'static [TransitionPresentation<T, M>] {
72 (self.get)()
73 }
74}
75
76impl<T, M> core::ops::Deref for TransitionPresentationInventory<T, M> {
77 type Target = [TransitionPresentation<T, M>];
78
79 fn deref(&self) -> &Self::Target {
80 self.as_slice()
81 }
82}
83
84impl<T, M> core::fmt::Debug for TransitionPresentationInventory<T, M> {
85 fn fmt(
86 &self,
87 formatter: &mut core::fmt::Formatter<'_>,
88 ) -> core::result::Result<(), core::fmt::Error> {
89 formatter
90 .debug_tuple("TransitionPresentationInventory")
91 .finish()
92 }
93}
94
95impl<T, M> core::cmp::PartialEq for TransitionPresentationInventory<T, M> {
96 fn eq(&self, other: &Self) -> bool {
97 core::ptr::eq(self.as_slice(), other.as_slice())
98 }
99}
100
101impl<T, M> core::cmp::Eq for TransitionPresentationInventory<T, M> {}
102
103#[derive(Clone, Copy)]
106pub struct LinkedTransitionInventory {
107 get: fn() -> &'static [LinkedTransitionDescriptor],
108}
109
110impl LinkedTransitionInventory {
111 pub const fn new(get: fn() -> &'static [LinkedTransitionDescriptor]) -> Self {
113 Self { get }
114 }
115
116 pub fn as_slice(&self) -> &'static [LinkedTransitionDescriptor] {
118 (self.get)()
119 }
120}
121
122impl core::ops::Deref for LinkedTransitionInventory {
123 type Target = [LinkedTransitionDescriptor];
124
125 fn deref(&self) -> &Self::Target {
126 self.as_slice()
127 }
128}
129
130impl core::fmt::Debug for LinkedTransitionInventory {
131 fn fmt(
132 &self,
133 formatter: &mut core::fmt::Formatter<'_>,
134 ) -> core::result::Result<(), core::fmt::Error> {
135 formatter.debug_tuple("LinkedTransitionInventory").finish()
136 }
137}
138
139impl core::cmp::PartialEq for LinkedTransitionInventory {
140 fn eq(&self, other: &Self) -> bool {
141 core::ptr::eq(self.as_slice(), other.as_slice())
142 }
143}
144
145impl core::cmp::Eq for LinkedTransitionInventory {}
146
147pub trait MachineStateIdentity: MachineIntrospection {
149 const STATE_ID: Self::StateId;
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub struct MachinePresentation<
156 S: 'static,
157 T: 'static,
158 MachineMeta: 'static = (),
159 StateMeta: 'static = (),
160 TransitionMeta: 'static = (),
161> {
162 pub machine: Option<MachinePresentationDescriptor<MachineMeta>>,
164 pub states: &'static [StatePresentation<S, StateMeta>],
166 pub transitions: TransitionPresentationInventory<T, TransitionMeta>,
168}
169
170impl<S, T, MachineMeta, StateMeta, TransitionMeta>
171 MachinePresentation<S, T, MachineMeta, StateMeta, TransitionMeta>
172where
173 S: Copy + Eq + 'static,
174 T: Copy + Eq + 'static,
175{
176 pub fn state(&self, id: S) -> Option<&StatePresentation<S, StateMeta>> {
178 self.states.iter().find(|state| state.id == id)
179 }
180
181 pub fn transition(&self, id: T) -> Option<&TransitionPresentation<T, TransitionMeta>> {
183 self.transitions
184 .iter()
185 .find(|transition| transition.id == id)
186 }
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct MachinePresentationDescriptor<M: 'static = ()> {
192 pub label: Option<&'static str>,
194 pub description: Option<&'static str>,
196 pub metadata: M,
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub struct StatePresentation<S: 'static, M: 'static = ()> {
203 pub id: S,
205 pub label: Option<&'static str>,
207 pub description: Option<&'static str>,
209 pub metadata: M,
211}
212
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct TransitionPresentation<T: 'static, M: 'static = ()> {
216 pub id: T,
218 pub label: Option<&'static str>,
220 pub description: Option<&'static str>,
222 pub metadata: M,
224}
225
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub struct RecordedTransition<S: 'static, T: 'static> {
229 pub machine: MachineDescriptor,
231 pub from: S,
233 pub transition: T,
235 pub chosen: S,
237}
238
239impl<S, T> RecordedTransition<S, T>
240where
241 S: 'static,
242 T: 'static,
243{
244 pub const fn new(machine: MachineDescriptor, from: S, transition: T, chosen: S) -> Self {
246 Self {
247 machine,
248 from,
249 transition,
250 chosen,
251 }
252 }
253
254 pub fn transition_in<'a>(
256 &self,
257 graph: &'a MachineGraph<S, T>,
258 ) -> Option<&'a TransitionDescriptor<S, T>>
259 where
260 S: Copy + Eq,
261 T: Copy + Eq,
262 {
263 let descriptor = graph.transition(self.transition)?;
264 if descriptor.from == self.from && descriptor.to.contains(&self.chosen) {
265 Some(descriptor)
266 } else {
267 None
268 }
269 }
270
271 pub fn source_state_in<'a>(
273 &self,
274 graph: &'a MachineGraph<S, T>,
275 ) -> Option<&'a StateDescriptor<S>>
276 where
277 S: Copy + Eq,
278 T: Copy + Eq,
279 {
280 self.transition_in(graph)?;
281 graph.state(self.from)
282 }
283
284 pub fn chosen_state_in<'a>(
286 &self,
287 graph: &'a MachineGraph<S, T>,
288 ) -> Option<&'a StateDescriptor<S>>
289 where
290 S: Copy + Eq,
291 T: Copy + Eq,
292 {
293 self.transition_in(graph)?;
294 graph.state(self.chosen)
295 }
296}
297
298pub trait MachineTransitionRecorder: MachineStateIdentity {
300 fn try_record_transition(
303 transition: Self::TransitionId,
304 chosen: Self::StateId,
305 ) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>> {
306 let graph = Self::GRAPH;
307 let descriptor = graph.transition(transition)?;
308 if descriptor.from != Self::STATE_ID || !descriptor.to.contains(&chosen) {
309 return None;
310 }
311
312 Some(RecordedTransition::new(
313 graph.machine,
314 Self::STATE_ID,
315 transition,
316 chosen,
317 ))
318 }
319
320 fn try_record_transition_to<Next>(
322 transition: Self::TransitionId,
323 ) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>>
324 where
325 Next: MachineStateIdentity<StateId = Self::StateId, TransitionId = Self::TransitionId>,
326 {
327 Self::try_record_transition(transition, Next::STATE_ID)
328 }
329}
330
331impl<M> MachineTransitionRecorder for M where M: MachineStateIdentity {}
332
333#[doc(hidden)]
335#[linkme::distributed_slice]
336pub static __STATUM_LINKED_MACHINES: [LinkedMachineGraph];
337
338pub fn linked_machines() -> &'static [LinkedMachineGraph] {
340 &__STATUM_LINKED_MACHINES
341}
342
343#[doc(hidden)]
345#[linkme::distributed_slice]
346pub static __STATUM_LINKED_VALIDATOR_ENTRIES: [LinkedValidatorEntryDescriptor];
347
348pub fn linked_validator_entries() -> &'static [LinkedValidatorEntryDescriptor] {
351 &__STATUM_LINKED_VALIDATOR_ENTRIES
352}
353
354#[doc(hidden)]
356#[linkme::distributed_slice]
357pub static __STATUM_LINKED_RELATIONS: [LinkedRelationDescriptor];
358
359pub fn linked_relations() -> &'static [LinkedRelationDescriptor] {
361 &__STATUM_LINKED_RELATIONS
362}
363
364#[doc(hidden)]
366#[linkme::distributed_slice]
367pub static __STATUM_LINKED_VIA_ROUTES: [LinkedViaRouteDescriptor];
368
369pub fn linked_via_routes() -> &'static [LinkedViaRouteDescriptor] {
372 &__STATUM_LINKED_VIA_ROUTES
373}
374
375#[doc(hidden)]
377#[linkme::distributed_slice]
378pub static __STATUM_LINKED_REFERENCE_TYPES: [LinkedReferenceTypeDescriptor];
379
380pub fn linked_reference_types() -> &'static [LinkedReferenceTypeDescriptor] {
383 &__STATUM_LINKED_REFERENCE_TYPES
384}
385
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
388pub struct MachineGraph<S: 'static, T: 'static> {
389 pub machine: MachineDescriptor,
391 pub states: &'static [StateDescriptor<S>],
393 pub transitions: TransitionInventory<S, T>,
395}
396
397impl<S, T> MachineGraph<S, T>
398where
399 S: Copy + Eq + 'static,
400 T: Copy + Eq + 'static,
401{
402 pub fn state(&self, id: S) -> Option<&StateDescriptor<S>> {
404 self.states.iter().find(|state| state.id == id)
405 }
406
407 pub fn transition(&self, id: T) -> Option<&TransitionDescriptor<S, T>> {
409 self.transitions
410 .iter()
411 .find(|transition| transition.id == id)
412 }
413
414 pub fn transitions_from(
416 &self,
417 state: S,
418 ) -> impl Iterator<Item = &TransitionDescriptor<S, T>> + '_ {
419 self.transitions
420 .iter()
421 .filter(move |transition| transition.from == state)
422 }
423
424 pub fn transition_from_method(
426 &self,
427 state: S,
428 method_name: &str,
429 ) -> Option<&TransitionDescriptor<S, T>> {
430 self.transitions
431 .iter()
432 .find(|transition| transition.from == state && transition.method_name == method_name)
433 }
434
435 pub fn transitions_named<'a>(
437 &'a self,
438 method_name: &'a str,
439 ) -> impl Iterator<Item = &'a TransitionDescriptor<S, T>> + 'a {
440 self.transitions
441 .iter()
442 .filter(move |transition| transition.method_name == method_name)
443 }
444
445 pub fn legal_targets(&self, id: T) -> Option<&'static [S]> {
447 self.transition(id).map(|transition| transition.to)
448 }
449}
450
451#[derive(Clone, Copy, Debug, Eq, PartialEq)]
454pub struct LinkedMachineGraph {
455 pub machine: MachineDescriptor,
457 pub label: Option<&'static str>,
459 pub description: Option<&'static str>,
461 pub docs: Option<&'static str>,
463 pub states: &'static [LinkedStateDescriptor],
465 pub transitions: LinkedTransitionInventory,
467 pub static_links: &'static [StaticMachineLinkDescriptor],
469}
470
471impl LinkedMachineGraph {
472 pub fn state(&self, rust_name: &str) -> Option<&LinkedStateDescriptor> {
474 self.states
475 .iter()
476 .find(|state| state.rust_name == rust_name)
477 }
478
479 pub fn transitions_from(
481 &self,
482 state: &'static str,
483 ) -> impl Iterator<Item = &LinkedTransitionDescriptor> + '_ {
484 self.transitions
485 .iter()
486 .filter(move |transition| transition.from == state)
487 }
488
489 pub fn transition_from_method(
491 &self,
492 state: &'static str,
493 method_name: &str,
494 ) -> Option<&LinkedTransitionDescriptor> {
495 self.transitions
496 .iter()
497 .find(|transition| transition.from == state && transition.method_name == method_name)
498 }
499}
500
501#[derive(Clone, Copy, Debug, Eq, PartialEq)]
504pub enum MachineRole {
505 Protocol,
507 Composition,
510}
511
512#[derive(Clone, Copy, Debug, Eq, PartialEq)]
514pub struct MachineDescriptor {
515 pub module_path: &'static str,
517 pub rust_type_path: &'static str,
519 pub role: MachineRole,
521}
522
523#[derive(Clone, Copy, Debug, Eq, PartialEq)]
525pub struct StateDescriptor<S: 'static> {
526 pub id: S,
528 pub rust_name: &'static str,
530 pub has_data: bool,
532}
533
534#[derive(Clone, Copy, Debug, Eq, PartialEq)]
536pub struct LinkedStateDescriptor {
537 pub rust_name: &'static str,
539 pub label: Option<&'static str>,
541 pub description: Option<&'static str>,
543 pub docs: Option<&'static str>,
545 pub has_data: bool,
547 pub direct_construction_available: bool,
549}
550
551#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub struct TransitionDescriptor<S: 'static, T: 'static> {
554 pub id: T,
556 pub method_name: &'static str,
558 pub from: S,
560 pub to: &'static [S],
562}
563
564#[derive(Clone, Copy, Debug, Eq, PartialEq)]
566pub struct LinkedTransitionDescriptor {
567 pub method_name: &'static str,
569 pub label: Option<&'static str>,
571 pub description: Option<&'static str>,
573 pub docs: Option<&'static str>,
575 pub from: &'static str,
577 pub to: &'static [&'static str],
579}
580
581#[derive(Clone, Copy, Debug, Eq, PartialEq)]
583pub struct StaticMachineLinkDescriptor {
584 pub from_state: &'static str,
586 pub field_name: Option<&'static str>,
588 pub to_machine_path: &'static [&'static str],
590 pub to_state: &'static str,
592}
593
594#[derive(Clone, Copy, Debug, Eq, PartialEq)]
596pub enum LinkedRelationKind {
597 StatePayload,
599 MachineField,
601 TransitionParam,
603}
604
605#[derive(Clone, Copy, Debug, Eq, PartialEq)]
607pub enum LinkedRelationBasis {
608 DirectTypeSyntax,
610 AttestedTypeSyntax,
613 DeclaredReferenceType,
615 ViaDeclaration,
617}
618
619#[derive(Clone, Copy, Debug)]
621pub enum LinkedRelationTarget {
622 DirectMachine {
624 machine_path: &'static [&'static str],
626 resolved_machine_type_name: fn() -> &'static str,
631 state: &'static str,
633 },
634 DeclaredReferenceType {
636 resolved_type_name: fn() -> &'static str,
638 },
639 AttestedProducerRoute {
643 via_module_path: &'static str,
645 route_name: &'static str,
647 resolved_route_type_name: fn() -> &'static str,
651 route_id: u64,
654 },
655 AttestedRoute {
657 via_module_path: &'static str,
659 route_name: &'static str,
661 resolved_route_type_name: fn() -> &'static str,
665 route_id: u64,
668 machine_path: &'static [&'static str],
671 resolved_machine_type_name: fn() -> &'static str,
676 state: &'static str,
678 },
679}
680
681impl PartialEq for LinkedRelationTarget {
682 fn eq(&self, other: &Self) -> bool {
683 match (self, other) {
684 (
685 Self::DirectMachine {
686 machine_path: left_machine_path,
687 resolved_machine_type_name: left_type_name,
688 state: left_state,
689 },
690 Self::DirectMachine {
691 machine_path: right_machine_path,
692 resolved_machine_type_name: right_type_name,
693 state: right_state,
694 },
695 ) => {
696 left_machine_path == right_machine_path
697 && left_type_name() == right_type_name()
698 && left_state == right_state
699 }
700 (
701 Self::DeclaredReferenceType {
702 resolved_type_name: left_name,
703 },
704 Self::DeclaredReferenceType {
705 resolved_type_name: right_name,
706 },
707 ) => left_name() == right_name(),
708 (
709 Self::AttestedProducerRoute {
710 via_module_path: left_module_path,
711 route_name: left_route_name,
712 resolved_route_type_name: left_type_name,
713 route_id: left_route_id,
714 },
715 Self::AttestedProducerRoute {
716 via_module_path: right_module_path,
717 route_name: right_route_name,
718 resolved_route_type_name: right_type_name,
719 route_id: right_route_id,
720 },
721 ) => {
722 left_module_path == right_module_path
723 && left_route_name == right_route_name
724 && left_type_name() == right_type_name()
725 && left_route_id == right_route_id
726 }
727 (
728 Self::AttestedRoute {
729 via_module_path: left_module_path,
730 route_name: left_route_name,
731 resolved_route_type_name: left_type_name,
732 route_id: left_route_id,
733 machine_path: left_machine_path,
734 resolved_machine_type_name: left_machine_type_name,
735 state: left_state,
736 },
737 Self::AttestedRoute {
738 via_module_path: right_module_path,
739 route_name: right_route_name,
740 resolved_route_type_name: right_type_name,
741 route_id: right_route_id,
742 machine_path: right_machine_path,
743 resolved_machine_type_name: right_machine_type_name,
744 state: right_state,
745 },
746 ) => {
747 left_module_path == right_module_path
748 && left_route_name == right_route_name
749 && left_type_name() == right_type_name()
750 && left_route_id == right_route_id
751 && left_machine_path == right_machine_path
752 && left_machine_type_name() == right_machine_type_name()
753 && left_state == right_state
754 }
755 _ => false,
756 }
757 }
758}
759
760impl Eq for LinkedRelationTarget {}
761
762#[derive(Clone, Copy, Debug, Eq, PartialEq)]
764pub enum LinkedRelationSource {
765 StatePayload {
767 state: &'static str,
769 field_name: Option<&'static str>,
771 },
772 MachineField {
774 field_name: Option<&'static str>,
776 field_index: usize,
778 },
779 TransitionParam {
781 state: &'static str,
783 transition: &'static str,
785 param_index: usize,
787 param_name: Option<&'static str>,
789 },
790}
791
792#[derive(Clone, Copy, Debug, Eq, PartialEq)]
794pub struct LinkedRelationDescriptor {
795 pub machine: MachineDescriptor,
797 pub kind: LinkedRelationKind,
799 pub source: LinkedRelationSource,
801 pub basis: LinkedRelationBasis,
803 pub target: LinkedRelationTarget,
805}
806
807#[derive(Clone, Copy, Debug)]
809pub struct LinkedViaRouteDescriptor {
810 pub machine: MachineDescriptor,
812 pub via_module_path: &'static str,
814 pub route_name: &'static str,
816 pub resolved_route_type_name: fn() -> &'static str,
819 pub route_id: u64,
821 pub transition: &'static str,
823 pub source_state: &'static str,
825 pub target_state: &'static str,
827}
828
829impl PartialEq for LinkedViaRouteDescriptor {
830 fn eq(&self, other: &Self) -> bool {
831 self.machine == other.machine
832 && self.via_module_path == other.via_module_path
833 && self.route_name == other.route_name
834 && (self.resolved_route_type_name)() == (other.resolved_route_type_name)()
835 && self.route_id == other.route_id
836 && self.transition == other.transition
837 && self.source_state == other.source_state
838 && self.target_state == other.target_state
839 }
840}
841
842impl Eq for LinkedViaRouteDescriptor {}
843
844#[derive(Clone, Copy, Debug)]
846pub struct LinkedReferenceTypeDescriptor {
847 pub rust_type_path: &'static str,
849 pub resolved_type_name: fn() -> &'static str,
851 pub to_machine_path: &'static [&'static str],
853 pub resolved_target_machine_type_name: fn() -> &'static str,
856 pub to_state: &'static str,
858}
859
860impl PartialEq for LinkedReferenceTypeDescriptor {
861 fn eq(&self, other: &Self) -> bool {
862 self.rust_type_path == other.rust_type_path
863 && (self.resolved_type_name)() == (other.resolved_type_name)()
864 && self.to_machine_path == other.to_machine_path
865 && (self.resolved_target_machine_type_name)()
866 == (other.resolved_target_machine_type_name)()
867 && self.to_state == other.to_state
868 }
869}
870
871impl Eq for LinkedReferenceTypeDescriptor {}
872
873
874#[derive(Clone, Copy, Debug)]
876pub struct LinkedValidatorEntryDescriptor {
877 pub machine: MachineDescriptor,
879 pub source_module_path: &'static str,
881 pub source_type_display: &'static str,
883 pub resolved_source_type_name: fn() -> &'static str,
885 pub docs: Option<&'static str>,
887 pub target_states: &'static [&'static str],
889}
890
891impl PartialEq for LinkedValidatorEntryDescriptor {
892 fn eq(&self, other: &Self) -> bool {
893 self.machine == other.machine
894 && self.source_module_path == other.source_module_path
895 && self.source_type_display == other.source_type_display
896 && (self.resolved_source_type_name)() == (other.resolved_source_type_name)()
897 && self.docs == other.docs
898 && self.target_states == other.target_states
899 }
900}
901
902impl Eq for LinkedValidatorEntryDescriptor {}
903
904#[cfg(test)]
905mod tests {
906 use super::{
907 LinkedMachineGraph, LinkedStateDescriptor, LinkedTransitionDescriptor,
908 LinkedTransitionInventory, LinkedValidatorEntryDescriptor, MachineDescriptor, MachineGraph,
909 MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, MachineRole,
910 MachineStateIdentity, MachineTransitionRecorder, RecordedTransition, StateDescriptor,
911 StatePresentation, StaticMachineLinkDescriptor, TransitionDescriptor, TransitionInventory,
912 TransitionPresentation, TransitionPresentationInventory,
913 };
914 use core::marker::PhantomData;
915
916 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
917 enum StateId {
918 Draft,
919 Review,
920 Published,
921 }
922
923 #[derive(Clone, Copy)]
924 struct TransitionId(&'static crate::__private::TransitionToken);
925
926 impl TransitionId {
927 const fn from_token(token: &'static crate::__private::TransitionToken) -> Self {
928 Self(token)
929 }
930 }
931
932 impl core::fmt::Debug for TransitionId {
933 fn fmt(
934 &self,
935 formatter: &mut core::fmt::Formatter<'_>,
936 ) -> core::result::Result<(), core::fmt::Error> {
937 formatter.write_str("TransitionId(..)")
938 }
939 }
940
941 impl core::cmp::PartialEq for TransitionId {
942 fn eq(&self, other: &Self) -> bool {
943 core::ptr::eq(self.0, other.0)
944 }
945 }
946
947 impl core::cmp::Eq for TransitionId {}
948
949 impl core::hash::Hash for TransitionId {
950 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
951 let ptr = core::ptr::from_ref(self.0) as usize;
952 <usize as core::hash::Hash>::hash(&ptr, state);
953 }
954 }
955
956 static REVIEW_TARGETS: [StateId; 1] = [StateId::Review];
957 static PUBLISH_TARGETS: [StateId; 1] = [StateId::Published];
958 static SUBMIT_FROM_DRAFT_TOKEN: crate::__private::TransitionToken =
959 crate::__private::TransitionToken::new();
960 static PUBLISH_FROM_REVIEW_TOKEN: crate::__private::TransitionToken =
961 crate::__private::TransitionToken::new();
962 const SUBMIT_FROM_DRAFT: TransitionId = TransitionId::from_token(&SUBMIT_FROM_DRAFT_TOKEN);
963 const PUBLISH_FROM_REVIEW: TransitionId = TransitionId::from_token(&PUBLISH_FROM_REVIEW_TOKEN);
964 static STATES: [StateDescriptor<StateId>; 3] = [
965 StateDescriptor {
966 id: StateId::Draft,
967 rust_name: "Draft",
968 has_data: false,
969 },
970 StateDescriptor {
971 id: StateId::Review,
972 rust_name: "Review",
973 has_data: true,
974 },
975 StateDescriptor {
976 id: StateId::Published,
977 rust_name: "Published",
978 has_data: false,
979 },
980 ];
981 static TRANSITIONS: [TransitionDescriptor<StateId, TransitionId>; 2] = [
982 TransitionDescriptor {
983 id: SUBMIT_FROM_DRAFT,
984 method_name: "submit",
985 from: StateId::Draft,
986 to: &REVIEW_TARGETS,
987 },
988 TransitionDescriptor {
989 id: PUBLISH_FROM_REVIEW,
990 method_name: "publish",
991 from: StateId::Review,
992 to: &PUBLISH_TARGETS,
993 },
994 ];
995 static TRANSITION_PRESENTATIONS: [TransitionPresentation<TransitionId, TransitionMeta>; 2] = [
996 TransitionPresentation {
997 id: SUBMIT_FROM_DRAFT,
998 label: Some("Submit"),
999 description: Some("Move work into review."),
1000 metadata: TransitionMeta {
1001 phase: Phase::Review,
1002 branch: false,
1003 },
1004 },
1005 TransitionPresentation {
1006 id: PUBLISH_FROM_REVIEW,
1007 label: Some("Publish"),
1008 description: Some("Complete the workflow."),
1009 metadata: TransitionMeta {
1010 phase: Phase::Output,
1011 branch: false,
1012 },
1013 },
1014 ];
1015
1016 struct Workflow<S>(PhantomData<S>);
1017 struct DraftMarker;
1018 struct ReviewMarker;
1019 struct PublishedMarker;
1020
1021 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1022 enum Phase {
1023 Intake,
1024 Review,
1025 Output,
1026 }
1027
1028 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1029 struct MachineMeta {
1030 phase: Phase,
1031 }
1032
1033 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1034 struct StateMeta {
1035 phase: Phase,
1036 term: &'static str,
1037 }
1038
1039 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1040 struct TransitionMeta {
1041 phase: Phase,
1042 branch: bool,
1043 }
1044
1045 static PRESENTATION: MachinePresentation<
1046 StateId,
1047 TransitionId,
1048 MachineMeta,
1049 StateMeta,
1050 TransitionMeta,
1051 > = MachinePresentation {
1052 machine: Some(MachinePresentationDescriptor {
1053 label: Some("Workflow"),
1054 description: Some("Example presentation metadata for introspection."),
1055 metadata: MachineMeta {
1056 phase: Phase::Intake,
1057 },
1058 }),
1059 states: &[
1060 StatePresentation {
1061 id: StateId::Draft,
1062 label: Some("Draft"),
1063 description: Some("Work has not been submitted yet."),
1064 metadata: StateMeta {
1065 phase: Phase::Intake,
1066 term: "draft",
1067 },
1068 },
1069 StatePresentation {
1070 id: StateId::Review,
1071 label: Some("Review"),
1072 description: Some("Work is awaiting review."),
1073 metadata: StateMeta {
1074 phase: Phase::Review,
1075 term: "review",
1076 },
1077 },
1078 StatePresentation {
1079 id: StateId::Published,
1080 label: Some("Published"),
1081 description: Some("Work is complete."),
1082 metadata: StateMeta {
1083 phase: Phase::Output,
1084 term: "published",
1085 },
1086 },
1087 ],
1088 transitions: TransitionPresentationInventory::new(|| &TRANSITION_PRESENTATIONS),
1089 };
1090
1091 impl<S> MachineIntrospection for Workflow<S> {
1092 type StateId = StateId;
1093 type TransitionId = TransitionId;
1094
1095 const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId> = &MachineGraph {
1096 machine: MachineDescriptor {
1097 module_path: "workflow",
1098 rust_type_path: "workflow::Machine",
1099 role: MachineRole::Protocol,
1100 },
1101 states: &STATES,
1102 transitions: TransitionInventory::new(|| &TRANSITIONS),
1103 };
1104 }
1105
1106 impl MachineStateIdentity for Workflow<DraftMarker> {
1107 const STATE_ID: Self::StateId = StateId::Draft;
1108 }
1109
1110 impl MachineStateIdentity for Workflow<ReviewMarker> {
1111 const STATE_ID: Self::StateId = StateId::Review;
1112 }
1113
1114 impl MachineStateIdentity for Workflow<PublishedMarker> {
1115 const STATE_ID: Self::StateId = StateId::Published;
1116 }
1117
1118 #[test]
1119 fn query_helpers_find_expected_items() {
1120 let graph = MachineGraph {
1121 machine: MachineDescriptor {
1122 module_path: "workflow",
1123 rust_type_path: "workflow::Machine",
1124 role: MachineRole::Protocol,
1125 },
1126 states: &STATES,
1127 transitions: TransitionInventory::new(|| &TRANSITIONS),
1128 };
1129
1130 assert_eq!(
1131 graph.state(StateId::Review).map(|state| state.rust_name),
1132 Some("Review")
1133 );
1134 assert_eq!(
1135 graph
1136 .transition(PUBLISH_FROM_REVIEW)
1137 .map(|transition| transition.method_name),
1138 Some("publish")
1139 );
1140 assert_eq!(
1141 graph
1142 .transition_from_method(StateId::Draft, "submit")
1143 .map(|transition| transition.id),
1144 Some(SUBMIT_FROM_DRAFT)
1145 );
1146 assert_eq!(
1147 graph.legal_targets(SUBMIT_FROM_DRAFT),
1148 Some(REVIEW_TARGETS.as_slice())
1149 );
1150 assert_eq!(graph.transitions_from(StateId::Draft).count(), 1);
1151 assert_eq!(graph.transitions_named("publish").count(), 1);
1152 }
1153
1154 #[test]
1155 fn runtime_transition_recording_joins_back_to_static_graph() {
1156 let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
1157 SUBMIT_FROM_DRAFT,
1158 )
1159 .expect("valid runtime transition");
1160
1161 assert_eq!(
1162 event,
1163 RecordedTransition::new(
1164 MachineDescriptor {
1165 module_path: "workflow",
1166 rust_type_path: "workflow::Machine",
1167 role: MachineRole::Protocol,
1168 },
1169 StateId::Draft,
1170 SUBMIT_FROM_DRAFT,
1171 StateId::Review,
1172 )
1173 );
1174 assert_eq!(
1175 Workflow::<DraftMarker>::GRAPH
1176 .transition(event.transition)
1177 .map(|transition| (transition.from, transition.to)),
1178 Some((StateId::Draft, REVIEW_TARGETS.as_slice()))
1179 );
1180 assert_eq!(
1181 event.source_state_in(Workflow::<DraftMarker>::GRAPH),
1182 Some(&StateDescriptor {
1183 id: StateId::Draft,
1184 rust_name: "Draft",
1185 has_data: false,
1186 })
1187 );
1188 }
1189
1190 #[test]
1191 fn runtime_transition_recording_rejects_illegal_target_or_site() {
1192 assert!(Workflow::<DraftMarker>::try_record_transition(
1193 PUBLISH_FROM_REVIEW,
1194 StateId::Published,
1195 )
1196 .is_none());
1197 assert!(
1198 Workflow::<ReviewMarker>::try_record_transition_to::<Workflow<PublishedMarker>>(
1199 SUBMIT_FROM_DRAFT,
1200 )
1201 .is_none()
1202 );
1203 }
1204
1205 #[test]
1206 fn presentation_queries_join_with_runtime_transitions() {
1207 let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
1208 SUBMIT_FROM_DRAFT,
1209 )
1210 .expect("valid runtime transition");
1211
1212 assert_eq!(
1213 PRESENTATION.machine,
1214 Some(MachinePresentationDescriptor {
1215 label: Some("Workflow"),
1216 description: Some("Example presentation metadata for introspection."),
1217 metadata: MachineMeta {
1218 phase: Phase::Intake,
1219 },
1220 })
1221 );
1222 assert_eq!(
1223 PRESENTATION.transition(event.transition),
1224 Some(&TransitionPresentation {
1225 id: SUBMIT_FROM_DRAFT,
1226 label: Some("Submit"),
1227 description: Some("Move work into review."),
1228 metadata: TransitionMeta {
1229 phase: Phase::Review,
1230 branch: false,
1231 },
1232 })
1233 );
1234 assert_eq!(
1235 PRESENTATION.state(event.chosen),
1236 Some(&StatePresentation {
1237 id: StateId::Review,
1238 label: Some("Review"),
1239 description: Some("Work is awaiting review."),
1240 metadata: StateMeta {
1241 phase: Phase::Review,
1242 term: "review",
1243 },
1244 })
1245 );
1246 }
1247
1248 fn linked_transitions() -> &'static [LinkedTransitionDescriptor] {
1249 static TRANSITIONS: [LinkedTransitionDescriptor; 1] = [LinkedTransitionDescriptor {
1250 method_name: "submit",
1251 label: Some("Submit"),
1252 description: None,
1253 docs: Some("Submits the draft for review."),
1254 from: "Draft",
1255 to: &["Review"],
1256 }];
1257 &TRANSITIONS
1258 }
1259
1260 #[test]
1261 fn linked_machine_graph_helpers_work() {
1262 static STATES: [LinkedStateDescriptor; 2] = [
1263 LinkedStateDescriptor {
1264 rust_name: "Draft",
1265 label: None,
1266 description: None,
1267 docs: Some("Initial draft state."),
1268 has_data: false,
1269 direct_construction_available: true,
1270 },
1271 LinkedStateDescriptor {
1272 rust_name: "Review",
1273 label: Some("Review"),
1274 description: None,
1275 docs: Some("Review state with payload."),
1276 has_data: true,
1277 direct_construction_available: true,
1278 },
1279 ];
1280 static LINKS: [StaticMachineLinkDescriptor; 1] = [StaticMachineLinkDescriptor {
1281 from_state: "Review",
1282 field_name: None,
1283 to_machine_path: &["task", "Machine"],
1284 to_state: "Running",
1285 }];
1286
1287 let linked = LinkedMachineGraph {
1288 machine: MachineDescriptor {
1289 module_path: "workflow",
1290 rust_type_path: "workflow::Machine",
1291 role: MachineRole::Protocol,
1292 },
1293 label: Some("Workflow"),
1294 description: None,
1295 docs: Some("Workflow machine docs."),
1296 states: &STATES,
1297 transitions: LinkedTransitionInventory::new(linked_transitions),
1298 static_links: &LINKS,
1299 };
1300
1301 assert_eq!(linked.state("Review"), Some(&STATES[1]));
1302 assert_eq!(linked.docs, Some("Workflow machine docs."));
1303 assert_eq!(
1304 linked.state("Draft").and_then(|state| state.docs),
1305 Some("Initial draft state.")
1306 );
1307 assert_eq!(
1308 linked.transition_from_method("Draft", "submit"),
1309 Some(&linked_transitions()[0])
1310 );
1311 assert_eq!(
1312 linked_transitions()[0].docs,
1313 Some("Submits the draft for review.")
1314 );
1315 assert_eq!(linked.transitions_from("Draft").count(), 1);
1316 assert_eq!(linked.static_links, &LINKS);
1317 }
1318
1319 #[test]
1320 fn linked_validator_entry_descriptor_exposes_declared_surface() {
1321 fn db_row_type_name() -> &'static str {
1322 "workflow::rows::DbRow"
1323 }
1324
1325 static ENTRY: LinkedValidatorEntryDescriptor = LinkedValidatorEntryDescriptor {
1326 machine: MachineDescriptor {
1327 module_path: "workflow",
1328 rust_type_path: "workflow::Machine",
1329 role: MachineRole::Protocol,
1330 },
1331 source_module_path: "workflow::rows",
1332 source_type_display: "DbRow",
1333 resolved_source_type_name: db_row_type_name,
1334 docs: Some("Rebuilds workflow machines from database rows."),
1335 target_states: &["Draft", "Review"],
1336 };
1337
1338 assert_eq!(ENTRY.machine.rust_type_path, "workflow::Machine");
1339 assert_eq!(ENTRY.source_module_path, "workflow::rows");
1340 assert_eq!(ENTRY.source_type_display, "DbRow");
1341 assert_eq!((ENTRY.resolved_source_type_name)(), "workflow::rows::DbRow");
1342 assert_eq!(
1343 ENTRY.docs,
1344 Some("Rebuilds workflow machines from database rows.")
1345 );
1346 assert_eq!(ENTRY.target_states, &["Draft", "Review"]);
1347 }
1348}