1use std::collections::{HashMap, HashSet};
36
37use statum::{
38 MachineDescriptor, MachineGraph, MachineIntrospection, StateDescriptor, TransitionDescriptor,
39};
40
41pub mod codebase;
42mod export;
43pub mod render;
44
45pub use codebase::{
46 CodebaseAttestedRoute, CodebaseDoc, CodebaseDocError, CodebaseLink, CodebaseMachine,
47 CodebaseMachineRelationGroup, CodebaseMachineRelationGroupSemantic, CodebaseRelation,
48 CodebaseRelationBasis, CodebaseRelationCount, CodebaseRelationDetail, CodebaseRelationKind,
49 CodebaseRelationSemantic, CodebaseRelationSource, CodebaseState, CodebaseTransition,
50 CodebaseValidatorEntry,
51};
52pub use export::{
53 ExportDoc, ExportDocError, ExportMachine, ExportSource, ExportState, ExportTransition,
54};
55
56#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct MachineDoc<S: 'static, T: 'static> {
63 machine: MachineDescriptor,
64 states: Vec<StateDoc<S>>,
65 edges: Vec<EdgeDoc<S, T>>,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum MachineDocError {
71 EmptyStateList { machine: &'static str },
73 DuplicateStateId {
75 machine: &'static str,
76 state: &'static str,
77 },
78 DuplicateTransitionId {
80 machine: &'static str,
81 transition: &'static str,
82 },
83 DuplicateTransitionSite {
85 machine: &'static str,
86 state: &'static str,
87 transition: &'static str,
88 },
89 MissingSourceState {
91 machine: &'static str,
92 transition: &'static str,
93 },
94 MissingTargetState {
96 machine: &'static str,
97 transition: &'static str,
98 },
99 EmptyTargetSet {
101 machine: &'static str,
102 transition: &'static str,
103 },
104 DuplicateTargetState {
106 machine: &'static str,
107 transition: &'static str,
108 state: &'static str,
109 },
110}
111
112impl core::fmt::Display for MachineDocError {
113 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114 match self {
115 Self::EmptyStateList { machine } => write!(
116 formatter,
117 "machine graph `{machine}` contains no states"
118 ),
119 Self::DuplicateStateId { machine, state } => write!(
120 formatter,
121 "machine graph `{machine}` contains duplicate state id for state `{state}`"
122 ),
123 Self::DuplicateTransitionId {
124 machine,
125 transition,
126 } => write!(
127 formatter,
128 "machine graph `{machine}` contains duplicate transition id for transition `{transition}`"
129 ),
130 Self::DuplicateTransitionSite {
131 machine,
132 state,
133 transition,
134 } => write!(
135 formatter,
136 "machine graph `{machine}` contains duplicate transition site `{state}::{transition}`"
137 ),
138 Self::MissingSourceState {
139 machine,
140 transition,
141 } => write!(
142 formatter,
143 "machine graph `{machine}` contains transition `{transition}` whose source state is missing from the state list"
144 ),
145 Self::MissingTargetState {
146 machine,
147 transition,
148 } => write!(
149 formatter,
150 "machine graph `{machine}` contains transition `{transition}` whose target state is missing from the state list"
151 ),
152 Self::EmptyTargetSet {
153 machine,
154 transition,
155 } => write!(
156 formatter,
157 "machine graph `{machine}` contains transition `{transition}` with no target states"
158 ),
159 Self::DuplicateTargetState {
160 machine,
161 transition,
162 state,
163 } => write!(
164 formatter,
165 "machine graph `{machine}` contains transition `{transition}` with duplicate target state `{state}`"
166 ),
167 }
168 }
169}
170
171impl std::error::Error for MachineDocError {}
172
173impl<S, T> TryFrom<&'static MachineGraph<S, T>> for MachineDoc<S, T>
174where
175 S: Copy + Eq + std::hash::Hash + 'static,
176 T: Copy + Eq + 'static,
177{
178 type Error = MachineDocError;
179
180 fn try_from(graph: &'static MachineGraph<S, T>) -> Result<Self, Self::Error> {
181 Self::try_from_graph(graph)
182 }
183}
184
185impl<S, T> MachineDoc<S, T> {
186 pub fn machine(&self) -> MachineDescriptor {
188 self.machine
189 }
190
191 pub fn states(&self) -> &[StateDoc<S>] {
193 &self.states
194 }
195
196 pub fn edges(&self) -> &[EdgeDoc<S, T>] {
198 &self.edges
199 }
200}
201
202impl<S, T> MachineDoc<S, T>
203where
204 S: Copy + Eq + 'static,
205{
206 pub fn state(&self, id: S) -> Option<&StateDoc<S>> {
208 self.states.iter().find(|state| state.descriptor.id == id)
209 }
210}
211
212impl<S, T> MachineDoc<S, T> {
213 pub fn roots(&self) -> impl Iterator<Item = &StateDoc<S>> {
215 self.states.iter().filter(|state| state.is_root)
216 }
217}
218
219impl<S, T> MachineDoc<S, T>
220where
221 S: Copy + Eq + std::hash::Hash + 'static,
222 T: Copy + Eq + 'static,
223{
224 pub fn from_machine<M>() -> Self
230 where
231 M: MachineIntrospection<StateId = S, TransitionId = T>,
232 {
233 Self::try_from_graph(M::GRAPH)
234 .expect("Statum emitted an invalid MachineIntrospection::GRAPH")
235 }
236
237 pub fn try_from_graph(graph: &'static MachineGraph<S, T>) -> Result<Self, MachineDocError> {
243 let transitions = graph.transitions.as_slice();
244 validate_graph(graph.machine, graph.states, transitions)?;
245 let incoming = incoming_states(transitions);
246 let state_positions = state_positions(graph.states);
247
248 let states = graph
249 .states
250 .iter()
251 .copied()
252 .map(|descriptor| StateDoc {
253 descriptor,
254 is_root: !incoming.contains(&descriptor.id),
255 })
256 .collect();
257
258 let mut edges = transitions
259 .iter()
260 .copied()
261 .map(|descriptor| EdgeDoc { descriptor })
262 .collect::<Vec<_>>();
263 edges.sort_by(|left, right| compare_edges(&state_positions, left, right));
264
265 Ok(Self {
266 machine: graph.machine,
267 states,
268 edges,
269 })
270 }
271}
272
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct StateDoc<S: 'static> {
276 pub descriptor: StateDescriptor<S>,
278 pub is_root: bool,
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub struct EdgeDoc<S: 'static, T: 'static> {
285 pub descriptor: TransitionDescriptor<S, T>,
287}
288
289fn validate_graph<S, T>(
290 machine: MachineDescriptor,
291 states: &[StateDescriptor<S>],
292 transitions: &[TransitionDescriptor<S, T>],
293) -> Result<(), MachineDocError>
294where
295 S: Copy + Eq + std::hash::Hash + 'static,
296 T: Copy + Eq + 'static,
297{
298 if states.is_empty() {
299 return Err(MachineDocError::EmptyStateList {
300 machine: machine.rust_type_path,
301 });
302 }
303
304 let mut state_names = HashMap::with_capacity(states.len());
305 for state in states.iter() {
306 if state_names.insert(state.id, state.rust_name).is_some() {
307 return Err(MachineDocError::DuplicateStateId {
308 machine: machine.rust_type_path,
309 state: state.rust_name,
310 });
311 }
312 }
313
314 let mut transition_sites = HashSet::with_capacity(transitions.len());
315 let mut transition_ids = Vec::with_capacity(transitions.len());
316 for transition in transitions.iter() {
317 if transition_ids.contains(&transition.id) {
318 return Err(MachineDocError::DuplicateTransitionId {
319 machine: machine.rust_type_path,
320 transition: transition.method_name,
321 });
322 }
323 transition_ids.push(transition.id);
324
325 if !state_names.contains_key(&transition.from) {
326 return Err(MachineDocError::MissingSourceState {
327 machine: machine.rust_type_path,
328 transition: transition.method_name,
329 });
330 }
331
332 let from_state_name = state_names[&transition.from];
333 if !transition_sites.insert((transition.from, transition.method_name)) {
334 return Err(MachineDocError::DuplicateTransitionSite {
335 machine: machine.rust_type_path,
336 state: from_state_name,
337 transition: transition.method_name,
338 });
339 }
340
341 if transition.to.is_empty() {
342 return Err(MachineDocError::EmptyTargetSet {
343 machine: machine.rust_type_path,
344 transition: transition.method_name,
345 });
346 }
347
348 let mut seen_targets = HashSet::with_capacity(transition.to.len());
349 for target in transition.to.iter().copied() {
350 let Some(state_name) = state_names.get(&target).copied() else {
351 return Err(MachineDocError::MissingTargetState {
352 machine: machine.rust_type_path,
353 transition: transition.method_name,
354 });
355 };
356
357 if !seen_targets.insert(target) {
358 return Err(MachineDocError::DuplicateTargetState {
359 machine: machine.rust_type_path,
360 transition: transition.method_name,
361 state: state_name,
362 });
363 }
364 }
365 }
366
367 Ok(())
368}
369
370fn incoming_states<S, T>(transitions: &[TransitionDescriptor<S, T>]) -> HashSet<S>
371where
372 S: Copy + Eq + std::hash::Hash + 'static,
373 T: Copy + Eq + 'static,
374{
375 let mut incoming = HashSet::new();
376 for transition in transitions.iter() {
377 for target in transition.to.iter().copied() {
378 incoming.insert(target);
379 }
380 }
381
382 incoming
383}
384
385fn state_positions<S>(states: &[StateDescriptor<S>]) -> HashMap<S, usize>
386where
387 S: Copy + Eq + std::hash::Hash + 'static,
388{
389 states
390 .iter()
391 .enumerate()
392 .map(|(index, state)| (state.id, index))
393 .collect()
394}
395
396fn compare_edges<S, T>(
397 state_positions: &HashMap<S, usize>,
398 left: &EdgeDoc<S, T>,
399 right: &EdgeDoc<S, T>,
400) -> std::cmp::Ordering
401where
402 S: Copy + Eq + std::hash::Hash + 'static,
403 T: Copy + Eq + 'static,
404{
405 state_positions[&left.descriptor.from]
406 .cmp(&state_positions[&right.descriptor.from])
407 .then_with(|| {
408 left.descriptor
409 .method_name
410 .cmp(right.descriptor.method_name)
411 })
412 .then_with(|| compare_targets(state_positions, left.descriptor.to, right.descriptor.to))
413}
414
415fn compare_targets<S>(
416 state_positions: &HashMap<S, usize>,
417 left: &[S],
418 right: &[S],
419) -> std::cmp::Ordering
420where
421 S: Copy + Eq + std::hash::Hash + 'static,
422{
423 let left = left.iter().map(|state| state_positions[state]);
424 let right = right.iter().map(|state| state_positions[state]);
425
426 left.cmp(right)
427}