# `ExGram.FSM.Helpers`
[🔗](https://github.com/rockneurotiko/ex_gram_fsm/blob/v0.1.0/lib/ex_gram/fsm/helpers.ex#L1)

Runtime helper functions for FSM flow and state management.

All functions take and return `ExGram.Cnt.t()` for pipeline compatibility.
These are automatically imported into your bot module when you call
`use ExGram.FSM`.

## Context keys

The helpers read FSM config from `context.extra` keys set by the middleware:

| Key | Type | Description |
|-----|------|-------------|
| `context.extra.fsm` | `%ExGram.FSM.State{}` | Current flow + state + data |
| `context.extra.fsm_key` | `term()` | Storage key (shape depends on configured key module) |
| `context.extra.fsm_storage` | module | Storage backend module |
| `context.extra.fsm_flows` | `%{atom => module}` | Registered flow modules map |
| `context.extra.fsm_on_invalid_transition` | atom or tuple | Invalid transition policy |

## Flow lifecycle

    # 1. Start a flow (sets flow name, default state, clears data)
    context |> start_flow(:registration)

    # 2. Transition through states (validated against the active flow)
    context |> transition(:get_email)

    # 3. Accumulate data
    context |> update_data(%{name: "Alice"})

    # 4. End the flow (resets to no-flow state)
    context |> clear_flow()

# `clear_flow`

```elixir
@spec clear_flow(ExGram.Cnt.t()) :: ExGram.Cnt.t()
```

Resets the FSM to a clean state: no active flow, no state, no data.

Calls `storage.clear/1` to remove the user's FSM record entirely.

## Example

    def handle({:command, :cancel, _}, context) do
      context
      |> clear_flow()
      |> answer("Cancelled. Send /start to begin again.")
    end

# `get_data`

```elixir
@spec get_data(ExGram.Cnt.t()) :: map()
```

Returns the current FSM data map from context.

Never returns `nil` — returns `%{}` if no FSM state exists.

## Example

    def handle({:command, :status, _}, context) do
      data = get_data(context)
      answer(context, "Your data: #{inspect(data)}")
    end

# `get_flow`

```elixir
@spec get_flow(ExGram.Cnt.t()) :: atom() | nil
```

Returns the current active flow name atom from context.

Returns `nil` if no flow is active or if the FSM middleware didn't run.

## Example

    def handle({:command, :status, _}, context) do
      flow = get_flow(context)
      answer(context, "Current flow: #{inspect(flow)}")
    end

# `get_state`

```elixir
@spec get_state(ExGram.Cnt.t()) :: atom() | nil
```

Returns the current FSM state atom (step within the active flow) from context.

Returns `nil` if no state is set or if the FSM middleware didn't run.

## Example

    def handle({:command, :status, _}, context) do
      state = get_state(context)
      answer(context, "Current step: #{inspect(state)}")
    end

# `set_state`

```elixir
@spec set_state(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
```

Unconditionally sets the FSM state within the currently active flow.

Requires an active flow. If no flow is active, the `on_invalid_transition`
policy is applied (with `from: nil, to: new_state`).

This bypasses transition validation — use `transition/2` for the normal path.

Use cases:
- Resetting to the flow's first step
- Admin override within a flow
- Recovery from an error state within a flow

## Example

    def handle({:command, :restart_flow, _}, context) do
      context
      |> set_state(:get_name)
      |> answer("Let's start over. What's your name?")
    end

# `set_state`

```elixir
@spec set_state(ExGram.Cnt.t(), atom(), atom()) :: ExGram.Cnt.t()
```

Force sets a flow and state, bypassing all conflict and validation checks.

This is the escape hatch — it always succeeds regardless of the current flow.
The data map is preserved as-is. Use this only for admin resets, recovery, or
testing.

## Example

    def handle({:command, :admin_reset, _}, context) do
      context
      |> set_state(:registration, :get_name)
      |> answer("Admin reset. Starting registration flow.")
    end

# `start_flow`

```elixir
@spec start_flow(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
```

Starts a named flow for the current user/chat.

Sets the flow name, applies the flow's `default_state/0` as the initial state
(if defined), and clears any previous data.

Behavior:
- If no flow is currently active → starts the new flow
- If the same flow is already active → re-starts it (resets state + data)
- If a **different** flow is active → applies the `on_invalid_transition` policy.
  The "from" is the current flow name (treated as a pseudo-state), "to" is
  the requested flow name.

## Example

    def handle({:command, :register, _}, context) do
      context
      |> start_flow(:registration)
      |> answer("What's your name?")
    end

# `transition`

```elixir
@spec transition(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
```

Sets the FSM state with transition validation against the active flow.

This is the normal path for moving through steps. Use `set_state/2` only as
an escape hatch when you want to force a state skip validation.

Behavior:
1. Reads the current state from `context.extra.fsm.state`
2. Reads the active flow from `context.extra.fsm.flow`
3. Looks up the flow module and checks whether `from -> to` is a valid transition
4. If valid (or no flows configured): updates state and persists to storage
5. If NOT valid: delegates to the `on_invalid_transition` policy

Return value depends on policy:
- `:raise` — raises `ExGram.FSM.TransitionError` (never returns normally)
- `:log` — logs a warning, returns `context` unchanged
- `:ignore` — returns `context` unchanged silently
- `{Module, :function}` — calls `Module.function(context, from, to)`, returns its result

## Example

    def handle({:text, name, _}, %{extra: %{fsm: %ExGram.FSM.State{flow: :registration, state: :get_name}}} = context) do
      context
      |> update_data(%{name: name})
      |> transition(:get_email)
      |> answer("Got it! What's your email?")
    end

# `update_data`

```elixir
@spec update_data(ExGram.Cnt.t(), map()) :: ExGram.Cnt.t()
```

Merges a map into the current FSM data, persisting the result.

Uses `Map.merge/2` semantics: new keys are added, existing keys are overwritten.
The active flow and state are preserved.

## Example

    def handle({:text, name, _}, context) do
      context
      |> update_data(%{name: name})
      |> transition(:get_email)
      |> answer("What's your email?")
    end

---

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