diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 811addb84..92e370956 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -23,7 +23,7 @@ steps: agents: queue: "default" docker: "*" - command: "cargo test" + command: "cargo test --workspace" timeout_in_minutes: 15 plugins: - docker-compose#v3.0.0: diff --git a/Cargo.toml b/Cargo.toml index 059a855dd..4835310f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,17 @@ edition = "2018" [dependencies] async-trait = "0.1" +derive_more = "0.99" prost = "0.6" prost-types = "0.6" thiserror = "1.0" tonic = "0.3" +[dependencies.rustfsm] +path = "fsm" + [build-dependencies] -tonic-build = "0.3" \ No newline at end of file +tonic-build = "0.3" + +[workspace] +members = [".", "fsm"] diff --git a/fsm/Cargo.toml b/fsm/Cargo.toml new file mode 100644 index 000000000..4f42ae438 --- /dev/null +++ b/fsm/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rustfsm" +version = "0.1.0" +authors = ["Spencer Judge "] +edition = "2018" + +[[test]] +name = "card_reader" +path = "tests/card_reader.rs" + +[dependencies] +thiserror = "1.0" +derive_more = "0.99" +state_machine_procmacro = { path = "state_machine_procmacro" } +state_machine_trait = { path = "state_machine_trait" } diff --git a/fsm/README.md b/fsm/README.md new file mode 100644 index 000000000..b8d04625e --- /dev/null +++ b/fsm/README.md @@ -0,0 +1,3 @@ +A procmacro and trait for implementing state machines in Rust + +We should move this to it's own repo once we're done iterating. \ No newline at end of file diff --git a/fsm/src/lib.rs b/fsm/src/lib.rs new file mode 100644 index 000000000..9f1f43fa8 --- /dev/null +++ b/fsm/src/lib.rs @@ -0,0 +1,2 @@ +pub use state_machine_procmacro::fsm; +pub use state_machine_trait::{StateMachine, TransitionResult}; diff --git a/fsm/state_machine_procmacro/Cargo.toml b/fsm/state_machine_procmacro/Cargo.toml new file mode 100644 index 000000000..3d7f0c5f0 --- /dev/null +++ b/fsm/state_machine_procmacro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "state_machine_procmacro" +version = "0.1.0" +authors = ["Spencer Judge "] +edition = "2018" + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/progress.rs" + +[dependencies] +derive_more = "0.99" +proc-macro2 = "1.0" +syn = { version = "1.0", features = ["default", "extra-traits"] } +quote = "1.0" +state_machine_trait = { path = "../state_machine_trait" } + +[dev-dependencies] +trybuild = { version = "1.0", features = ["diff"] } + diff --git a/fsm/state_machine_procmacro/src/lib.rs b/fsm/state_machine_procmacro/src/lib.rs new file mode 100644 index 000000000..ae59346c8 --- /dev/null +++ b/fsm/state_machine_procmacro/src/lib.rs @@ -0,0 +1,394 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use std::collections::{HashMap, HashSet}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream, Result}, + parse_macro_input, + punctuated::Punctuated, + spanned::Spanned, + Error, Fields, Ident, Token, Variant, +}; + +/// Parses a DSL for defining finite state machines, and produces code implementing the +/// [StateMachine](trait.StateMachine.html) trait. +/// +/// An example state machine definition of a card reader for unlocking a door: +/// ``` +/// # extern crate state_machine_trait as rustfsm; +/// use state_machine_procmacro::fsm; +/// use std::convert::Infallible; +/// use state_machine_trait::{StateMachine, TransitionResult}; +/// +/// fsm! { +/// CardReader, Commands, Infallible +/// +/// Locked --(CardReadable(CardData), on_card_readable) --> ReadingCard; +/// ReadingCard --(CardAccepted, on_card_accepted) --> DoorOpen; +/// ReadingCard --(CardRejected, on_card_rejected) --> Locked; +/// DoorOpen --(DoorClosed, on_door_closed) --> Locked; +/// } +/// +/// #[derive(Debug, Clone, Eq, PartialEq, Hash)] +/// pub enum Commands { +/// StartBlinkingLight, +/// StopBlinkingLight, +/// ProcessData(CardData), +/// } +/// +/// type CardData = String; +/// +/// /// Door is locked / idle / we are ready to read +/// #[derive(Debug, Clone, Eq, PartialEq, Hash)] +/// pub struct Locked {} +/// +/// /// Actively reading the card +/// #[derive(Debug, Clone, Eq, PartialEq, Hash)] +/// pub struct ReadingCard { +/// card_data: CardData, +/// } +/// +/// /// The door is open, we shouldn't be accepting cards and should be blinking the light +/// #[derive(Debug, Clone, Eq, PartialEq, Hash)] +/// pub struct DoorOpen {} +/// impl DoorOpen { +/// fn on_door_closed(&self) -> CardReaderTransition { +/// TransitionResult::ok(vec![], Locked {}) +/// } +/// } +/// +/// impl Locked { +/// fn on_card_readable(&self, data: CardData) -> CardReaderTransition { +/// TransitionResult::ok( +/// vec![ +/// Commands::ProcessData(data.clone()), +/// Commands::StartBlinkingLight, +/// ], +/// ReadingCard { card_data: data }, +/// ) +/// } +/// } +/// +/// impl ReadingCard { +/// fn on_card_accepted(&self) -> CardReaderTransition { +/// TransitionResult::ok(vec![Commands::StopBlinkingLight], DoorOpen {}) +/// } +/// fn on_card_rejected(&self) -> CardReaderTransition { +/// TransitionResult::ok(vec![Commands::StopBlinkingLight], Locked {}) +/// } +/// } +/// +/// let cr = CardReader::Locked(Locked {}); +/// let (cr, cmds) = cr +/// .on_event(CardReaderEvents::CardReadable("badguy".to_string())) +/// .unwrap(); +/// assert_eq!(cmds[0], Commands::ProcessData("badguy".to_string())); +/// assert_eq!(cmds[1], Commands::StartBlinkingLight); +/// +/// let (cr, cmds) = cr.on_event(CardReaderEvents::CardRejected).unwrap(); +/// assert_eq!(cmds[0], Commands::StopBlinkingLight); +/// +/// let (cr, cmds) = cr +/// .on_event(CardReaderEvents::CardReadable("goodguy".to_string())) +/// .unwrap(); +/// assert_eq!(cmds[0], Commands::ProcessData("goodguy".to_string())); +/// assert_eq!(cmds[1], Commands::StartBlinkingLight); +/// +/// let (_, cmds) = cr.on_event(CardReaderEvents::CardAccepted).unwrap(); +/// assert_eq!(cmds[0], Commands::StopBlinkingLight); +/// ``` +/// +/// In the above example the first word is the name of the state machine, then after the comma the +/// type (which you must define separately) of commands produced by the machine. +/// +/// then each line represents a transition, where the first word is the initial state, the tuple +/// inside the arrow is `(eventtype[, event handler])`, and the word after the arrow is the +/// destination state. here `eventtype` is an enum variant , and `event_handler` is a function you +/// must define outside the enum whose form depends on the event variant. the only variant types +/// allowed are unit and one-item tuple variants. For unit variants, the function takes no +/// parameters. For the tuple variants, the function takes the variant data as its parameter. In +/// either case the function is expected to return a `TransitionResult` to the appropriate state. +/// +/// The first transition can be interpreted as "If the machine is in the locked state, when a +/// `CardReadable` event is seen, call `on_card_readable` (pasing in `CardData`) and transition to +/// the `ReadingCard` state. +/// +/// The macro will generate a few things: +/// * An enum with a variant for each state, named with the provided name. In this case: +/// ```ignore +/// enum CardMachine { +/// Locked(Locked), +/// ReadingCard(ReadingCard), +/// Unlocked(Unlocked), +/// } +/// ``` +/// +/// You are expected to define a type for each state, to contain that state's data. If there is +/// no data, you can simply: `type StateName = ()` +/// * An enum with a variant for each event. You are expected to define the type (if any) contained +/// in the event variant. In this case: +/// ```ignore +/// enum CardMachineEvents { +/// CardReadable(CardData) +/// } +/// ``` +/// * An implementation of the [StateMachine](trait.StateMachine.html) trait for the generated state +/// machine enum (in this case, `CardMachine`) +/// * A type alias for a [TransitionResult](enum.TransitionResult.html) with the appropriate generic +/// parameters set for your machine. It is named as your machine with `Transition` appended. In +/// this case, `CardMachineTransition`. +#[proc_macro] +pub fn fsm(input: TokenStream) -> TokenStream { + let def: StateMachineDefinition = parse_macro_input!(input as StateMachineDefinition); + def.codegen() +} + +struct StateMachineDefinition { + name: Ident, + command_type: Ident, + error_type: Ident, + transitions: HashSet, +} + +impl Parse for StateMachineDefinition { + // TODO: Pub keyword + fn parse(input: ParseStream) -> Result { + // First parse the state machine name, command type, and error type + let (name, command_type, error_type) = parse_first_line(&input).map_err(|mut e| { + e.combine(Error::new( + e.span(), + "The first line of the fsm definition should be `MachineName, CommandType, ErrorType`", + )); + e + })?; + // Then the state machine definition is simply a sequence of transitions separated by + // semicolons + let transitions: Punctuated = + input.parse_terminated(Transition::parse)?; + let transitions = transitions.into_iter().collect(); + Ok(Self { + name, + transitions, + command_type, + error_type, + }) + } +} + +fn parse_first_line(input: &ParseStream) -> Result<(Ident, Ident, Ident)> { + let name: Ident = input.parse()?; + input.parse::()?; + let command_type: Ident = input.parse()?; + input.parse::()?; + let error_type: Ident = input.parse()?; + Ok((name, command_type, error_type)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct Transition { + from: Ident, + to: Ident, + event: Variant, + handler: Option, +} + +impl Parse for Transition { + fn parse(input: ParseStream) -> Result { + // TODO: Use keywords instead of implicit placement, or other better arg-passing method + // maybe `enum MachineName` + // and need start state + // TODO: Currently the handlers are not required to transition to the state they claimed + // they would. It would be great to find a way to fix that. + // TODO: It is desirable to be able to define immutable state the machine is initialized + // with and is accessible from all states. + // Parse the initial state name + let from: Ident = input.parse()?; + // Parse at least one dash + input.parse::()?; + while input.peek(Token![-]) { + input.parse::()?; + } + // Parse transition information inside parens + let transition_info; + parenthesized!(transition_info in input); + // Get the event variant definition + let event: Variant = transition_info.parse()?; + // Reject non-unit or single-item-tuple variants + match &event.fields { + Fields::Named(_) => { + return Err(Error::new( + event.span(), + "Struct variants are not supported for events", + )) + } + Fields::Unnamed(uf) => { + if uf.unnamed.len() != 1 { + return Err(Error::new( + event.span(), + "Only tuple variants with exactly one item are supported for events", + )); + } + } + Fields::Unit => {} + } + // Check if there is an event handler, and parse it + let handler = if transition_info.peek(Token![,]) { + transition_info.parse::()?; + Some(transition_info.parse()?) + } else { + None + }; + // Parse at least one dash followed by the "arrow" + input.parse::()?; + while input.peek(Token![-]) { + input.parse::()?; + } + input.parse::]>()?; + // Parse the destination state + let to: Ident = input.parse()?; + + Ok(Self { + from, + event, + handler, + to, + }) + } +} + +impl StateMachineDefinition { + fn codegen(&self) -> TokenStream { + // First extract all of the states into a set, and build the enum's insides + let states: HashSet<_> = self + .transitions + .iter() + .flat_map(|t| vec![t.from.clone(), t.to.clone()]) + .collect(); + let state_variants = states.iter().map(|s| { + quote! { + #s(#s) + } + }); + let name = &self.name; + let main_enum = quote! { + #[derive(::derive_more::From)] + pub enum #name { + #(#state_variants),* + } + }; + + // Build the events enum + let events: HashSet = self.transitions.iter().map(|t| t.event.clone()).collect(); + let events_enum_name = Ident::new(&format!("{}Events", name), name.span()); + let events: Vec<_> = events.into_iter().collect(); + let events_enum = quote! { + pub enum #events_enum_name { + #(#events),* + } + }; + + // Construct the trait implementation + let cmd_type = &self.command_type; + let err_type = &self.error_type; + let mut statemap: HashMap> = HashMap::new(); + for t in &self.transitions { + statemap + .entry(t.from.clone()) + .and_modify(|v| v.push(t.clone())) + .or_insert_with(|| vec![t.clone()]); + } + // Add any states without any transitions to the map + for s in &states { + if !statemap.contains_key(s) { + statemap.insert(s.clone(), vec![]); + } + } + let state_branches = statemap.iter().map(|(from, transitions)| { + let event_branches = transitions + .iter() + .map(|ts| { + let ev_variant = &ts.event.ident; + if let Some(ts_fn) = ts.handler.clone() { + let span = ts_fn.span(); + match ts.event.fields { + Fields::Unnamed(_) => quote_spanned! {span=> + #events_enum_name::#ev_variant(val) => { + state_data.#ts_fn(val) + } + }, + Fields::Unit => quote_spanned! {span=> + #events_enum_name::#ev_variant => { + state_data.#ts_fn() + } + }, + Fields::Named(_) => unreachable!(), + } + } else { + // If events do not have a handler, attempt to construct the next state + // using `Default`. + let new_state = ts.to.clone(); + let span = new_state.span(); + let default_trans = quote_spanned! {span=> + TransitionResult::from::<#from, #new_state>(state_data) + }; + let span = ts.event.span(); + match ts.event.fields { + Fields::Unnamed(_) => quote_spanned! {span=> + #events_enum_name::#ev_variant(_val) => { + #default_trans + } + }, + Fields::Unit => quote_spanned! {span=> + #events_enum_name::#ev_variant => { + #default_trans + } + }, + Fields::Named(_) => unreachable!(), + } + } + }) + // Since most states won't handle every possible event, return an error to that effect + .chain(std::iter::once( + quote! { _ => { return TransitionResult::InvalidTransition } }, + )); + quote! { + #name::#from(state_data) => match event { + #(#event_branches),* + } + } + }); + + let trait_impl = quote! { + impl ::rustfsm::StateMachine<#name, #events_enum_name, #cmd_type> for #name { + type Error = #err_type; + + fn on_event(self, event: #events_enum_name) + -> ::rustfsm::TransitionResult<#name, Self::Error, #cmd_type> { + match self { + #(#state_branches),* + } + } + + fn state(&self) -> &Self { + &self + } + } + }; + + let transition_result_name = Ident::new(&format!("{}Transition", name), name.span()); + let transition_type_alias = quote! { + type #transition_result_name = TransitionResult<#name, #err_type, #cmd_type>; + }; + + let output = quote! { + #transition_type_alias + #main_enum + #events_enum + #trait_impl + }; + + output.into() + } +} diff --git a/fsm/state_machine_procmacro/tests/progress.rs b/fsm/state_machine_procmacro/tests/progress.rs new file mode 100644 index 000000000..36a59c500 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/progress.rs @@ -0,0 +1,49 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/trybuild/*_pass.rs"); + t.compile_fail("tests/trybuild/*_fail.rs"); +} + +//Kept here to inspect manual expansion +state_machine_procmacro::fsm! { + SimpleMachine, SimpleMachineCommand, Infallible + + One --(A(String), foo)--> Two; + One --(B)--> Two; + Two --(B)--> One; + Two --(C, baz)--> One +} + +#[derive(Default)] +pub struct One {} +impl One { + fn foo(self, _: String) -> SimpleMachineTransition { + TransitionResult::default::() + } +} +impl From for One { + fn from(_: Two) -> Self { + One {} + } +} + +#[derive(Default)] +pub struct Two {} +impl Two { + fn baz(self) -> SimpleMachineTransition { + TransitionResult::default::() + } +} +impl From for Two { + fn from(_: One) -> Self { + Two {} + } +} + +enum SimpleMachineCommand {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.rs b/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.rs new file mode 100644 index 000000000..4c21a01b4 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.rs @@ -0,0 +1,12 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; + +fsm! { + One --(A)--> Two +} + +pub struct One {} +pub struct Two {} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.stderr b/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.stderr new file mode 100644 index 000000000..c7d3f6fff --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/forgot_name_fail.stderr @@ -0,0 +1,11 @@ +error: expected `,` + --> $DIR/forgot_name_fail.rs:6:9 + | +6 | One --(A)--> Two + | ^ + +error: The first line of the fsm definition should be `MachineName, CommandType, ErrorType` + --> $DIR/forgot_name_fail.rs:6:9 + | +6 | One --(A)--> Two + | ^ diff --git a/fsm/state_machine_procmacro/tests/trybuild/handler_arg_pass.rs b/fsm/state_machine_procmacro/tests/trybuild/handler_arg_pass.rs new file mode 100644 index 000000000..1a792e705 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/handler_arg_pass.rs @@ -0,0 +1,27 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +fsm! { + Simple, SimpleCommand, Infallible + + One --(A(String), on_a)--> Two +} + +pub struct One {} +impl One { + fn on_a(self, _: String) -> SimpleTransition { + SimpleTransition::ok(vec![], Two {}) + } +} +pub struct Two {} + +pub enum SimpleCommand {} + +fn main() { + // main enum exists with both states + let _ = Simple::One(One {}); + let _ = Simple::Two(Two {}); +} diff --git a/fsm/state_machine_procmacro/tests/trybuild/handler_pass.rs b/fsm/state_machine_procmacro/tests/trybuild/handler_pass.rs new file mode 100644 index 000000000..30fc7e056 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/handler_pass.rs @@ -0,0 +1,27 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +fsm! { + Simple, SimpleCommand, Infallible + + One --(A, on_a)--> Two +} + +pub struct One {} +impl One { + fn on_a(self) -> SimpleTransition { + SimpleTransition::ok(vec![], Two {}) + } +} +pub struct Two {} + +pub enum SimpleCommand {} + +fn main() { + // main enum exists with both states + let _ = Simple::One(One {}); + let _ = Simple::Two(Two {}); +} diff --git a/fsm/state_machine_procmacro/tests/trybuild/medium_complex_pass.rs b/fsm/state_machine_procmacro/tests/trybuild/medium_complex_pass.rs new file mode 100644 index 000000000..6479bc159 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/medium_complex_pass.rs @@ -0,0 +1,44 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +fsm! { + SimpleMachine, SimpleMachineCommand, Infallible + + One --(A(String), foo)--> Two; + One --(B)--> Two; + Two --(B)--> One; + Two --(C, baz)--> One +} + +#[derive(Default)] +pub struct One {} +impl One { + fn foo(self, _: String) -> SimpleMachineTransition { + TransitionResult::default::() + } +} +impl From for One { + fn from(_: Two) -> Self { + One {} + } +} + +#[derive(Default)] +pub struct Two {} +impl Two { + fn baz(self) -> SimpleMachineTransition { + TransitionResult::default::() + } +} +impl From for Two { + fn from(_: One) -> Self { + Two {} + } +} + +enum SimpleMachineCommand {} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.rs b/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.rs new file mode 100644 index 000000000..8d5080563 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.rs @@ -0,0 +1,27 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +fsm! { + SimpleMachine, SimpleMachineCommand, Infallible + + One --(A)--> Two; + Two --(B)--> One; +} + +pub struct One {} + +pub struct Two {} +// We implement one of them because trait bound satisfaction error output is not deterministically +// ordered +impl From for Two { + fn from(_: One) -> Self { + Two {} + } +} + +enum SimpleMachineCommand {} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.stderr b/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.stderr new file mode 100644 index 000000000..ddb471296 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `One: From` is not satisfied + --> $DIR/no_handle_conversions_require_into_fail.rs:11:18 + | +11 | Two --(B)--> One; + | ^^^ the trait `From` is not implemented for `One` + | + = note: required because of the requirements on the impl of `Into` for `Two` + = note: required by `TransitionResult::::from` diff --git a/fsm/state_machine_procmacro/tests/trybuild/simple_pass.rs b/fsm/state_machine_procmacro/tests/trybuild/simple_pass.rs new file mode 100644 index 000000000..46fe99ae7 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/simple_pass.rs @@ -0,0 +1,30 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; +use state_machine_trait::TransitionResult; +use std::convert::Infallible; + +fsm! { + SimpleMachine, SimpleMachineCommand, Infallible + + One --(A)--> Two +} + +pub struct One {} + +pub struct Two {} +impl From for Two { + fn from(_: One) -> Self { + Two {} + } +} + +pub enum SimpleMachineCommand {} + +fn main() { + // main enum exists with both states + let _ = SimpleMachine::One(One {}); + let _ = SimpleMachine::Two(Two {}); + // Event enum exists + let _ = SimpleMachineEvents::A; +} diff --git a/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.rs b/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.rs new file mode 100644 index 000000000..d2d9bca50 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.rs @@ -0,0 +1,16 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; + +fsm! { + Simple, SimpleCommand, Infallible + + One --(A{foo: String}, on_a)--> Two +} + +pub struct One {} +pub struct Two {} + +pub enum SimpleCommand {} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.stderr b/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.stderr new file mode 100644 index 000000000..d89dbdc93 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/struct_event_variant_fail.stderr @@ -0,0 +1,5 @@ +error: Struct variants are not supported for events + --> $DIR/struct_event_variant_fail.rs:8:12 + | +8 | One --(A{foo: String}, on_a)--> Two + | ^ diff --git a/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.rs b/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.rs new file mode 100644 index 000000000..f15eb6db7 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.rs @@ -0,0 +1,11 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; + +fsm! { + Simple, SimpleCmd, Infallible + + One --(A(Foo, Bar), on_a)--> Two +} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.stderr b/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.stderr new file mode 100644 index 000000000..a141f0023 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/tuple_more_item_event_variant_fail.stderr @@ -0,0 +1,5 @@ +error: Only tuple variants with exactly one item are supported for events + --> $DIR/tuple_more_item_event_variant_fail.rs:8:12 + | +8 | One --(A(Foo, Bar), on_a)--> Two + | ^ diff --git a/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.rs b/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.rs new file mode 100644 index 000000000..fd1d885b7 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.rs @@ -0,0 +1,11 @@ +extern crate state_machine_trait as rustfsm; + +use state_machine_procmacro::fsm; + +fsm! { + Simple, SimpleCmd, Infallible + + One --(A(), on_a)--> Two +} + +fn main() {} diff --git a/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.stderr b/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.stderr new file mode 100644 index 000000000..5a5cfaee0 --- /dev/null +++ b/fsm/state_machine_procmacro/tests/trybuild/tuple_zero_item_event_variant_fail.stderr @@ -0,0 +1,5 @@ +error: Only tuple variants with exactly one item are supported for events + --> $DIR/tuple_zero_item_event_variant_fail.rs:8:12 + | +8 | One --(A(), on_a)--> Two + | ^ diff --git a/fsm/state_machine_trait/Cargo.toml b/fsm/state_machine_trait/Cargo.toml new file mode 100644 index 000000000..a0f00ae92 --- /dev/null +++ b/fsm/state_machine_trait/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "state_machine_trait" +version = "0.1.0" +authors = ["Spencer Judge "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/fsm/state_machine_trait/src/lib.rs b/fsm/state_machine_trait/src/lib.rs new file mode 100644 index 000000000..6efe99644 --- /dev/null +++ b/fsm/state_machine_trait/src/lib.rs @@ -0,0 +1,84 @@ +use std::error::Error; + +/// This trait defines a state machine (more formally, a [finite state +/// transducer](https://en.wikipedia.org/wiki/Finite-state_transducer)) which accepts events (the +/// input alphabet), uses them to mutate itself, and (may) output some commands (the output +/// alphabet) as a result. +/// +/// The `State`, `Event`, and `Command` type parameters will generally be enumerations +pub trait StateMachine { + /// The error type produced by this state machine when handling events + type Error: Error; + + /// Handle an incoming event + fn on_event(self, event: Event) -> TransitionResult; + + /// Returns the current state of the machine + fn state(&self) -> &State; +} + +// TODO: Likely need to return existing state with invalid trans/err +/// A transition result is emitted every time the [StateMachine] handles an event. +pub enum TransitionResult { + /// This state does not define a transition for this event from this state. All other errors + /// should use the [Err](enum.TransitionResult.html#variant.Err) variant. + InvalidTransition, + /// The transition was successful + Ok { + commands: Vec, + new_state: StateMachine, + }, + /// There was some error performing the transition + Err(StateMachineError), +} + +impl TransitionResult { + /// Produce a transition with the provided commands to the provided state + pub fn ok(commands: CI, new_state: IS) -> Self + where + CI: IntoIterator, + IS: Into, + { + Self::Ok { + commands: commands.into_iter().collect(), + new_state: new_state.into(), + } + } + + /// Produce a transition with no commands relying on [Default] for the destination state's + /// value + pub fn default() -> Self + where + DestState: Into + Default, + { + Self::Ok { + commands: vec![], + new_state: DestState::default().into(), + } + } + + /// Uses `Into` to produce a transition with no commands from the provided current state to + /// the provided (by type parameter) destination state. + pub fn from(current_state: CurrentState) -> Self + where + DestState: Into, + CurrentState: Into, + { + let as_dest: DestState = current_state.into(); + Self::Ok { + commands: vec![], + new_state: as_dest.into(), + } + } + + // TODO: Make test only or something? + pub fn unwrap(self) -> (S, Vec) { + match self { + Self::Ok { + commands, + new_state, + } => (new_state, commands), + _ => panic!("Transition was not successful!"), + } + } +} diff --git a/fsm/tests/card_reader.rs b/fsm/tests/card_reader.rs new file mode 100644 index 000000000..23fd44e78 --- /dev/null +++ b/fsm/tests/card_reader.rs @@ -0,0 +1,139 @@ +//! We'll imagine a (idealized) card reader which unlocks a door / blinks a light when it's open +//! +//! This is the by-hand version, useful to compare to the macro version in the docs + +use state_machine_trait::{StateMachine, TransitionResult}; + +#[derive(Clone)] +pub enum CardReader { + Locked(Locked), + ReadingCard(ReadingCard), + Unlocked(DoorOpen), +} + +#[derive(thiserror::Error, Debug)] +pub enum CardReaderError {} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum CardReaderEvents { + /// Someone's presented a card for reading + CardReadable(CardData), + /// Door latch connected + DoorClosed, + CardAccepted, + CardRejected, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Commands { + StartBlinkingLight, + StopBlinkingLight, + ProcessData(CardData), +} + +type CardData = String; + +impl CardReader { + /// Reader starts locked + pub fn new() -> Self { + CardReader::Locked(Locked {}) + } +} + +impl StateMachine for CardReader { + type Error = CardReaderError; + + fn on_event(self, event: CardReaderEvents) -> TransitionResult { + let mut commands = vec![]; + let new_state = match self { + CardReader::Locked(ls) => match event { + CardReaderEvents::CardReadable(data) => { + commands.push(Commands::ProcessData(data.clone())); + commands.push(Commands::StartBlinkingLight); + Self::ReadingCard(ls.on_card_readable(data)) + } + _ => return TransitionResult::InvalidTransition, + }, + CardReader::ReadingCard(rc) => match event { + CardReaderEvents::CardAccepted => { + commands.push(Commands::StopBlinkingLight); + Self::Unlocked(rc.on_card_accepted()) + } + CardReaderEvents::CardRejected => { + commands.push(Commands::StopBlinkingLight); + Self::Locked(rc.on_card_rejected()) + } + _ => return TransitionResult::InvalidTransition, + }, + CardReader::Unlocked(_) => match event { + CardReaderEvents::DoorClosed => Self::Locked(Locked {}), + _ => return TransitionResult::InvalidTransition, + }, + }; + TransitionResult::Ok { + commands, + new_state, + } + } + + fn state(&self) -> &CardReader { + self + } +} + +/// Door is locked / idle / we are ready to read +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Locked {} + +/// Actively reading the card +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ReadingCard { + card_data: CardData, +} + +/// The door is open, we shouldn't be accepting cards and should be blinking the light +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct DoorOpen {} + +impl Locked { + fn on_card_readable(&self, data: CardData) -> ReadingCard { + ReadingCard { card_data: data } + } +} + +impl ReadingCard { + fn on_card_accepted(&self) -> DoorOpen { + DoorOpen {} + } + fn on_card_rejected(&self) -> Locked { + Locked {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Should be kept the same the main example doctest + #[test] + fn run_a_card_reader() { + let cr = CardReader::Locked(Locked {}); + let (cr, cmds) = cr + .on_event(CardReaderEvents::CardReadable("badguy".to_string())) + .unwrap(); + assert_eq!(cmds[0], Commands::ProcessData("badguy".to_string())); + assert_eq!(cmds[1], Commands::StartBlinkingLight); + + let (cr, cmds) = cr.on_event(CardReaderEvents::CardRejected).unwrap(); + assert_eq!(cmds[0], Commands::StopBlinkingLight); + + let (cr, cmds) = cr + .on_event(CardReaderEvents::CardReadable("goodguy".to_string())) + .unwrap(); + assert_eq!(cmds[0], Commands::ProcessData("goodguy".to_string())); + assert_eq!(cmds[1], Commands::StartBlinkingLight); + + let (_, cmds) = cr.on_event(CardReaderEvents::CardAccepted).unwrap(); + assert_eq!(cmds[0], Commands::StopBlinkingLight); + } +} diff --git a/src/lib.rs b/src/lib.rs index c9c5fb531..829db137a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod machines; pub mod protos; use protos::coresdk::{CompleteSdkTaskReq, CompleteSdkTaskResp, PollSdkTaskReq, PollSdkTaskResp}; diff --git a/src/machines/activity_state_machine.rs b/src/machines/activity_state_machine.rs new file mode 100644 index 000000000..27ee45ed7 --- /dev/null +++ b/src/machines/activity_state_machine.rs @@ -0,0 +1,192 @@ +use rustfsm::{fsm, TransitionResult}; + +// Schedule / cancel are "explicit events" (imperative rather than past events?) + +fsm! { + ActivityMachine, ActivityCommand, ActivityMachineError + + Created --(Schedule, on_schedule)--> ScheduleCommandCreated; + + ScheduleCommandCreated --(CommandScheduleActivityTask) --> ScheduleCommandCreated; + ScheduleCommandCreated + --(ActivityTaskScheduled, on_activity_task_scheduled) --> ScheduleEventRecorded; + ScheduleCommandCreated --(Cancel, on_canceled) --> Canceled; + + ScheduleEventRecorded --(ActivityTaskStarted, on_task_started) --> Started; + ScheduleEventRecorded --(ActivityTaskTimedOut, on_task_timed_out) --> TimedOut; + ScheduleEventRecorded --(Cancel, on_canceled) --> ScheduledActivityCancelCommandCreated; + + Started --(ActivityTaskCompleted, on_activity_task_completed) --> Completed; + Started --(ActivityTaskFailed, on_activity_task_failed) --> Failed; + Started --(ActivityTaskTimedOut, on_activity_task_timed_out) --> TimedOut; + Started --(Cancel, on_canceled) --> StartedActivityCancelCommandCreated; + + ScheduledActivityCancelCommandCreated + --(CommandRequestCancelActivityTask, + on_command_request_cancel_activity_task) --> ScheduledActivityCancelCommandCreated; + ScheduledActivityCancelCommandCreated + --(ActivityTaskCancelRequested) --> ScheduledActivityCancelEventRecorded; + + ScheduledActivityCancelEventRecorded + --(ActivityTaskCanceled, on_activity_task_canceled) --> Canceled; + ScheduledActivityCancelEventRecorded + --(ActivityTaskStarted) --> StartedActivityCancelEventRecorded; + ScheduledActivityCancelEventRecorded + --(ActivityTaskTimedOut, on_activity_task_timed_out) --> TimedOut; + + StartedActivityCancelCommandCreated + --(CommandRequestCancelActivityTask) --> StartedActivityCancelCommandCreated; + StartedActivityCancelCommandCreated + --(ActivityTaskCancelRequested, + on_activity_task_cancel_requested) --> StartedActivityCancelEventRecorded; + + StartedActivityCancelEventRecorded --(ActivityTaskFailed, on_activity_task_failed) --> Failed; + StartedActivityCancelEventRecorded + --(ActivityTaskCompleted, on_activity_task_completed) --> Completed; + StartedActivityCancelEventRecorded + --(ActivityTaskTimedOut, on_activity_task_timed_out) --> TimedOut; + StartedActivityCancelEventRecorded + --(ActivityTaskCanceled, on_activity_task_canceled) --> Canceled; +} + +#[derive(thiserror::Error, Debug)] +pub enum ActivityMachineError {} +pub enum ActivityCommand {} + +#[derive(Default)] +pub struct Created {} +impl Created { + pub fn on_schedule(self) -> ActivityMachineTransition { + // would add command here + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct ScheduleCommandCreated {} +impl ScheduleCommandCreated { + pub fn on_activity_task_scheduled(self) -> ActivityMachineTransition { + // set initial command event id + // this.initialCommandEventId = currentEvent.getEventId(); + ActivityMachineTransition::default::() + } + pub fn on_canceled(self) -> ActivityMachineTransition { + // cancelCommandNotifyCanceled + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct ScheduleEventRecorded {} +impl ScheduleEventRecorded { + pub fn on_task_started(self) -> ActivityMachineTransition { + // setStartedCommandEventId + ActivityMachineTransition::default::() + } + pub fn on_task_timed_out(self) -> ActivityMachineTransition { + // notify_timed_out + ActivityMachineTransition::default::() + } + pub fn on_canceled(self) -> ActivityMachineTransition { + // createRequestCancelActivityTaskCommand + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct Started {} +impl Started { + pub fn on_activity_task_completed(self) -> ActivityMachineTransition { + // notify_completed + ActivityMachineTransition::default::() + } + pub fn on_activity_task_failed(self) -> ActivityMachineTransition { + // notify_failed + ActivityMachineTransition::default::() + } + pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + // notify_timed_out + ActivityMachineTransition::default::() + } + pub fn on_canceled(self) -> ActivityMachineTransition { + // createRequestCancelActivityTaskCommand + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct ScheduledActivityCancelCommandCreated {} +impl ScheduledActivityCancelCommandCreated { + pub fn on_command_request_cancel_activity_task(self) -> ActivityMachineTransition { + // notifyCanceledIfTryCancel + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct ScheduledActivityCancelEventRecorded {} +impl ScheduledActivityCancelEventRecorded { + pub fn on_activity_task_canceled(self) -> ActivityMachineTransition { + // notify_canceled + ActivityMachineTransition::default::() + } + pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + // notify_timed_out + ActivityMachineTransition::default::() + } +} +impl From for ScheduledActivityCancelEventRecorded { + fn from(_: ScheduledActivityCancelCommandCreated) -> Self { + Self::default() + } +} + +#[derive(Default)] +pub struct StartedActivityCancelCommandCreated {} +impl StartedActivityCancelCommandCreated { + pub fn on_activity_task_cancel_requested(self) -> ActivityMachineTransition { + // notifyCanceledIfTryCancel + ActivityMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct StartedActivityCancelEventRecorded {} +impl StartedActivityCancelEventRecorded { + pub fn on_activity_task_completed(self) -> ActivityMachineTransition { + // notify_completed + ActivityMachineTransition::default::() + } + pub fn on_activity_task_failed(self) -> ActivityMachineTransition { + // notify_failed + ActivityMachineTransition::default::() + } + pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + // notify_timed_out + ActivityMachineTransition::default::() + } + pub fn on_activity_task_canceled(self) -> ActivityMachineTransition { + // notifyCancellationFromEvent + ActivityMachineTransition::default::() + } +} +impl From for StartedActivityCancelEventRecorded { + fn from(_: ScheduledActivityCancelEventRecorded) -> Self { + Self::default() + } +} + +#[derive(Default)] +pub struct Completed {} +#[derive(Default)] +pub struct Failed {} +#[derive(Default)] +pub struct TimedOut {} +#[derive(Default)] +pub struct Canceled {} + +#[cfg(test)] +mod activity_machine_tests { + #[test] + fn test() {} +} diff --git a/src/machines/mod.rs b/src/machines/mod.rs new file mode 100644 index 000000000..5912a65a5 --- /dev/null +++ b/src/machines/mod.rs @@ -0,0 +1,6 @@ +// TODO: Use! +#[allow(unused)] +mod activity_state_machine; +// TODO: Use! +#[allow(unused)] +mod timer_state_machine; diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs new file mode 100644 index 000000000..772ed4506 --- /dev/null +++ b/src/machines/timer_state_machine.rs @@ -0,0 +1,82 @@ +use rustfsm::{fsm, TransitionResult}; + +fsm! { + TimerMachine, TimerCommand, TimerMachineError + + Created --(Schedule, on_schedule)--> StartCommandCreated; + + StartCommandCreated --(CommandStartTimer) --> StartCommandCreated; + StartCommandCreated --(TimerStarted, on_timer_started) --> StartCommandRecorded; + StartCommandCreated --(Cancel, on_cancel) --> Canceled; + + StartCommandRecorded --(TimerFired, on_fired) --> Fired; + StartCommandRecorded --(Cancel, on_cancel) --> CancelTimerCommandCreated; + + CancelTimerCommandCreated --(Cancel) --> CancelTimerCommandCreated; + CancelTimerCommandCreated --(CommandTypeCancelTimer, on_cancel) --> CancelTimerCommandSent; + + CancelTimerCommandSent --(TimerCanceled) --> Canceled; +} + +#[derive(thiserror::Error, Debug)] +pub enum TimerMachineError {} +pub enum TimerCommand {} + +#[derive(Default)] +pub struct Created {} +impl Created { + pub fn on_schedule(self) -> TimerMachineTransition { + // would add command here + TimerMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct StartCommandCreated {} +impl StartCommandCreated { + pub fn on_timer_started(self) -> TimerMachineTransition { + TimerMachineTransition::default::() + } + pub fn on_cancel(self) -> TimerMachineTransition { + TimerMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct StartCommandRecorded {} +impl StartCommandRecorded { + pub fn on_cancel(self) -> TimerMachineTransition { + TimerMachineTransition::default::() + } + pub fn on_fired(self) -> TimerMachineTransition { + TimerMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct CancelTimerCommandCreated {} +impl CancelTimerCommandCreated { + pub fn on_cancel(self) -> TimerMachineTransition { + TimerMachineTransition::default::() + } +} + +#[derive(Default)] +pub struct CancelTimerCommandSent {} + +#[derive(Default)] +pub struct Fired {} + +#[derive(Default)] +pub struct Canceled {} +impl From for Canceled { + fn from(_: CancelTimerCommandSent) -> Self { + Canceled::default() + } +} + +#[cfg(test)] +mod activity_machine_tests { + #[test] + fn test() {} +}