An open API service indexing awesome lists of open source software.

https://github.com/fitzgen/state_machine_future

Easily create type-safe `Future`s from state machines — without the boilerplate.
https://github.com/fitzgen/state_machine_future

Last synced: over 1 year ago
JSON representation

Easily create type-safe `Future`s from state machines — without the boilerplate.

Awesome Lists containing this project

README

          

# `state_machine_future`

[![](https://docs.rs/state_machine_future/badge.svg)](https://docs.rs/state_machine_future/) [![](https://img.shields.io/crates/v/state_machine_future.svg)](https://crates.io/crates/state_machine_future) [![](https://img.shields.io/crates/d/state_machine_future.png)](https://crates.io/crates/state_machine_future) [![Build Status](https://travis-ci.org/fitzgen/state_machine_future.png?branch=master)](https://travis-ci.org/fitzgen/state_machine_future)

Easily create type-safe `Future`s from state machines — without the boilerplate.

`state_machine_future` type checks state machines and their state transitions,
and then generates `Future` implementations and typestate[0][]
boilerplate for you.

* [Introduction](#introduction)
* [Guide](#guide)
* [Example](#example)
* [Attributes](#attributes)
* [Macro](#macro)
* [Features](#features)
* [License](#license)
* [Contribution](#contribution)

### 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
*typestates*[0][]: 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:

* A typestate for each state in the state machine.

* A type for the whole state machine that implements `Future`.

* A concrete `start` method that constructs the state machine `Future` for you,
initialized to its start state.

* A state transition polling trait, with a `poll_zee_choo` method for each
non-final state `ZeeChoo`. This trait describes the state machine's valid
transitions, and its methods are called by `Future::poll`.

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:

* Every state is reachable from the start state: *there are no useless states.*

* *There are no states which cannot reach a final state*. These states would
otherwise lead to infinite loops.

* *All state transitions are valid.* Attempting to make an invalid state
transition fails to type check, thanks to the generated typestates.

### 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 {
#[state_machine_future(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 {
#[state_machine_future(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:

* A typestate for the state. The type's name matches the variant name, for
example the `Intermediate` state variant's typestate is also named `Intermediate`.
The kind of struct type generated matches the variant kind: a unit-style variant
results in a unit struct, a tuple-style variant results in a tuple struct, and a
struct-style variant results in a normal struct with fields.

| 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 };` |

* An `enum` for the possible states that can come after this state. This `enum`
is named `AfterX` where `X` is the state's name. There is also a `From`
implementation for each `Y` state that can be transitioned to after `X`. For
example, the `Intermediate` state would get:

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

impl From for AfterIntermediate {
// ...
}

impl From for AfterIntermediate {
// ...
}
```

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

* A state machine `Future` type, which is essentially an `enum` of all the
different typestates. This type is named `BlahFuture` where `Blah` is the name
of the state machine description `enum`. In this example, where the state
machine description is named `MyStateMachine`, the generated state machine
future type would be named `MyStateMachineFuture`.

* A polling trait, `PollBordle` where `Bordle` is this state machine
description's name. For each non-final state `TootWasabi`, this trait has a
method, `poll_toot_wasabi`, which is like `Future::poll` but specialized to the
current state. Each method takes conditional ownership of its state (via
[`RentToOwn`][rent_to_own]) and returns a `futures::Poll`
where `Error` is the state machine's error type. This signature *does not allow
invalid state transitions*, which makes attempting an illegal state transition
fail to type check. Here is the `MyStateMachine`'s polling trait, for example:

```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;
}
```

* An implementation of `Future` for that type. This implementation dispatches to
the appropriate polling trait method depending on what state the future is
in:

* If the `Future` is in the `Start` state, then it uses `::poll_start`.

* If it is in the `Intermediate` state, then it uses `::poll_intermediate`.

* Etc...

* A concrete `start` method for the description type (so `MyStateMachine::start`
in this example) which constructs a new state machine `Future` type in its
**start** state for you. This method has a parameter for each field in the
**start** state variant.

| 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 {
// Call `try_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, return `Ok(Async::NotReady)`. If we encounter an error,
// return `Err(...)`.
}

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

### Context

The state machine also allows to pass in a context that is available in every `poll_*` method
without having to explicitly include it in every one.

The context can be specified through the `context` argument of the `state_machine_future` attribute.
This will add parameters to the `start` method as well as to each `poll_*` method of the trait.

```rust
#[macro_use]
extern crate state_machine_future;
extern crate futures;

use futures::*;
use state_machine_future::*;

struct MyContext {

}

struct MyItem {

}

enum MyError {

}

#[derive(StateMachineFuture)]
#[state_machine_future(context = "MyContext")]
enum MyStateMachine {
#[state_machine_future(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),
}

impl PollMyStateMachine for MyStateMachine {
fn poll_start<'s, 'c>(
start: &'s mut RentToOwn<'s, Start>,
context: &'c mut RentToOwn<'c, MyContext>
) -> Poll {

// The `context` instance passed into `start` is available here.
// It is a mutable reference, so are free to modify it.

unimplemented!()
}

fn poll_intermediate<'s, 'c>(
intermediate: &'s mut RentToOwn<'s, Intermediate>,
context: &'c mut RentToOwn<'c, MyContext>
) -> Poll {

// The `context` is available here as well.
// It is the same instance. This means if `poll_start` modified it, those
// changes will be visible to this method as well.

unimplemented!()
}
}

fn main() {
let _ = MyStateMachine::start(MyContext { });
}
```

Same as for the state argument, the context can be taken through the `RentToOwn` type!
However, be aware that once you take the context, the state machine will **always** return
`Async::NotReady` **without** invoking the `poll_` methods anymore. The one exception to
this is when the state machine is in a ready or error state, where it will resolve normally
when polled if the context has been taken.

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 state_machine_future;

#[macro_use]
extern crate futures;

use futures::{Async, Future, Poll};
use state_machine_future::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.
#[state_machine_future(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 poll_invite<'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.
try_ready!(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();
let waiting = WaitingForTurn {
turn: invite.from.request_turn(),
active: invite.from,
idle: invite.to,
};
transition!(waiting)
}

fn poll_waiting_for_turn<'a>(
waiting: &'a mut RentToOwn<'a, WaitingForTurn>
) -> Poll {
// 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) {
transition!(Finished(game_result))
} else {
let next_waiting = WaitingForTurn {
turn: waiting.idle.request_turn(),
active: waiting.idle,
idle: waiting.active,
};
Ok(Async::Ready(next_waiting.into()))
}
}
}

// 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 spawn_game(handle: TokioHandle) {
let from = get_some_player();
let to = get_another_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`:

* `#[derive(StateMachineFuture)]`: Placed on an `enum` that describes a state
machine.

* `#[state_machine_future(derive(Clone, Debug, ...))]`: Placed on the `enum`
that describes the state machine. This attribute describes which
`#[derive(...)]`s to place on the generated `Future` type.

* `#[state_machine_future(start)]`: Used on a variant of the state machine
description `enum`. There must be exactly one variant with this attribute. This
describes the initial starting state. The generated `start` method has a
parameter for each field in this variant.

* `#[state_machine_future(ready)]`: Used on a variant of the state machine
description `enum`. There must be exactly one variant with this attribute. It
must be a tuple-style variant with one field, for example `Ready(MyItemType)`.
The generated `Future` implementation uses the field's type as `Future::Item`.

* `#[state_machine_future(error)]`: Used on a variant of the state machine
description `enum`. There must be exactly one variant with this attribute. It
must be a tuple-style variant with one field, for example `Error(MyError)`. The
generated `Future` implementation uses the field's type as `Future::Error`.

* `#[state_machine_future(transitions(OtherState, AnotherState, ...))]`: Used on
a variant of the state machine description `enum`. Describes the states that
this one can transition to.

### Macro

An auxiliary macro is provided that helps reducing boilerplate code for state
transitions. So, the following code:

```Ok(Ready(NextState(1).into()))```

Can be reduced to:

```transition!(NextState(1))```

### Features

Here are the `cargo` features that you can enable:

* `debug_code_generation`: Prints the code generated by
`#[derive(StateMachineFuture)]` to `stdout` for debugging purposes.

### License

Licensed under either of

* [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)

* [MIT license](http://opensource.org/licenses/MIT)

at your option.

### Contribution

See
[CONTRIBUTING.md](https://github.com/fitzgen/state_machine_future/blob/master/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.

[0]: https://en.wikipedia.org/wiki/Typestate_analysis

[rent_to_own]: https://crates.io/crates/rent_to_own