use compact_str::CompactString;
use crate::model::{Action, DataItem, DataModel, State, Statechart, Transition};
pub struct StatechartBuilder {
initial: CompactString,
name: Option<CompactString>,
states: Vec<State>,
datamodel: DataModel,
}
pub struct StateBuilder {
state: State,
}
impl StatechartBuilder {
pub fn new(initial: impl Into<CompactString>) -> Self {
Self {
initial: initial.into(),
name: None,
states: Vec::new(),
datamodel: DataModel::default(),
}
}
pub fn name(mut self, name: impl Into<CompactString>) -> Self {
self.name = Some(name.into());
self
}
pub fn state(
mut self,
id: impl Into<CompactString>,
f: impl FnOnce(&mut StateBuilder),
) -> Self {
let mut sb = StateBuilder {
state: State::atomic(id),
};
f(&mut sb);
self.states.push(sb.state);
self
}
pub fn final_state(mut self, id: impl Into<CompactString>) -> Self {
self.states.push(State::final_state(id));
self
}
pub fn compound(
mut self,
id: impl Into<CompactString>,
initial_child: impl Into<CompactString>,
f: impl FnOnce(&mut CompoundBuilder),
) -> Self {
let mut cb = CompoundBuilder { states: Vec::new() };
f(&mut cb);
self.states
.push(State::compound(id, initial_child, cb.states));
self
}
pub fn parallel(
mut self,
id: impl Into<CompactString>,
f: impl FnOnce(&mut CompoundBuilder),
) -> Self {
let mut cb = CompoundBuilder { states: Vec::new() };
f(&mut cb);
self.states.push(State::parallel(id, cb.states));
self
}
pub fn data(mut self, id: impl Into<CompactString>) -> Self {
self.datamodel.items.push(DataItem::new(id));
self
}
pub fn build(self) -> Statechart {
let mut chart = Statechart::new(self.initial, self.states);
chart.name = self.name;
chart.datamodel = self.datamodel;
chart
}
}
pub struct CompoundBuilder {
states: Vec<State>,
}
impl CompoundBuilder {
pub fn state(&mut self, id: impl Into<CompactString>, f: impl FnOnce(&mut StateBuilder)) {
let mut sb = StateBuilder {
state: State::atomic(id),
};
f(&mut sb);
self.states.push(sb.state);
}
pub fn final_state(&mut self, id: impl Into<CompactString>) {
self.states.push(State::final_state(id));
}
pub fn compound(
&mut self,
id: impl Into<CompactString>,
initial_child: impl Into<CompactString>,
f: impl FnOnce(&mut CompoundBuilder),
) {
let mut cb = CompoundBuilder { states: Vec::new() };
f(&mut cb);
self.states
.push(State::compound(id, initial_child, cb.states));
}
}
impl StateBuilder {
pub fn on_event(
&mut self,
event: impl Into<CompactString>,
target: impl Into<CompactString>,
) -> &mut Transition {
self.state.transitions.push(Transition::new(event, target));
self.state.transitions.last_mut().unwrap()
}
pub fn always(&mut self, target: impl Into<CompactString>) -> &mut Transition {
self.state.transitions.push(Transition::eventless(target));
self.state.transitions.last_mut().unwrap()
}
pub fn on_entry(&mut self, action: Action) -> &mut Self {
self.state.on_entry.push(action);
self
}
pub fn on_exit(&mut self, action: Action) -> &mut Self {
self.state.on_exit.push(action);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::StateKind;
use crate::validate;
#[test]
fn builder_simple_workflow() {
let chart = StatechartBuilder::new("draft")
.name("test")
.state("draft", |s| {
s.on_event("submit", "review").set_guard("has_documents");
})
.state("review", |s| {
s.on_event("approve", "done");
s.on_event("reject", "draft");
})
.final_state("done")
.build();
validate(&chart).unwrap();
assert_eq!(chart.name.as_deref(), Some("test"));
assert_eq!(chart.states.len(), 3);
assert_eq!(chart.states[0].transitions.len(), 1);
assert_eq!(
chart.states[0].transitions[0].guard.as_deref(),
Some("has_documents")
);
}
#[test]
fn builder_with_compound() {
let chart = StatechartBuilder::new("main")
.compound("main", "a", |c| {
c.state("a", |s| {
s.on_event("next", "b");
});
c.state("b", |s| {
s.on_event("done", "end");
});
c.final_state("end");
})
.build();
validate(&chart).unwrap();
assert_eq!(chart.states[0].kind, StateKind::Compound);
assert_eq!(chart.states[0].children.len(), 3);
}
#[test]
fn builder_with_delay_and_quorum() {
let chart = StatechartBuilder::new("pending")
.state("pending", |s| {
s.on_event("approve", "done")
.set_guard("approval.committee")
.set_quorum(3);
s.on_event("timeout", "expired").set_delay("PT48H");
})
.final_state("done")
.final_state("expired")
.build();
validate(&chart).unwrap();
assert_eq!(chart.states[0].transitions[0].quorum, Some(3));
assert_eq!(
chart.states[0].transitions[1].delay.as_deref(),
Some("PT48H")
);
}
}