README.md

[![Coverage Status](https://coveralls.io/repos/github/gpedic/ex_q/badge.svg?branch=master)](https://coveralls.io/github/gpedic/ex_q?branch=master)

# ExQ

ExQ provides a way of queuing the execution of operations and aggregates all returned values similar to `Ecto.Multi`.
Operations are queued and executed in FIFO order.

```elixir
    iex> pipeline = Q.new()
    |> Q.put(:init, %{test: "setup"})
    |> Q.run(:read, fn _ -> {:ok, "Once upon a time ..."} end)

    iex> pipeline |> Q.exec()

    {:ok, %{init: %{test: "setup"}, read: "Once upon a time ..."}}


    iex> pipeline 
    |> Q.run(:write, fn %{read: _read} -> {:error, :write_failed} end)
    |> Q.exec()

    {:error, :write, :write_failed, %{init: %{test: "setup"}, read: "Once upon a time ..."}}
```

## Comparison to `with`

A major benefit of using ExQ over `with` is that the results of all steps before an error are available and the step which produced the error is also easily identifiable.

```elixir
  user_id = 1234
  with {:ok, user} <- Blog.fetch_user(user_id),
    {:ok, post} <- Blog.create_post(user, "test") do
    broadcast(post)
  else
    {:error, :create_post_failed} = error ->
      Logger.error("Failed to create post for user #{user_id}")
      error
    {:error, error} = error ->
      Logger.error(error)
      error
  end
```
### Note
* When `with` is used on its own the return of the `user` step is not available for error handling if create_post fails
* To match a specific error in `else` requires workarounds e.g. `create_post/2` returning a special error tuple


```elixir
  defmodule Blog do
    def create_post(%{user: user, content: content}, opts) do
      upcase = Keyword.get(opts, :upcase, false)

      if upcase do
        insert_post(user, String.upcase(content))
      else
        insert_post(user, content)
      end
    end

    def count_posts(%{user: user}) do
      {:ok, get_post_count(user)}
    end
  end

  ex_que = Q.new()
  |> Q.put(:content, "hello world")
  |> Q.run(:user, fn %{user_id: id} -> Blog.fetch_user(id) end)
  |> Q.run(:post, Blog, :create_post, [[upcase: true]])
  |> Q.run(:post_count, &Blog.count_posts/1)

  with {:ok, %{post: post}} <- Q.exec(ex_que) do
    broadcast(post)
  else
    {:error, :post, failed_value, %{user: user}} ->
      Logger.error("Failed to create post for user #{user.id}")
      {:error, failed_value}
    {:error, failed_operation, failed_value, _changes_so_far} ->
      Logger.error("Operation #{failed_operation} failed, #{inspect(failed_value)}")
      {:error, failed_value}
  end
```
### Note
* With ExQ the `user` result is available for error handling if `create_post` fails
* We can also handle errors for specific steps while `create_post/2` can return a standard error tuple like `{:error, "msg"}`

## Ending execution early
Return `{:halt, value}` to end execution early

```elixir
  iex> Q.new()
  |> Q.put(:user_id, 1235)
  |> Q.run(:user, &Blog.fetch_user/1 end)
  |> Q.run(:earned_daily_award, fn %{user: user} ->
      count = Blog.count_posts_today(user)
      if count < 1 do
        {:halt, false}
      else
        {:ok, true}
      end
  end)
  |> Q.run(:send_daily_award, Blog, :send_daily_award, [])
  |> Q.exec()
```

## Installation

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

```elixir
def deps do
  [
    {:ex_q, "~> 1.0"}
  ]
end
```

Documentation can be found at [https://hexdocs.pm/ex_q](https://hexdocs.pm/ex_q).