state_machine_future

Build Status

Easily create type-safe Futures from state machines — without the boilerplate.

state_machine_future type checks state machines and their state transitions, and then generates Future implementations and typestate0 boilerplate for you.

Introduction

Most of the time, using Future combinators like map and then are a great way to describe an asynchronous computation. Other times, the most natural way to describe the process at hand is a state machine.

When writing state machines in Rust, we want to leverage the type system to enforce that only valid state transitions may occur. To do that, we want typestates0: types that represents each state in the state machine, and methods whose signatures only permit valid state transitions. But we also need an enum of every possible state, so we can treat the whole state machine as a single entity, and implement Future for it. But this is getting to be a lot of boilerplate...

Enter #[derive(StateMachineFuture)].

With #[derive(StateMachineFuture)], we describe the states and the possible transitions between them, and then the custom derive generates:

Then, all we need to do is implement the generated state transition polling trait.

Additionally, #[derive(StateMachineFuture)] will statically prevent against some footguns that can arise when writing state machines:

Guide

Describe the state machine's states with an enum and add #[derive(StateMachineFuture)] to it:

```rust

[derive(StateMachineFuture)]

enum MyStateMachine { // ... } ```

There must be one start state, which is the initial state upon construction; one ready state, which corresponds to Future::Item; and one error state, which corresponds to Future::Error.

```rust

[derive(StateMachineFuture)]

enum MyStateMachine { #[statemachinefuture(start)] Start,

// ...

#[state_machine_future(ready)]
Ready(MyItem),

#[state_machine_future(error)]
Error(MyError),

} ```

Any other variants of the enum are intermediate states.

We define which state-to-state transitions are valid with #[state_machine_future(transitions(...))]. This attribute annotates a state variant, and lists which other states can be transitioned to immediately after this state.

A final state (either ready or error) must be reachable from every intermediate state and the start state. Final states are not allowed to have transitions.

```rust

[derive(StateMachineFuture)]

enum MyStateMachine { #[statemachinefuture(start, transitions(Intermediate))] Start,

#[state_machine_future(transitions(Start, Ready))]
Intermediate { x: usize, y: usize },

#[state_machine_future(ready)]
Ready(MyItem),

#[state_machine_future(error)]
Error(MyError),

} ```

From this state machine description, the custom derive generates boilerplate for us.

For each state, the custom derive creates:

| State enum Variant | Generated Typestate | | ------------------------------------------------- | ------------------------------ | | enum StateMachine { MyState, ... } | struct MyState; | | enum StateMachine { MyState(bool, usize), ... } | struct MyState(bool, usize); | | enum StateMachine { MyState { x: usize }, ... } | struct MyState { x: usize }; |

rust enum AfterIntermediate { Start(Start), Ready(Ready), }

Next, for the state machine as a whole, the custom derive generates:

```rust trait PollMyStateMachine { fn poll_start<'a>( start: &'a mut RentToOwn<'a, Start>, ) -> Poll;

fn poll_intermediate<'a>(
    intermediate: &'a mut RentToOwn<'a, Intermediate>,
) -> Poll<AfterIntermediate, Error>;

} ```

| Start enum Variant | Generated start Method | | ------------------------------- | ------------------------------------------------------------------- | | MyStart, | fn start() -> MyStateMachineFuture { ... } | | MyStart(bool, usize), | fn start(arg0: bool, arg1: usize) -> MyStateMachineFuture { ... } | | MyStart { x: char, y: bool }, | fn start(x: char, y: bool) -> MyStateMachineFuture { ... } |

Given all those generated types and traits, all we have to do is impl PollBlah for Blah for our state machine Blah.

``rust impl PollMyStateMachine for MyStateMachine { fn poll_start<'a>( start: &'a mut RentToOwn<'a, Start> ) -> Poll<AfterStart, MyError> { // Calltry_ready!(start.inner.poll())with any inner futures here. // // If we're ready to transition states, then we should return //Ok(Async::Ready(AfterStart)). If we are not ready to transition // states, returnOk(Async::NotReady). If we encounter an error, // returnErr(...)`. }

fn poll_intermediate<'a>(
    intermediate: &'a mut RentToOwn<'a, Intermediate>
) -> Poll<AfterIntermediate, MyError> {
    // Same deal as above...
}

} ```

That's it!

Example

Here is an example of a simple turn-based game played by two players over HTTP.

```rust

