pub trait MachineIntrospection {
type StateId: Copy + Eq + core::hash::Hash + 'static;
type TransitionId: Copy + Eq + core::hash::Hash + 'static;
const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId>;
}
#[derive(Clone, Copy)]
pub struct TransitionInventory<S: 'static, T: 'static> {
get: fn() -> &'static [TransitionDescriptor<S, T>],
}
impl<S, T> TransitionInventory<S, T> {
pub const fn new(get: fn() -> &'static [TransitionDescriptor<S, T>]) -> Self {
Self { get }
}
pub fn as_slice(&self) -> &'static [TransitionDescriptor<S, T>] {
(self.get)()
}
}
impl<S, T> core::ops::Deref for TransitionInventory<S, T> {
type Target = [TransitionDescriptor<S, T>];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<S, T> core::fmt::Debug for TransitionInventory<S, T> {
fn fmt(
&self,
formatter: &mut core::fmt::Formatter<'_>,
) -> core::result::Result<(), core::fmt::Error> {
formatter.debug_tuple("TransitionInventory").finish()
}
}
impl<S, T> core::cmp::PartialEq for TransitionInventory<S, T> {
fn eq(&self, other: &Self) -> bool {
core::ptr::eq(self.as_slice(), other.as_slice())
}
}
impl<S, T> core::cmp::Eq for TransitionInventory<S, T> {}
#[derive(Clone, Copy)]
pub struct TransitionPresentationInventory<T: 'static, M: 'static = ()> {
get: fn() -> &'static [TransitionPresentation<T, M>],
}
impl<T, M> TransitionPresentationInventory<T, M> {
pub const fn new(get: fn() -> &'static [TransitionPresentation<T, M>]) -> Self {
Self { get }
}
pub fn as_slice(&self) -> &'static [TransitionPresentation<T, M>] {
(self.get)()
}
}
impl<T, M> core::ops::Deref for TransitionPresentationInventory<T, M> {
type Target = [TransitionPresentation<T, M>];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<T, M> core::fmt::Debug for TransitionPresentationInventory<T, M> {
fn fmt(
&self,
formatter: &mut core::fmt::Formatter<'_>,
) -> core::result::Result<(), core::fmt::Error> {
formatter
.debug_tuple("TransitionPresentationInventory")
.finish()
}
}
impl<T, M> core::cmp::PartialEq for TransitionPresentationInventory<T, M> {
fn eq(&self, other: &Self) -> bool {
core::ptr::eq(self.as_slice(), other.as_slice())
}
}
impl<T, M> core::cmp::Eq for TransitionPresentationInventory<T, M> {}
#[derive(Clone, Copy)]
pub struct LinkedTransitionInventory {
get: fn() -> &'static [LinkedTransitionDescriptor],
}
impl LinkedTransitionInventory {
pub const fn new(get: fn() -> &'static [LinkedTransitionDescriptor]) -> Self {
Self { get }
}
pub fn as_slice(&self) -> &'static [LinkedTransitionDescriptor] {
(self.get)()
}
}
impl core::ops::Deref for LinkedTransitionInventory {
type Target = [LinkedTransitionDescriptor];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl core::fmt::Debug for LinkedTransitionInventory {
fn fmt(
&self,
formatter: &mut core::fmt::Formatter<'_>,
) -> core::result::Result<(), core::fmt::Error> {
formatter.debug_tuple("LinkedTransitionInventory").finish()
}
}
impl core::cmp::PartialEq for LinkedTransitionInventory {
fn eq(&self, other: &Self) -> bool {
core::ptr::eq(self.as_slice(), other.as_slice())
}
}
impl core::cmp::Eq for LinkedTransitionInventory {}
pub trait MachineStateIdentity: MachineIntrospection {
const STATE_ID: Self::StateId;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MachinePresentation<
S: 'static,
T: 'static,
MachineMeta: 'static = (),
StateMeta: 'static = (),
TransitionMeta: 'static = (),
> {
pub machine: Option<MachinePresentationDescriptor<MachineMeta>>,
pub states: &'static [StatePresentation<S, StateMeta>],
pub transitions: TransitionPresentationInventory<T, TransitionMeta>,
}
impl<S, T, MachineMeta, StateMeta, TransitionMeta>
MachinePresentation<S, T, MachineMeta, StateMeta, TransitionMeta>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
pub fn state(&self, id: S) -> Option<&StatePresentation<S, StateMeta>> {
self.states.iter().find(|state| state.id == id)
}
pub fn transition(&self, id: T) -> Option<&TransitionPresentation<T, TransitionMeta>> {
self.transitions
.iter()
.find(|transition| transition.id == id)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MachinePresentationDescriptor<M: 'static = ()> {
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub metadata: M,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StatePresentation<S: 'static, M: 'static = ()> {
pub id: S,
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub metadata: M,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TransitionPresentation<T: 'static, M: 'static = ()> {
pub id: T,
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub metadata: M,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RecordedTransition<S: 'static, T: 'static> {
pub machine: MachineDescriptor,
pub from: S,
pub transition: T,
pub chosen: S,
}
impl<S, T> RecordedTransition<S, T>
where
S: 'static,
T: 'static,
{
pub const fn new(machine: MachineDescriptor, from: S, transition: T, chosen: S) -> Self {
Self {
machine,
from,
transition,
chosen,
}
}
pub fn transition_in<'a>(
&self,
graph: &'a MachineGraph<S, T>,
) -> Option<&'a TransitionDescriptor<S, T>>
where
S: Copy + Eq,
T: Copy + Eq,
{
let descriptor = graph.transition(self.transition)?;
if descriptor.from == self.from && descriptor.to.contains(&self.chosen) {
Some(descriptor)
} else {
None
}
}
pub fn source_state_in<'a>(
&self,
graph: &'a MachineGraph<S, T>,
) -> Option<&'a StateDescriptor<S>>
where
S: Copy + Eq,
T: Copy + Eq,
{
self.transition_in(graph)?;
graph.state(self.from)
}
pub fn chosen_state_in<'a>(
&self,
graph: &'a MachineGraph<S, T>,
) -> Option<&'a StateDescriptor<S>>
where
S: Copy + Eq,
T: Copy + Eq,
{
self.transition_in(graph)?;
graph.state(self.chosen)
}
}
pub trait MachineTransitionRecorder: MachineStateIdentity {
fn try_record_transition(
transition: Self::TransitionId,
chosen: Self::StateId,
) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>> {
let graph = Self::GRAPH;
let descriptor = graph.transition(transition)?;
if descriptor.from != Self::STATE_ID || !descriptor.to.contains(&chosen) {
return None;
}
Some(RecordedTransition::new(
graph.machine,
Self::STATE_ID,
transition,
chosen,
))
}
fn try_record_transition_to<Next>(
transition: Self::TransitionId,
) -> Option<RecordedTransition<Self::StateId, Self::TransitionId>>
where
Next: MachineStateIdentity<StateId = Self::StateId, TransitionId = Self::TransitionId>,
{
Self::try_record_transition(transition, Next::STATE_ID)
}
}
impl<M> MachineTransitionRecorder for M where M: MachineStateIdentity {}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __STATUM_LINKED_MACHINES: [LinkedMachineGraph];
pub fn linked_machines() -> &'static [LinkedMachineGraph] {
&__STATUM_LINKED_MACHINES
}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __STATUM_LINKED_VALIDATOR_ENTRIES: [LinkedValidatorEntryDescriptor];
pub fn linked_validator_entries() -> &'static [LinkedValidatorEntryDescriptor] {
&__STATUM_LINKED_VALIDATOR_ENTRIES
}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __STATUM_LINKED_RELATIONS: [LinkedRelationDescriptor];
pub fn linked_relations() -> &'static [LinkedRelationDescriptor] {
&__STATUM_LINKED_RELATIONS
}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __STATUM_LINKED_VIA_ROUTES: [LinkedViaRouteDescriptor];
pub fn linked_via_routes() -> &'static [LinkedViaRouteDescriptor] {
&__STATUM_LINKED_VIA_ROUTES
}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __STATUM_LINKED_REFERENCE_TYPES: [LinkedReferenceTypeDescriptor];
pub fn linked_reference_types() -> &'static [LinkedReferenceTypeDescriptor] {
&__STATUM_LINKED_REFERENCE_TYPES
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MachineGraph<S: 'static, T: 'static> {
pub machine: MachineDescriptor,
pub states: &'static [StateDescriptor<S>],
pub transitions: TransitionInventory<S, T>,
}
impl<S, T> MachineGraph<S, T>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
pub fn state(&self, id: S) -> Option<&StateDescriptor<S>> {
self.states.iter().find(|state| state.id == id)
}
pub fn transition(&self, id: T) -> Option<&TransitionDescriptor<S, T>> {
self.transitions
.iter()
.find(|transition| transition.id == id)
}
pub fn transitions_from(
&self,
state: S,
) -> impl Iterator<Item = &TransitionDescriptor<S, T>> + '_ {
self.transitions
.iter()
.filter(move |transition| transition.from == state)
}
pub fn transition_from_method(
&self,
state: S,
method_name: &str,
) -> Option<&TransitionDescriptor<S, T>> {
self.transitions
.iter()
.find(|transition| transition.from == state && transition.method_name == method_name)
}
pub fn transitions_named<'a>(
&'a self,
method_name: &'a str,
) -> impl Iterator<Item = &'a TransitionDescriptor<S, T>> + 'a {
self.transitions
.iter()
.filter(move |transition| transition.method_name == method_name)
}
pub fn legal_targets(&self, id: T) -> Option<&'static [S]> {
self.transition(id).map(|transition| transition.to)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LinkedMachineGraph {
pub machine: MachineDescriptor,
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub docs: Option<&'static str>,
pub states: &'static [LinkedStateDescriptor],
pub transitions: LinkedTransitionInventory,
pub static_links: &'static [StaticMachineLinkDescriptor],
}
impl LinkedMachineGraph {
pub fn state(&self, rust_name: &str) -> Option<&LinkedStateDescriptor> {
self.states
.iter()
.find(|state| state.rust_name == rust_name)
}
pub fn transitions_from(
&self,
state: &'static str,
) -> impl Iterator<Item = &LinkedTransitionDescriptor> + '_ {
self.transitions
.iter()
.filter(move |transition| transition.from == state)
}
pub fn transition_from_method(
&self,
state: &'static str,
method_name: &str,
) -> Option<&LinkedTransitionDescriptor> {
self.transitions
.iter()
.find(|transition| transition.from == state && transition.method_name == method_name)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MachineRole {
Protocol,
Composition,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MachineDescriptor {
pub module_path: &'static str,
pub rust_type_path: &'static str,
pub role: MachineRole,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StateDescriptor<S: 'static> {
pub id: S,
pub rust_name: &'static str,
pub has_data: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LinkedStateDescriptor {
pub rust_name: &'static str,
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub docs: Option<&'static str>,
pub has_data: bool,
pub direct_construction_available: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TransitionDescriptor<S: 'static, T: 'static> {
pub id: T,
pub method_name: &'static str,
pub from: S,
pub to: &'static [S],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LinkedTransitionDescriptor {
pub method_name: &'static str,
pub label: Option<&'static str>,
pub description: Option<&'static str>,
pub docs: Option<&'static str>,
pub from: &'static str,
pub to: &'static [&'static str],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StaticMachineLinkDescriptor {
pub from_state: &'static str,
pub field_name: Option<&'static str>,
pub to_machine_path: &'static [&'static str],
pub to_state: &'static str,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LinkedRelationKind {
StatePayload,
MachineField,
TransitionParam,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LinkedRelationBasis {
DirectTypeSyntax,
AttestedTypeSyntax,
DeclaredReferenceType,
ViaDeclaration,
}
#[derive(Clone, Copy, Debug)]
pub enum LinkedRelationTarget {
DirectMachine {
machine_path: &'static [&'static str],
resolved_machine_type_name: fn() -> &'static str,
state: &'static str,
},
DeclaredReferenceType {
resolved_type_name: fn() -> &'static str,
},
AttestedProducerRoute {
via_module_path: &'static str,
route_name: &'static str,
resolved_route_type_name: fn() -> &'static str,
route_id: u64,
},
AttestedRoute {
via_module_path: &'static str,
route_name: &'static str,
resolved_route_type_name: fn() -> &'static str,
route_id: u64,
machine_path: &'static [&'static str],
resolved_machine_type_name: fn() -> &'static str,
state: &'static str,
},
}
impl PartialEq for LinkedRelationTarget {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::DirectMachine {
machine_path: left_machine_path,
resolved_machine_type_name: left_type_name,
state: left_state,
},
Self::DirectMachine {
machine_path: right_machine_path,
resolved_machine_type_name: right_type_name,
state: right_state,
},
) => {
left_machine_path == right_machine_path
&& left_type_name() == right_type_name()
&& left_state == right_state
}
(
Self::DeclaredReferenceType {
resolved_type_name: left_name,
},
Self::DeclaredReferenceType {
resolved_type_name: right_name,
},
) => left_name() == right_name(),
(
Self::AttestedProducerRoute {
via_module_path: left_module_path,
route_name: left_route_name,
resolved_route_type_name: left_type_name,
route_id: left_route_id,
},
Self::AttestedProducerRoute {
via_module_path: right_module_path,
route_name: right_route_name,
resolved_route_type_name: right_type_name,
route_id: right_route_id,
},
) => {
left_module_path == right_module_path
&& left_route_name == right_route_name
&& left_type_name() == right_type_name()
&& left_route_id == right_route_id
}
(
Self::AttestedRoute {
via_module_path: left_module_path,
route_name: left_route_name,
resolved_route_type_name: left_type_name,
route_id: left_route_id,
machine_path: left_machine_path,
resolved_machine_type_name: left_machine_type_name,
state: left_state,
},
Self::AttestedRoute {
via_module_path: right_module_path,
route_name: right_route_name,
resolved_route_type_name: right_type_name,
route_id: right_route_id,
machine_path: right_machine_path,
resolved_machine_type_name: right_machine_type_name,
state: right_state,
},
) => {
left_module_path == right_module_path
&& left_route_name == right_route_name
&& left_type_name() == right_type_name()
&& left_route_id == right_route_id
&& left_machine_path == right_machine_path
&& left_machine_type_name() == right_machine_type_name()
&& left_state == right_state
}
_ => false,
}
}
}
impl Eq for LinkedRelationTarget {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LinkedRelationSource {
StatePayload {
state: &'static str,
field_name: Option<&'static str>,
},
MachineField {
field_name: Option<&'static str>,
field_index: usize,
},
TransitionParam {
state: &'static str,
transition: &'static str,
param_index: usize,
param_name: Option<&'static str>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LinkedRelationDescriptor {
pub machine: MachineDescriptor,
pub kind: LinkedRelationKind,
pub source: LinkedRelationSource,
pub basis: LinkedRelationBasis,
pub target: LinkedRelationTarget,
}
#[derive(Clone, Copy, Debug)]
pub struct LinkedViaRouteDescriptor {
pub machine: MachineDescriptor,
pub via_module_path: &'static str,
pub route_name: &'static str,
pub resolved_route_type_name: fn() -> &'static str,
pub route_id: u64,
pub transition: &'static str,
pub source_state: &'static str,
pub target_state: &'static str,
}
impl PartialEq for LinkedViaRouteDescriptor {
fn eq(&self, other: &Self) -> bool {
self.machine == other.machine
&& self.via_module_path == other.via_module_path
&& self.route_name == other.route_name
&& (self.resolved_route_type_name)() == (other.resolved_route_type_name)()
&& self.route_id == other.route_id
&& self.transition == other.transition
&& self.source_state == other.source_state
&& self.target_state == other.target_state
}
}
impl Eq for LinkedViaRouteDescriptor {}
#[derive(Clone, Copy, Debug)]
pub struct LinkedReferenceTypeDescriptor {
pub rust_type_path: &'static str,
pub resolved_type_name: fn() -> &'static str,
pub to_machine_path: &'static [&'static str],
pub resolved_target_machine_type_name: fn() -> &'static str,
pub to_state: &'static str,
}
impl PartialEq for LinkedReferenceTypeDescriptor {
fn eq(&self, other: &Self) -> bool {
self.rust_type_path == other.rust_type_path
&& (self.resolved_type_name)() == (other.resolved_type_name)()
&& self.to_machine_path == other.to_machine_path
&& (self.resolved_target_machine_type_name)()
== (other.resolved_target_machine_type_name)()
&& self.to_state == other.to_state
}
}
impl Eq for LinkedReferenceTypeDescriptor {}
#[derive(Clone, Copy, Debug)]
pub struct LinkedValidatorEntryDescriptor {
pub machine: MachineDescriptor,
pub source_module_path: &'static str,
pub source_type_display: &'static str,
pub resolved_source_type_name: fn() -> &'static str,
pub docs: Option<&'static str>,
pub target_states: &'static [&'static str],
}
impl PartialEq for LinkedValidatorEntryDescriptor {
fn eq(&self, other: &Self) -> bool {
self.machine == other.machine
&& self.source_module_path == other.source_module_path
&& self.source_type_display == other.source_type_display
&& (self.resolved_source_type_name)() == (other.resolved_source_type_name)()
&& self.docs == other.docs
&& self.target_states == other.target_states
}
}
impl Eq for LinkedValidatorEntryDescriptor {}
#[cfg(test)]
mod tests {
use super::{
LinkedMachineGraph, LinkedStateDescriptor, LinkedTransitionDescriptor,
LinkedTransitionInventory, LinkedValidatorEntryDescriptor, MachineDescriptor, MachineGraph,
MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, MachineRole,
MachineStateIdentity, MachineTransitionRecorder, RecordedTransition, StateDescriptor,
StatePresentation, StaticMachineLinkDescriptor, TransitionDescriptor, TransitionInventory,
TransitionPresentation, TransitionPresentationInventory,
};
use core::marker::PhantomData;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
enum StateId {
Draft,
Review,
Published,
}
#[derive(Clone, Copy)]
struct TransitionId(&'static crate::__private::TransitionToken);
impl TransitionId {
const fn from_token(token: &'static crate::__private::TransitionToken) -> Self {
Self(token)
}
}
impl core::fmt::Debug for TransitionId {
fn fmt(
&self,
formatter: &mut core::fmt::Formatter<'_>,
) -> core::result::Result<(), core::fmt::Error> {
formatter.write_str("TransitionId(..)")
}
}
impl core::cmp::PartialEq for TransitionId {
fn eq(&self, other: &Self) -> bool {
core::ptr::eq(self.0, other.0)
}
}
impl core::cmp::Eq for TransitionId {}
impl core::hash::Hash for TransitionId {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
let ptr = core::ptr::from_ref(self.0) as usize;
<usize as core::hash::Hash>::hash(&ptr, state);
}
}
static REVIEW_TARGETS: [StateId; 1] = [StateId::Review];
static PUBLISH_TARGETS: [StateId; 1] = [StateId::Published];
static SUBMIT_FROM_DRAFT_TOKEN: crate::__private::TransitionToken =
crate::__private::TransitionToken::new();
static PUBLISH_FROM_REVIEW_TOKEN: crate::__private::TransitionToken =
crate::__private::TransitionToken::new();
const SUBMIT_FROM_DRAFT: TransitionId = TransitionId::from_token(&SUBMIT_FROM_DRAFT_TOKEN);
const PUBLISH_FROM_REVIEW: TransitionId = TransitionId::from_token(&PUBLISH_FROM_REVIEW_TOKEN);
static STATES: [StateDescriptor<StateId>; 3] = [
StateDescriptor {
id: StateId::Draft,
rust_name: "Draft",
has_data: false,
},
StateDescriptor {
id: StateId::Review,
rust_name: "Review",
has_data: true,
},
StateDescriptor {
id: StateId::Published,
rust_name: "Published",
has_data: false,
},
];
static TRANSITIONS: [TransitionDescriptor<StateId, TransitionId>; 2] = [
TransitionDescriptor {
id: SUBMIT_FROM_DRAFT,
method_name: "submit",
from: StateId::Draft,
to: &REVIEW_TARGETS,
},
TransitionDescriptor {
id: PUBLISH_FROM_REVIEW,
method_name: "publish",
from: StateId::Review,
to: &PUBLISH_TARGETS,
},
];
static TRANSITION_PRESENTATIONS: [TransitionPresentation<TransitionId, TransitionMeta>; 2] = [
TransitionPresentation {
id: SUBMIT_FROM_DRAFT,
label: Some("Submit"),
description: Some("Move work into review."),
metadata: TransitionMeta {
phase: Phase::Review,
branch: false,
},
},
TransitionPresentation {
id: PUBLISH_FROM_REVIEW,
label: Some("Publish"),
description: Some("Complete the workflow."),
metadata: TransitionMeta {
phase: Phase::Output,
branch: false,
},
},
];
struct Workflow<S>(PhantomData<S>);
struct DraftMarker;
struct ReviewMarker;
struct PublishedMarker;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Phase {
Intake,
Review,
Output,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct MachineMeta {
phase: Phase,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct StateMeta {
phase: Phase,
term: &'static str,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct TransitionMeta {
phase: Phase,
branch: bool,
}
static PRESENTATION: MachinePresentation<
StateId,
TransitionId,
MachineMeta,
StateMeta,
TransitionMeta,
> = MachinePresentation {
machine: Some(MachinePresentationDescriptor {
label: Some("Workflow"),
description: Some("Example presentation metadata for introspection."),
metadata: MachineMeta {
phase: Phase::Intake,
},
}),
states: &[
StatePresentation {
id: StateId::Draft,
label: Some("Draft"),
description: Some("Work has not been submitted yet."),
metadata: StateMeta {
phase: Phase::Intake,
term: "draft",
},
},
StatePresentation {
id: StateId::Review,
label: Some("Review"),
description: Some("Work is awaiting review."),
metadata: StateMeta {
phase: Phase::Review,
term: "review",
},
},
StatePresentation {
id: StateId::Published,
label: Some("Published"),
description: Some("Work is complete."),
metadata: StateMeta {
phase: Phase::Output,
term: "published",
},
},
],
transitions: TransitionPresentationInventory::new(|| &TRANSITION_PRESENTATIONS),
};
impl<S> MachineIntrospection for Workflow<S> {
type StateId = StateId;
type TransitionId = TransitionId;
const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId> = &MachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
states: &STATES,
transitions: TransitionInventory::new(|| &TRANSITIONS),
};
}
impl MachineStateIdentity for Workflow<DraftMarker> {
const STATE_ID: Self::StateId = StateId::Draft;
}
impl MachineStateIdentity for Workflow<ReviewMarker> {
const STATE_ID: Self::StateId = StateId::Review;
}
impl MachineStateIdentity for Workflow<PublishedMarker> {
const STATE_ID: Self::StateId = StateId::Published;
}
#[test]
fn query_helpers_find_expected_items() {
let graph = MachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
states: &STATES,
transitions: TransitionInventory::new(|| &TRANSITIONS),
};
assert_eq!(
graph.state(StateId::Review).map(|state| state.rust_name),
Some("Review")
);
assert_eq!(
graph
.transition(PUBLISH_FROM_REVIEW)
.map(|transition| transition.method_name),
Some("publish")
);
assert_eq!(
graph
.transition_from_method(StateId::Draft, "submit")
.map(|transition| transition.id),
Some(SUBMIT_FROM_DRAFT)
);
assert_eq!(
graph.legal_targets(SUBMIT_FROM_DRAFT),
Some(REVIEW_TARGETS.as_slice())
);
assert_eq!(graph.transitions_from(StateId::Draft).count(), 1);
assert_eq!(graph.transitions_named("publish").count(), 1);
}
#[test]
fn runtime_transition_recording_joins_back_to_static_graph() {
let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
SUBMIT_FROM_DRAFT,
)
.expect("valid runtime transition");
assert_eq!(
event,
RecordedTransition::new(
MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
StateId::Draft,
SUBMIT_FROM_DRAFT,
StateId::Review,
)
);
assert_eq!(
Workflow::<DraftMarker>::GRAPH
.transition(event.transition)
.map(|transition| (transition.from, transition.to)),
Some((StateId::Draft, REVIEW_TARGETS.as_slice()))
);
assert_eq!(
event.source_state_in(Workflow::<DraftMarker>::GRAPH),
Some(&StateDescriptor {
id: StateId::Draft,
rust_name: "Draft",
has_data: false,
})
);
}
#[test]
fn runtime_transition_recording_rejects_illegal_target_or_site() {
assert!(Workflow::<DraftMarker>::try_record_transition(
PUBLISH_FROM_REVIEW,
StateId::Published,
)
.is_none());
assert!(
Workflow::<ReviewMarker>::try_record_transition_to::<Workflow<PublishedMarker>>(
SUBMIT_FROM_DRAFT,
)
.is_none()
);
}
#[test]
fn presentation_queries_join_with_runtime_transitions() {
let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
SUBMIT_FROM_DRAFT,
)
.expect("valid runtime transition");
assert_eq!(
PRESENTATION.machine,
Some(MachinePresentationDescriptor {
label: Some("Workflow"),
description: Some("Example presentation metadata for introspection."),
metadata: MachineMeta {
phase: Phase::Intake,
},
})
);
assert_eq!(
PRESENTATION.transition(event.transition),
Some(&TransitionPresentation {
id: SUBMIT_FROM_DRAFT,
label: Some("Submit"),
description: Some("Move work into review."),
metadata: TransitionMeta {
phase: Phase::Review,
branch: false,
},
})
);
assert_eq!(
PRESENTATION.state(event.chosen),
Some(&StatePresentation {
id: StateId::Review,
label: Some("Review"),
description: Some("Work is awaiting review."),
metadata: StateMeta {
phase: Phase::Review,
term: "review",
},
})
);
}
fn linked_transitions() -> &'static [LinkedTransitionDescriptor] {
static TRANSITIONS: [LinkedTransitionDescriptor; 1] = [LinkedTransitionDescriptor {
method_name: "submit",
label: Some("Submit"),
description: None,
docs: Some("Submits the draft for review."),
from: "Draft",
to: &["Review"],
}];
&TRANSITIONS
}
#[test]
fn linked_machine_graph_helpers_work() {
static STATES: [LinkedStateDescriptor; 2] = [
LinkedStateDescriptor {
rust_name: "Draft",
label: None,
description: None,
docs: Some("Initial draft state."),
has_data: false,
direct_construction_available: true,
},
LinkedStateDescriptor {
rust_name: "Review",
label: Some("Review"),
description: None,
docs: Some("Review state with payload."),
has_data: true,
direct_construction_available: true,
},
];
static LINKS: [StaticMachineLinkDescriptor; 1] = [StaticMachineLinkDescriptor {
from_state: "Review",
field_name: None,
to_machine_path: &["task", "Machine"],
to_state: "Running",
}];
let linked = LinkedMachineGraph {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
label: Some("Workflow"),
description: None,
docs: Some("Workflow machine docs."),
states: &STATES,
transitions: LinkedTransitionInventory::new(linked_transitions),
static_links: &LINKS,
};
assert_eq!(linked.state("Review"), Some(&STATES[1]));
assert_eq!(linked.docs, Some("Workflow machine docs."));
assert_eq!(
linked.state("Draft").and_then(|state| state.docs),
Some("Initial draft state.")
);
assert_eq!(
linked.transition_from_method("Draft", "submit"),
Some(&linked_transitions()[0])
);
assert_eq!(
linked_transitions()[0].docs,
Some("Submits the draft for review.")
);
assert_eq!(linked.transitions_from("Draft").count(), 1);
assert_eq!(linked.static_links, &LINKS);
}
#[test]
fn linked_validator_entry_descriptor_exposes_declared_surface() {
fn db_row_type_name() -> &'static str {
"workflow::rows::DbRow"
}
static ENTRY: LinkedValidatorEntryDescriptor = LinkedValidatorEntryDescriptor {
machine: MachineDescriptor {
module_path: "workflow",
rust_type_path: "workflow::Machine",
role: MachineRole::Protocol,
},
source_module_path: "workflow::rows",
source_type_display: "DbRow",
resolved_source_type_name: db_row_type_name,
docs: Some("Rebuilds workflow machines from database rows."),
target_states: &["Draft", "Review"],
};
assert_eq!(ENTRY.machine.rust_type_path, "workflow::Machine");
assert_eq!(ENTRY.source_module_path, "workflow::rows");
assert_eq!(ENTRY.source_type_display, "DbRow");
assert_eq!((ENTRY.resolved_source_type_name)(), "workflow::rows::DbRow");
assert_eq!(
ENTRY.docs,
Some("Rebuilds workflow machines from database rows.")
);
assert_eq!(ENTRY.target_states, &["Draft", "Review"]);
}
}