README.md

# Flume

This library is meant to help with managing control flow, when there are lots of steps and some may go wrong. You have probably seen or tried something complex along these lines with `with` statements. Most of the time, they work great.

Let's use a hypothetical example that's not too far from the real world. Let's say you are processing an order from a customer in another country. You need to retrieve some information from a database, get an fx rate, get tax rates for that country, process the order, and finally store the result. Anything could fail too, so you have to annotate operations so you know which one failed. Using `with`, it may look something like this:

```elixir
defmodule MyApp.Orders do
  def process(order) do
    with
      {:customer, {:ok, customer}} <- {:customer, MyApp.Customers.get(order)},
      {:tax, {:ok, tax}} <- {:tax, MyApp.Tax.rate(order)},
      {:fx_rate, {:ok, fx_rate}} <- {:fx_rate, MyApp.Fx.rate(order)},
      {:payment, {:ok, items}} <- {:payment, MyApp.Payments.process(order, tax, fx_rate)},
      {:order, {:ok, order}} <- {:order, create(order, customer, tax, fx_rate)} do
      order.id
    else
      {:customer, {:error, error}} -> handle_error(:customer, error)
      {:tax, {:error, error}} -> handle_error(:tax, error)
      {:fx_rate, {:error, error}} -> handle_error(:fx_rate, error)
      {:payment, {:error, error}} -> handle_error(:payment, error)
      {:order, {:error, error}} -> handle_error(:order, error)
    end
  end

  defp handle_error(step, error) do
    # do something
  end

  def create(order, customer, tax, fx_rate) do
    # do something
  end
end
```

That can easily get out of hand and become hard to reason about. `flume` allows you to do something which is arguably clearer and more succinct:

```elixir
defmodule MyApp.Orders do
  def process(order) do
    Flume.new(on_error: &handle_error/2)
    |> Flume.run_async(:customer, fn -> MyApp.Customers.get(order) end)
    |> Flume.run_async(:tax, fn -> MyApp.Tax.rate(order) end)
    |> Flume.run_async(:fx_rate, fn -> MyApp.Fx.rate(order) end)
    |> Flume.run(:payment, &handle_payment(&1, order), wait_for: [:customer, :tax, :fx_rate])
    |> Flume.run(:order, &handle_order(&1, order), on_success: &Map.fetch!(&1, :id))
    |> Flume.result()
  end

  defp handle_payment(%{fx_rate: fx_rate, tax: tax}, order) do
    MyApp.Payments.process(order, tax, fx_rate)
  end

  defp handle_order(%{fx_rate: fx_rate, tax: tax, customer: customer}, order) do
    create(order, customer, tax, fx_rate)
  end
end
```

All that it asks is that your callbacks return predictable tuples, either `{:ok, result}` or `{:error, reason}`. It will stop at the first error and nothing later will be executed - unless `run_async` is used. See the docs for more info.

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `flume` to your list of dependencies in `mix.exs`:

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

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/flume](https://hexdocs.pm/flume).