[macro_use]

extern crate statemachinefuture;

[macro_use]

extern crate futures;

use futures::{Async, Future, Poll}; use statemachinefuture::RentToOwn;

/// The result of a game. pub struct GameResult { winner: Player, loser: Player, }

/// Some kind of simple turn based game. /// /// text /// Invite /// | /// | /// | accept invitation /// | /// | /// V /// WaitingForTurn --------+ /// | ^ | /// | | | receive turn /// | | | /// | +-------------+ /// game concludes | /// | /// | /// | /// V /// Finished ///

[derive(StateMachineFuture)]

enum Game { /// The game begins with an invitation to play from one player to another. /// /// Once the invited player accepts the invitation over HTTP, then we will /// switch states into playing the game, waiting to recieve each turn. #[statemachinefuture(start, transitions(WaitingForTurn))] Invite { invitation: HttpInvitationFuture, from: Player, to: Player, },

// We are waiting on a turn.
//
// Upon receiving it, if the game is now complete, then we go to the
// `Finished` state. Otherwise, we give the other player a turn.
#[state_machine_future(transitions(WaitingForTurn, Finished))]
WaitingForTurn {
    turn: HttpTurnFuture,
    active: Player,
    idle: Player,
},

// The game is finished with a `GameResult`.
//
// The `GameResult` becomes the `Future::Item`.
#[state_machine_future(ready)]
Finished(GameResult),

// Any state transition can implicitly go to this error state if we get an
// `HttpError` while waiting on a turn or invitation acceptance.
//
// This `HttpError` is used as the `Future::Error`.
#[state_machine_future(error)]
Error(HttpError),

}

// Now, we implement the generated state transition polling trait for our state // machine description type.

impl PollGame for Game { fn pollinvite<'a>( invite: &'a mut RentToOwn<'a, Invite> ) -> Poll { // See if the invitation has been accepted. If not, this will early // return with Ok(Async::NotReady) or propagate any HTTP errors. tryready!(invite.invitation.poll());

    // We're ready to transition into the `WaitingForTurn` state, so take
    // ownership of the `Invite` and then construct and return the new
    // state.
    let invite = invite.take();
    Ok(Async::Ready(AfterInvite::WaitingForTurn(WaitingForTurn {
        turn: invite.from.request_turn(),
        active: invite.from,
        idle: invite.to,
    })))
}

fn poll_waiting_for_turn<'a>(
    waiting: &'a mut RentToOwn<'a, WaitingForTurn>
) -> Poll<AfterWaitingForTurn, HttpError> {
    // See if the next turn has arrived over HTTP. Again, this will early
    // return `Ok(Async::NotReady)` if the turn hasn't arrived yet, and
    // propagate any HTTP errors that we might encounter.
    let turn = try_ready!(waiting.turn.poll());

    // Ok, we have a new turn. Take ownership of the `WaitingForTurn` state,
    // process the turn and if the game is over, then transition to the
    // `Finished` state, otherwise swap which player we need a new turn from
    // and request the turn over HTTP.
    let waiting = waiting.take();
    if let Some(game_result) = process_turn(turn) {
        Ok(Async::Ready(AfterWaitingForTurn::Finished(Finished(game_result))))
    } else {
        Ok(Async::Ready(AfterWaitingForTurn::WaitingForTurn(WaitingForTurn {
            turn: waiting.idle.request_turn(),
            active: waiting.idle,
            idle: waiting.active,
        })))
    }
}

}

// To spawn a new Game as a Future on whatever executor we're using (for // example tokio), we use Game::start to construct the Future in its start // state and then pass it to the executor. fn spawngame(handle: TokioHandle) { let from = getsomeplayer(); let to = getanother_player(); let invitation = invite(&from, &to); let future = Game::start(invitation, from, to); handle.spawn(future) } ```

Attributes

This is a list of all of the attributes used by state_machine_future:

Features

Here are the cargo features that you can enable:

License

Licensed under either of

at your option.

Contribution

See CONTRIBUTING.md for hacking.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.