use crate::MachineGraph;
use std::vec::IntoIter;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WalkStep<S: 'static, T: 'static> {
pub from: S,
pub transition: T,
pub method_name: &'static str,
pub legal_targets: &'static [S],
pub chosen_target: S,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LegalWalk<S: 'static, T: 'static> {
pub start: S,
pub steps: Vec<WalkStep<S, T>>,
pub end: S,
}
#[derive(Clone, Debug)]
pub struct Generator<S: 'static, T: 'static> {
graph: &'static MachineGraph<S, T>,
start: S,
max_depth: usize,
terminal_states: Vec<S>,
include_empty: bool,
}
pub fn legal_walks_from<S, T>(graph: &'static MachineGraph<S, T>, start: S) -> Generator<S, T>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
Generator {
graph,
start,
max_depth: graph.states.len().saturating_sub(1),
terminal_states: Vec::new(),
include_empty: true,
}
}
impl<S, T> Generator<S, T>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn terminal_states<I>(mut self, states: I) -> Self
where
I: IntoIterator<Item = S>,
{
self.terminal_states = states.into_iter().collect();
self
}
pub fn include_empty(mut self, include_empty: bool) -> Self {
self.include_empty = include_empty;
self
}
fn expand(
&self,
current: S,
steps: &mut Vec<WalkStep<S, T>>,
walks: &mut Vec<LegalWalk<S, T>>,
) {
if self.include_empty || !steps.is_empty() {
walks.push(LegalWalk {
start: self.start,
steps: steps.clone(),
end: current,
});
}
if steps.len() >= self.max_depth || self.terminal_states.contains(¤t) {
return;
}
for transition in self.graph.transitions_from(current) {
for chosen_target in transition.to.iter().copied() {
steps.push(WalkStep {
from: current,
transition: transition.id,
method_name: transition.method_name,
legal_targets: transition.to,
chosen_target,
});
self.expand(chosen_target, steps, walks);
steps.pop();
}
}
}
}
impl<S, T> IntoIterator for Generator<S, T>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
type Item = LegalWalk<S, T>;
type IntoIter = IntoIter<LegalWalk<S, T>>;
fn into_iter(self) -> Self::IntoIter {
let mut walks = Vec::new();
if self.graph.state(self.start).is_some() {
self.expand(self.start, &mut Vec::new(), &mut walks);
}
walks.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::legal_walks_from;
use crate::{
MachineDescriptor, MachineGraph, StateDescriptor, TransitionDescriptor, TransitionInventory,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StateId {
Draft,
Review,
Published,
Rejected,
Archived,
Missing,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TransitionId {
Submit,
Decide,
Archive,
Reopen,
}
static REVIEW_TARGETS: [StateId; 1] = [StateId::Review];
static DECIDE_TARGETS: [StateId; 2] = [StateId::Published, StateId::Rejected];
static ARCHIVE_TARGETS: [StateId; 1] = [StateId::Archived];
static REOPEN_TARGETS: [StateId; 1] = [StateId::Review];
static STATES: [StateDescriptor<StateId>; 5] = [
StateDescriptor {
id: StateId::Draft,
rust_name: "Draft",
has_data: false,
},
StateDescriptor {
id: StateId::Review,
rust_name: "Review",
has_data: false,
},
StateDescriptor {
id: StateId::Published,
rust_name: "Published",
has_data: false,
},
StateDescriptor {
id: StateId::Rejected,
rust_name: "Rejected",
has_data: false,
},
StateDescriptor {
id: StateId::Archived,
rust_name: "Archived",
has_data: false,
},
];
static TRANSITIONS: [TransitionDescriptor<StateId, TransitionId>; 4] = [
TransitionDescriptor {
id: TransitionId::Submit,
method_name: "submit",
from: StateId::Draft,
to: &REVIEW_TARGETS,
},
TransitionDescriptor {
id: TransitionId::Decide,
method_name: "decide",
from: StateId::Review,
to: &DECIDE_TARGETS,
},
TransitionDescriptor {
id: TransitionId::Archive,
method_name: "archive",
from: StateId::Published,
to: &ARCHIVE_TARGETS,
},
TransitionDescriptor {
id: TransitionId::Reopen,
method_name: "reopen",
from: StateId::Rejected,
to: &REOPEN_TARGETS,
},
];
static GRAPH: MachineGraph<StateId, TransitionId> = MachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::DocumentMachine",
},
states: &STATES,
transitions: TransitionInventory::new(|| &TRANSITIONS),
};
#[test]
fn enumerates_branching_walk_prefixes_from_graph_metadata() {
let walks = legal_walks_from(&GRAPH, StateId::Draft)
.max_depth(2)
.include_empty(false)
.into_iter()
.map(|walk| (walk.end, walk.steps.len()))
.collect::<Vec<_>>();
assert_eq!(
walks,
vec![
(StateId::Review, 1),
(StateId::Published, 2),
(StateId::Rejected, 2)
]
);
}
#[test]
fn terminal_states_stop_expansion_before_outgoing_edges() {
let walks = legal_walks_from(&GRAPH, StateId::Draft)
.max_depth(3)
.terminal_states([StateId::Review])
.include_empty(false)
.into_iter()
.collect::<Vec<_>>();
assert_eq!(walks.len(), 1);
assert_eq!(walks[0].end, StateId::Review);
assert_eq!(walks[0].steps[0].method_name, "submit");
}
#[test]
fn max_depth_bounds_cycles() {
let walks = legal_walks_from(&GRAPH, StateId::Rejected)
.max_depth(3)
.include_empty(false)
.into_iter()
.map(|walk| walk.end)
.collect::<Vec<_>>();
assert_eq!(
walks,
vec![
StateId::Review,
StateId::Published,
StateId::Archived,
StateId::Rejected,
StateId::Review,
]
);
}
#[test]
fn absent_start_state_yields_no_walks() {
let walks = legal_walks_from(&GRAPH, StateId::Missing)
.max_depth(2)
.into_iter()
.collect::<Vec<_>>();
assert!(walks.is_empty());
}
}