README.md

# Banzai

[![CI](https://github.com/threeaccents/banzai/actions/workflows/ci.yml/badge.svg)](https://github.com/threeaccents/banzai/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/banzai.svg)](https://hex.pm/packages/banzai)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/banzai)

A pipeline library for Elixir providing a token-based pattern for sequential function execution with automatic error handling.

> **Why "Banzai"?** The name comes from the [Banzai Pipeline](https://en.wikipedia.org/wiki/Banzai_Pipeline), the legendary surf reef break on the North Shore of Oahu, Hawaii — one of the most famous wave pipelines in the world.

Banzai implements a workflow pattern where a **token** (a map or struct) is passed through a series of **steps** (functions) that transform it. Each step receives the token, performs its operation, and returns an updated token. If any step fails, execution stops immediately and the error is returned.

## Installation

Add `banzai` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:banzai, "~> 0.1.0"}
  ]
end
```

## Quick Start

```elixir
defmodule CalculateTotal do
  use Banzai

  embedded_schema do
    field :value, :integer
  end

  def perform(params) do
    %__MODULE__{}
    |> new(params)
    |> step(fn token -> {:ok, Map.put(token, :value, token.value + 10)} end)
    |> step(fn token -> {:ok, Map.put(token, :value, token.value * 2)} end)
    |> step(fn token -> {:ok, Map.put(token, :value, token.value + 5)} end)
    |> run()
  end
end

CalculateTotal.perform(%{})
# => {:ok, %{value: 25}}
```

## How It Works

1. Create a token (map or struct) that holds your pipeline state
2. Add steps that transform the token — each returns `{:ok, updated_token}` or `{:error, reason}`
3. Run the pipeline — steps execute in order, stopping at the first error

```elixir
defmodule ProcessOrder do
  use Banzai

  embedded_schema do
  field :order_id, :string

  # internal token fields
  field :total, :integer
  field :charged, :boolean

  embeds_one :order, Order

  end

  def perform(params) do
    %__MODULE__{}
    |> new(params)
    |> step(&fetch_order/1)
    |> step(&calculate_total/1)
    |> step(&charge_customer/1)
    |> run()
  end

  defp fetch_order(%__MODULE__{} = token) do
    case Orders.get(token.order_id) do
      nil -> {:error, :order_not_found}
      order -> {:ok, %__MODULE__{token | order: order}}
    end
  end

  defp calculate_total(%__MODULE__{} = token) do
    {:ok, %__MODULE__{token | total: Order.total(token.order)}
  end

  defp charge_customer(%__MODULE__{} = token) do
    case Billing.charge(token.order.customer_id, token.total) do
      :ok -> {:ok, %__MODULE__{token | charged: true}
      {:error, reason} -> {:error, reason}
    end
  end
end
```

## Error Handling

When a step returns `{:error, reason}`:

- Execution stops immediately — subsequent steps are skipped
- The error is logged with context (action ID, step index)
- `{:error, reason}` is returned to the caller

## License

MIT