#![allow(dead_code)]
use std::fs;
use std::io::ErrorKind;
use std::path::Path;
use statum::{
LinkedMachineGraph, LinkedStateDescriptor, LinkedTransitionDescriptor,
LinkedTransitionInventory, LinkedValidatorEntryDescriptor, MachineDescriptor, MachineRole,
StaticMachineLinkDescriptor,
};
use statum_graph::{
codebase::{render, CodebaseMachineRelationGroupSemantic, CodebaseMachineRole},
CodebaseDoc,
};
fn broken_row_type_name() -> &'static str {
"broken::BrokenRow"
}
fn workflow_db_row_type_name() -> &'static str {
"workflow::DbRow"
}
mod task {
use statum::{machine, state, transition, validators, Error};
#[state]
pub enum State {
Idle,
#[present(label = "Running", description = "Task execution is active.")]
Running,
Done,
}
#[machine]
#[present(
label = "Task Machine",
description = "Owns the exact task execution lifecycle."
)]
pub struct Machine<State> {}
#[transition]
impl Machine<Idle> {
#[present(
label = "Start Task",
description = "Moves the task from idle into running work."
)]
fn start(self) -> Machine<Running> {
self.transition()
}
}
#[transition]
impl Machine<Running> {
fn finish(self) -> Machine<Done> {
self.transition()
}
}
pub struct TaskRow {
pub status: &'static str,
}
#[validators(Machine)]
impl TaskRow {
fn is_idle(&self) -> statum::Result<()> {
if self.status == "idle" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
fn is_running(&self) -> statum::Result<()> {
if self.status == "running" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
fn is_done(&self) -> statum::Result<()> {
if self.status == "done" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
}
}
mod workflow {
use super::task;
use statum::{machine, state, transition, validators, Error};
#[state]
pub enum State {
Draft,
#[present(
label = "In Progress",
description = "Work is currently delegated to a running task."
)]
InProgress(super::task::Machine<super::task::Running>),
Complete,
}
#[machine(role = composition)]
#[present(
label = "Workflow Machine",
description = "Tracks workflow progress across task execution."
)]
pub struct Machine<State> {}
#[transition]
impl Machine<Draft> {
#[present(
label = "Start Workflow",
description = "Begins workflow execution with a running task."
)]
fn start(
self,
running_task: super::task::Machine<super::task::Running>,
) -> Machine<InProgress> {
self.transition_with(running_task)
}
}
#[transition]
impl Machine<InProgress> {
fn finish(self) -> Machine<Complete> {
self.transition()
}
}
pub struct WorkflowRow {
pub status: &'static str,
}
#[validators(Machine)]
impl WorkflowRow {
fn is_draft(&self) -> statum::Result<()> {
if self.status == "draft" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
fn is_in_progress(&self) -> statum::Result<task::Machine<task::Running>> {
if self.status == "in_progress" {
Ok(task::Machine::<task::Running>::builder().build())
} else {
Err(Error::InvalidState)
}
}
fn is_complete(&self) -> statum::Result<()> {
if self.status == "complete" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
}
}
mod named_holder {
use statum::{machine, state, transition};
#[state]
pub enum State {
Pending {
child: super::task::Machine<super::task::Done>,
note: &'static str,
},
Settled,
}
#[machine]
pub struct Machine<State> {}
#[transition]
impl Machine<Pending> {
fn settle(self) -> Machine<Settled> {
self.transition()
}
}
}
mod detached {
use statum::{machine, state};
#[state]
pub enum State {
Alone,
}
#[machine]
pub struct Machine<State> {}
}
#[test]
fn linked_codebase_doc_collects_machines_and_links() {
let doc = CodebaseDoc::linked().expect("linked codebase doc");
assert_eq!(doc.machines().len(), 4);
assert_eq!(doc.links().len(), 2);
let workflow = doc
.machines()
.iter()
.find(|machine| machine.rust_type_path.ends_with("workflow::Machine"))
.expect("workflow machine");
assert_eq!(workflow.role, CodebaseMachineRole::Composition);
assert_eq!(workflow.label, Some("Workflow Machine"));
assert_eq!(
workflow.description,
Some("Tracks workflow progress across task execution.")
);
assert_eq!(
workflow.docs,
Some("Coordinates workflow progress around task execution.")
);
assert_eq!(
workflow
.states
.iter()
.find(|state| state.rust_name == "InProgress")
.map(|state| state.label),
Some(Some("In Progress"))
);
assert_eq!(
workflow
.states
.iter()
.find(|state| state.rust_name == "InProgress")
.and_then(|state| state.description),
Some("Work is currently delegated to a running task.")
);
assert_eq!(
workflow
.states
.iter()
.find(|state| state.rust_name == "InProgress")
.and_then(|state| state.docs),
Some("Workflow execution is delegated to a running task.")
);
let workflow_start = workflow
.transitions
.iter()
.find(|transition| transition.method_name == "start")
.expect("workflow start transition");
assert_eq!(
workflow_start.description,
Some("Begins workflow execution with a running task.")
);
assert_eq!(
workflow_start.docs,
Some("Starts the workflow with a running task.")
);
assert_eq!(workflow.validator_entries.len(), 1);
assert_eq!(
workflow.validator_entries[0].source_type_display,
"WorkflowRow"
);
assert_eq!(workflow.validator_entries[0].target_states, vec![0, 1, 2]);
assert_eq!(
workflow.validator_entries[0].docs,
Some("Rebuilds workflow machines from persisted workflow rows.")
);
let workflow_link = doc
.links()
.iter()
.find(|link| {
doc.machine(link.from_machine)
.map(|machine| machine.rust_type_path.ends_with("workflow::Machine"))
.unwrap_or(false)
})
.expect("workflow link");
assert_eq!(workflow_link.field_name, None);
let named_link = doc
.links()
.iter()
.find(|link| link.field_name == Some("child"))
.expect("named child link");
let target_machine = doc
.machine(named_link.to_machine)
.expect("named link target machine");
let target_state = target_machine
.state(named_link.to_state)
.expect("named link target state");
assert!(target_machine.rust_type_path.ends_with("task::Machine"));
assert_eq!(target_state.rust_name, "Done");
assert_eq!(target_machine.validator_entries.len(), 1);
assert_eq!(
target_machine.description,
Some("Owns the exact task execution lifecycle.")
);
assert_eq!(
target_machine.docs,
Some("Handles the task lifecycle from idle to done.")
);
assert_eq!(
target_machine.validator_entries[0].display_label().as_ref(),
"TaskRow::into_machine()"
);
assert_eq!(
target_machine.validator_entries[0].docs,
Some("Rebuilds task machines from persisted task rows.")
);
let relation_groups = doc.machine_relation_groups();
let workflow_group = relation_groups
.iter()
.find(|group| group.from_machine == workflow.index && group.to_machine == target_machine.index)
.expect("workflow composition group");
assert_eq!(
workflow_group.semantic,
CodebaseMachineRelationGroupSemantic::CompositionDirectChild
);
assert_eq!(workflow_group.display_label(), "composition refs: payload, param");
let named_holder = doc
.machines()
.iter()
.find(|machine| machine.rust_type_path.ends_with("named_holder::Machine"))
.expect("named holder machine");
let named_group = relation_groups
.iter()
.find(|group| group.from_machine == named_holder.index && group.to_machine == target_machine.index)
.expect("named holder exact group");
assert_eq!(named_group.semantic, CodebaseMachineRelationGroupSemantic::Exact);
assert_eq!(named_group.display_label(), "exact refs: payload");
}
#[test]
fn linked_codebase_renderers_are_stable() {
let doc = CodebaseDoc::linked().expect("linked codebase doc");
insta::assert_snapshot!("linked_codebase_mermaid", render::mermaid(&doc));
insta::assert_snapshot!("linked_codebase_dot", render::dot(&doc));
insta::assert_snapshot!("linked_codebase_plantuml", render::plantuml(&doc));
insta::assert_snapshot!("linked_codebase_json", render::json(&doc));
}
#[test]
fn linked_codebase_writes_all_formats() {
let doc = CodebaseDoc::linked().expect("linked codebase doc");
let dir = tempfile::tempdir().expect("temp dir");
let paths = render::write_all_to_dir(&doc, dir.path().join("nested"), "codebase")
.expect("write linked codebase bundle");
let file_names = paths
.iter()
.map(|path| {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("")
})
.collect::<Vec<_>>();
assert_eq!(
file_names,
vec![
"codebase.mmd",
"codebase.dot",
"codebase.puml",
"codebase.json",
]
);
let mermaid_path = dir.path().join("nested").join("codebase.mmd");
assert!(mermaid_path.exists());
let mermaid = fs::read_to_string(mermaid_path).expect("mermaid file");
assert!(mermaid.contains("Workflow Machine"));
assert!(mermaid.contains("Task Machine"));
}
#[test]
fn linked_codebase_write_all_rejects_path_like_stem() {
let doc = CodebaseDoc::linked().expect("linked codebase doc");
let dir = tempfile::tempdir().expect("temp dir");
let bundle_dir = dir.path().join("nested");
let outside = dir.path().join("escape.mmd");
let stem = Path::new("..").join("escape");
let error = render::write_all_to_dir(&doc, &bundle_dir, stem.to_str().expect("utf-8 stem"))
.expect_err("path-like stem should be rejected");
assert_eq!(error.kind(), ErrorKind::InvalidInput);
assert!(!bundle_dir.exists());
assert!(!outside.exists());
}
#[test]
fn builder_markers_only_render_for_directly_constructible_states() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 2] = [
LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: false,
},
LinkedStateDescriptor {
rust_name: "Review",
label: None,
description: None,
docs: None,
has_data: true,
direct_construction_available: true,
},
];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "builder_markers",
rust_type_path: "builder_markers::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
let doc = CodebaseDoc::try_from_linked(&LINKED).expect("codebase doc");
let mermaid = render::mermaid(&doc);
let dot = render::dot(&doc);
let plantuml = render::plantuml(&doc);
assert!(mermaid.contains("Review (data) [build]"));
assert!(!mermaid.contains("Draft [build]"));
assert!(dot.contains("Review (data) [build]"));
assert!(!dot.contains("Draft [build]"));
assert!(plantuml.contains("Review (data) [build]"));
assert!(!plantuml.contains("Draft [build]"));
}
#[test]
fn malformed_inventory_rejects_missing_transition_source_before_sort() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&TRANSITIONS
}
static STATES: [LinkedStateDescriptor; 2] = [
LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
},
LinkedStateDescriptor {
rust_name: "Review",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
},
];
static TRANSITIONS: [LinkedTransitionDescriptor; 2] = [
LinkedTransitionDescriptor {
method_name: "submit",
from: "Draft",
to: &["Review"],
label: None,
description: None,
docs: None,
},
LinkedTransitionDescriptor {
method_name: "ghost",
from: "Missing",
to: &["Review"],
label: None,
description: None,
docs: None,
},
];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "broken",
rust_type_path: "broken::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
assert_eq!(
CodebaseDoc::try_from_linked(&LINKED)
.unwrap_err()
.to_string(),
"linked machine `broken::Machine` contains transition `ghost` whose source state is missing from the state list"
);
}
#[test]
fn malformed_inventory_rejects_missing_static_link_source_state() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 1] = [LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
}];
static LINKS: [StaticMachineLinkDescriptor; 1] = [StaticMachineLinkDescriptor {
from_state: "Missing",
field_name: None,
to_machine_path: &["task", "Machine"],
to_state: "Running",
}];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "broken",
rust_type_path: "broken::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &LINKS,
}];
assert_eq!(
CodebaseDoc::try_from_linked(&LINKED)
.unwrap_err()
.to_string(),
"linked machine `broken::Machine` contains a static payload link from missing source state `Missing`"
);
}
#[test]
fn malformed_inventory_rejects_missing_validator_machine() {
static VALIDATORS: [LinkedValidatorEntryDescriptor; 1] = [LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "broken",
rust_type_path: "broken::Machine",
role: MachineRole::Protocol,
},
source_module_path: "broken",
source_type_display: "BrokenRow",
resolved_source_type_name: broken_row_type_name,
docs: None,
target_states: &["Draft"],
}];
assert_eq!(
CodebaseDoc::try_from_linked_with_validator_entries(&[], &VALIDATORS)
.unwrap_err()
.to_string(),
"linked validator entry `BrokenRow::into_machine()` from module `broken` points at missing machine `broken::Machine`"
);
}
#[test]
fn malformed_inventory_rejects_missing_validator_target_state() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 1] = [LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
}];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
static VALIDATORS: [LinkedValidatorEntryDescriptor; 1] = [LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow",
source_type_display: "DbRow",
resolved_source_type_name: workflow_db_row_type_name,
docs: None,
target_states: &["Missing"],
}];
assert_eq!(
CodebaseDoc::try_from_linked_with_validator_entries(&LINKED, &VALIDATORS)
.unwrap_err()
.to_string(),
"linked validator entry `DbRow::into_machine()` from module `workflow` points at missing state `workflow::Machine::Missing`"
);
}
#[test]
fn malformed_inventory_rejects_empty_validator_target_set() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 1] = [LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
}];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
static VALIDATORS: [LinkedValidatorEntryDescriptor; 1] = [LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow",
source_type_display: "DbRow",
resolved_source_type_name: workflow_db_row_type_name,
docs: None,
target_states: &[],
}];
assert_eq!(
CodebaseDoc::try_from_linked_with_validator_entries(&LINKED, &VALIDATORS)
.unwrap_err()
.to_string(),
"linked validator entry `DbRow::into_machine()` from module `workflow` for machine `workflow::Machine` contains no target states"
);
}
#[test]
fn malformed_inventory_rejects_duplicate_validator_target_state() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 1] = [LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
}];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
static VALIDATORS: [LinkedValidatorEntryDescriptor; 1] = [LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow",
source_type_display: "DbRow",
resolved_source_type_name: workflow_db_row_type_name,
docs: None,
target_states: &["Draft", "Draft"],
}];
assert_eq!(
CodebaseDoc::try_from_linked_with_validator_entries(&LINKED, &VALIDATORS)
.unwrap_err()
.to_string(),
"linked validator entry `DbRow::into_machine()` from module `workflow` for machine `workflow::Machine` contains duplicate target state `Draft`"
);
}
#[test]
fn malformed_inventory_rejects_duplicate_validator_entry_identity() {
fn transitions() -> &'static [LinkedTransitionDescriptor] {
&[]
}
static STATES: [LinkedStateDescriptor; 1] = [LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: None,
has_data: false,
direct_construction_available: true,
}];
static LINKED: [LinkedMachineGraph; 1] = [LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
label: None,
description: None,
docs: None,
states: &STATES,
transitions: LinkedTransitionInventory::new(transitions),
static_links: &[],
}];
static VALIDATORS: [LinkedValidatorEntryDescriptor; 2] = [
LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow",
source_type_display: "DbRow",
resolved_source_type_name: workflow_db_row_type_name,
docs: None,
target_states: &["Draft"],
},
LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow",
source_type_display: "DbRow",
resolved_source_type_name: workflow_db_row_type_name,
docs: None,
target_states: &["Draft"],
},
];
assert_eq!(
CodebaseDoc::try_from_linked_with_validator_entries(&LINKED, &VALIDATORS)
.unwrap_err()
.to_string(),
"linked validator entry `DbRow::into_machine()` from module `workflow` appears more than once for machine `workflow::Machine`"
);
}