# `Crank`
[🔗](https://github.com/code-of-kai/crank/blob/v0.2.0/lib/crank.ex#L1)

## What this is

Crank lets you build a state machine as ordinary data. The machine is a
struct, `%Crank.Machine{}`, which holds the current state, whatever data
you've accumulated, and any side effects the last transition declared. To
advance the machine, you call `Crank.crank(machine, event)`. You get back
a new struct. That's the whole interface.

There's no process involved, no message passing, no supervision tree.
It's a function that takes a struct and an event and returns a new struct.
You can call it in a test, in a LiveView, in an Oban worker, in a script.
Anywhere you can call a function.

## What you write

A Crank module is a set of functions. Crank calls them at the right
moments — Elixir calls this pattern "callbacks." Define the function;
the library calls it back when something happens. The functions are
never called directly.

There are three callbacks:

**`init/1`** — Crank calls this once when the machine is created. It
returns the starting state and any initial data:

    def init(opts) do
      {:ok, :idle, %{price: opts[:price] || 100, balance: 0}}
    end

**`handle/3`** — Crank calls this every time an event arrives, passing
the event, the current state, and the accumulated data. It returns the
next state:

    def handle({:coin, amount}, :idle, data) do
      {:next_state, :accepting, %{data | balance: amount}}
    end

    def handle({:coin, amount}, :accepting, data) do
      {:next_state, :accepting, %{data | balance: data.balance + amount}}
    end

Each function clause is one transition. Read it like a sentence: "When a
coin arrives and we're idle, move to accepting and record the amount."
The set of all clauses is the complete specification of the state machine.
There's nothing else to configure, no tables to fill in, no DSL to learn.

**`on_enter/3`** — Optional. Crank calls this after a state change,
passing the old state, the new state, and the data. Useful for recording
that a transition happened — a timestamp, a counter, a log entry —
without cluttering the transition logic itself.

That's everything. The rest is pattern matching.

## The struct

After each `crank/2` call, you get back a `%Crank.Machine{}` with five
fields:

  * `module` — the callback module (so the struct knows which functions to call)
  * `state` — the current state (an atom, a struct, a tuple — any Elixir term)
  * `data` — whatever your `init/1` and `handle/3` have accumulated
  * `effects` — side effects from the last transition, stored as data (never executed)
  * `status` — `:running` or `{:stopped, reason}`

The `effects` field is important. When `handle/3` returns actions like
timeouts or replies, the pure core doesn't execute them. It stores them
as a list in `effects`. They can be inspected, asserted on in tests, or
ignored. They're just data until something decides to act on them.

Each `crank/2` call replaces `effects` — they don't pile up from previous
transitions.

## Running it as a process

Everything above works without a process. But sometimes you need one.
Maybe you want a timeout that fires after 30 seconds of inactivity. Maybe
you want the machine to live in a supervision tree so it restarts on
failure. Maybe another process needs to send it a message and get a reply.

`Crank.Server` handles this. It takes the same module — the exact same
one, unchanged — and runs the functions inside OTP's `gen_statem`:

    {:ok, pid} = Crank.Server.start_link(MyApp.VendingMachine, price: 75)
    Crank.Server.cast(pid, {:coin, 25})

The logic is the same. What changes is the plumbing around it: who calls
the functions, and what happens to the effects afterward. In pure mode,
effects are stored as data. In process mode, `gen_statem` executes them —
timeouts fire, replies get sent, telemetry events are emitted.

There isn't one module for pure and another for process. There is one
module. It works in both contexts because it's just functions.

## When you need `handle_event/4`

`handle/3` is enough for most logic. But when a machine runs as a
process, events arrive in different ways. A cast is asynchronous — fire
and forget. A call is synchronous — the caller is waiting for a reply.
A timeout fires because time passed. A raw message arrives from another
process.

Sometimes the function needs to know which of these happened. That's
what `handle_event/4` is for. It's `handle/3` with one extra argument —
the event type — prepended:

    def handle_event({:call, from}, :status, state, data) do
      {:keep_state, data, [{:reply, from, state}]}
    end

The event types are:

  * `:internal` — programmatic events (this is what pure `crank/2` always uses)
  * `:cast` — someone called `Crank.Server.cast(pid, event)`
  * `{:call, from}` — someone called `Crank.Server.call(pid, event)` and is waiting
  * `:info` — a raw Erlang message from another process
  * `:timeout`, `:state_timeout`, `{:timeout, name}` — a timer fired

If a module defines `handle_event/4`, Crank uses it instead of
`handle/3`. If a module needs both — `handle/3` for business logic
and `handle_event/4` for replies — the specific clauses go in
`handle_event/4`, and a catch-all delegates everything else:

    def handle_event({:call, from}, :status, state, data) do
      {:keep_state, data, [{:reply, from, state}]}
    end

    # Everything that isn't a call goes to handle/3
    def handle_event(_event_type, event, state, data) do
      handle(event, state, data)
    end

## What you return

Every `handle/3` (or `handle_event/4`) clause returns a tuple that tells
Crank what should happen next. The most common ones:

  * `{:next_state, new_state, new_data}` — move to a different state
  * `{:next_state, new_state, new_data, actions}` — move and declare side effects
  * `{:keep_state, new_data}` — stay in the same state, update the data
  * `{:stop, reason, new_data}` — shut down the machine

There are a few more (`{:keep_state, new_data, actions}`,
`:keep_state_and_data`, `{:keep_state_and_data, actions}`). These match
`:gen_statem`'s return values exactly. `{:next_state, ...}` and
`{:keep_state, ...}` cover nearly everything.

The `actions` list is where side effects are declared: timeouts, replies,
internal events. In pure mode these get stored in `machine.effects`. In
process mode `gen_statem` executes them.

## Example

A door with three states — locked, unlocked, opened — and four transitions:

    defmodule MyApp.Door do
      use Crank

      @impl true
      def init(_opts), do: {:ok, :locked, %{}}

      @impl true
      def handle(:unlock, :locked, data), do: {:next_state, :unlocked, data}
      def handle(:lock, :unlocked, data), do: {:next_state, :locked, data}
      def handle(:open, :unlocked, data), do: {:next_state, :opened, data}
      def handle(:close, :opened, data), do: {:next_state, :unlocked, data}
    end

Use it:

    machine =
      MyApp.Door
      |> Crank.new()
      |> Crank.crank(:unlock)
      |> Crank.crank(:open)

    machine.state
    #=> :opened

Four clauses. Four transitions. That's the whole machine. If an event
arrives that no clause matches — say, `:open` when the door is `:locked`
— Elixir raises a `FunctionClauseError`. That's deliberate. A state
machine that silently ignores unexpected events is hiding bugs.

# `event_type`

```elixir
@type event_type() ::
  :internal
  | :cast
  | {:call, from :: GenServer.from()}
  | :info
  | :timeout
  | :state_timeout
  | {:timeout, name :: term()}
```

How an event was delivered. This is the first argument to `handle_event/4`.

In pure mode (`crank/2`), the event type is always `:internal`. The other
types only appear when the machine runs as a process via `Crank.Server`.

# `handle_event_result`

```elixir
@type handle_event_result() ::
  {:next_state, new_state :: term(), new_data :: term()}
  | {:next_state, new_state :: term(), new_data :: term(),
     actions :: [Crank.Machine.action()]}
  | {:keep_state, new_data :: term()}
  | {:keep_state, new_data :: term(), actions :: [Crank.Machine.action()]}
  | :keep_state_and_data
  | {:keep_state_and_data, actions :: [Crank.Machine.action()]}
  | {:stop, reason :: term(), new_data :: term()}
```

What `handle/3` or `handle_event/4` returns. The tuple tells Crank what
to do next — move to a new state, stay in the current one, or stop.
Matches `:gen_statem` return values exactly.

# `init_result`

```elixir
@type init_result() ::
  {:ok, state :: term(), data :: term()} | {:stop, reason :: term()}
```

What `init/1` returns — either `{:ok, state, data}` to start, or `{:stop, reason}` to refuse.

# `on_enter_result`

```elixir
@type on_enter_result() ::
  {:keep_state, new_data :: term()}
  | {:keep_state, new_data :: term(), actions :: [Crank.Machine.action()]}
```

What `on_enter/3` returns. Can only keep the current state (optionally updating data).

# `handle`
*optional* 

```elixir
@callback handle(
  event :: term(),
  state :: term(),
  data :: term()
) :: handle_event_result()
```

Crank calls this every time an event arrives. This is the simplified
signature — just the event, the current state, and the data. No event
type.

This is the callback for business logic. Each clause is one transition:

    def handle({:coin, amount}, :accepting, data) do
      {:next_state, :accepting, %{data | balance: data.balance + amount}}
    end

Read it as: "When a coin event arrives and the machine is in the
accepting state, stay in accepting and add the amount to the balance."

If a module also defines `handle_event/4`, Crank uses that instead.
This allows process-specific concerns (replies, timeouts) to live in
`handle_event/4` while everything else delegates:

    def handle_event({:call, from}, :status, state, data) do
      {:keep_state, data, [{:reply, from, state}]}
    end

    def handle_event(_, event, state, data), do: handle(event, state, data)

# `handle_event`
*optional* 

```elixir
@callback handle_event(
  event_type :: event_type(),
  event_content :: term(),
  state :: term(),
  data :: term()
) :: handle_event_result()
```

Crank calls this every time an event arrives. This is the full
signature — it includes the event type as the first argument, which
tells the function *how* the event was delivered.

Most of the time the delivery method doesn't matter. `handle/3` drops
the event type and is simpler. `handle_event/4` is for when the function
needs to reply to a synchronous call, distinguish timeouts from casts,
or handle raw process messages:

    # Reply to a synchronous caller
    def handle_event({:call, from}, :status, state, data) do
      {:keep_state, data, [{:reply, from, state}]}
    end

The event types:

  * `:internal` — pure cranks via `Crank.crank/2` (always this in pure mode)
  * `:cast` — async, via `Crank.Server.cast/2`
  * `{:call, from}` — sync, via `Crank.Server.call/3` (caller is waiting)
  * `:info` — raw Erlang message from another process
  * `:timeout` / `:state_timeout` / `{:timeout, name}` — a timer fired

If a module defines both `handle_event/4` and `handle/3`, Crank uses
`handle_event/4`.

# `init`

```elixir
@callback init(args :: term()) :: init_result()
```

Crank calls this once when the machine is created. It returns the
starting state and any data the machine should carry.

    def init(opts) do
      {:ok, :idle, %{price: opts[:price] || 100, balance: 0}}
    end

Returns `{:ok, state, data}` to start the machine, or `{:stop, reason}`
to refuse.

# `on_enter`
*optional* 

```elixir
@callback on_enter(
  old_state :: term(),
  new_state :: term(),
  data :: term()
) :: on_enter_result()
```

Crank calls this after the machine enters a new state. Optional.

Receives the state the machine just left, the state it just entered, and
the current data. Only fires on actual state changes — when `handle/3`
returns `{:next_state, ...}` with a different state.

Useful for recording that a transition happened without cluttering the
transition logic:

    def on_enter(_old_state, _new_state, data) do
      {:keep_state, Map.put(data, :entered_at, System.monotonic_time())}
    end

# `crank`

```elixir
@spec crank(Crank.Machine.t(), event_content :: term()) :: Crank.Machine.t()
```

Send an event to the machine. Returns a new machine with the updated state.

This is the core operation. Calls `handle/3` (or `handle_event/4`) with
the event, the current state, and the data. Whatever the function returns
becomes the new machine.

If the function returns `{:stop, reason, data}`, the machine's status
changes to `{:stopped, reason}`. After that, any further `crank/2` calls
raise `Crank.StoppedError` — a stopped machine can't process events.

If the function returns actions (timeouts, replies), they're stored in
`machine.effects` as inert data. Each `crank/2` replaces effects from
the previous call — they don't accumulate.

Works naturally in pipelines:

## Examples

    iex> machine = Crank.new(Crank.Examples.Door) |> Crank.crank(:unlock)
    iex> machine.state
    :unlocked

    iex> machine = Crank.new(Crank.Examples.Turnstile) |> Crank.crank(:coin) |> Crank.crank(:push)
    iex> machine.state
    :locked
    iex> machine.data
    %{coins: 1, passes: 1}

Pipeline style:

    iex> machine =
    ...>   Crank.Examples.Door
    ...>   |> Crank.new()
    ...>   |> Crank.crank(:unlock)
    ...>   |> Crank.crank(:open)
    iex> machine.state
    :opened

# `crank!`

```elixir
@spec crank!(Crank.Machine.t(), event_content :: term()) :: Crank.Machine.t()
```

Same as `crank/2`, but raises if the transition stops the machine.

In tests and scripts, a stop usually means something went wrong. This
lets you write a pipeline without checking for stops at each step — if
any transition returns `{:stop, reason, data}`, you get a
`Crank.StoppedError` immediately.

## Examples

    iex> Crank.new(Crank.Examples.Door) |> Crank.crank!(:unlock) |> Map.get(:state)
    :unlocked

# `new`

```elixir
@spec new(module(), term()) :: Crank.Machine.t()
```

Create a new machine.

Takes a callback module and any arguments for `init/1`. Calls `init/1`,
gets the starting state and data, and returns a `%Crank.Machine{}` struct
ready to receive events.

Raises if the module doesn't define the required callbacks, or if
`init/1` returns `{:stop, reason}`.

## Examples

    iex> machine = Crank.new(Crank.Examples.Door)
    iex> machine.state
    :locked
    iex> machine.effects
    []
    iex> machine.status
    :running

    iex> machine = Crank.new(Crank.Examples.Turnstile)
    iex> machine.data
    %{coins: 0, passes: 0}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
