lib/tx/macro.ex

defmodule Tx.Macro do
  @moduledoc """
  Export macros to allow create complex transactions without boilerplate.
  """

  @doc """
  Create a transaction with a binding to `repo`.

  The `repo` binding can be used within the body for raw db operations
  (e.g. `Repo.insert`, `Repo.update`, ...).

  Example:

  The following code:

      import Tx.Macro

      tx repo do
        {:ok, value} <- repo.insert(foo)
        {:ok, a} <- create_a_tx(value)
        {:ok, b} <- create_b_tx(a)
        {:ok, {a, b}}
      end

  will expands into:

      Tx.new(fn repo ->
        with {:ok, value} <- Tx.run(repo, repo.insert(foo)),
             {:ok, a} <- Tx.run(repo, create_a_tx(value)),
             {:ok, b} <- Tx.run(repo, create_b_tx(a)) do
          Tx.run(repo, {:ok, {a, b}})
        end
      end)
  """
  defmacro tx(repo, do: {:__block__, _, body}), do: rewrite(repo, body, nil)
  defmacro tx(repo, do: {:__block__, _, body}, else: e), do: rewrite(repo, body, e)

  @doc """
  Create a transaction.

      import Tx.Macro

      tx do
        {:ok, a} <- create_a_tx(value)
        {:ok, b} <- create_b_tx(a)
        {:ok, {a, b}}
      end

  will expands into:

      Tx.new(fn repo ->
        with {:ok, a} <- Tx.run(repo, create_a_tx(value)),
             {:ok, b} <- Tx.run(repo, create_b_tx(a)) do
          Tx.run(repo, {:ok, {a, b}})
        end
      end)

  You can use `tx/2` if you need to access to a binding to `repo` from
  the transaction.
  """
  defmacro tx(do: {:__block__, _, body}), do: rewrite(nil, body, nil)
  defmacro tx(do: {:__block__, _, body}, else: e), do: rewrite(nil, body, e)

  defp rewrite(repo, body, else_) do
    repo = repo || Macro.var(:repo, __MODULE__)

    quote location: :keep do
      Tx.new(fn unquote(repo) ->
        unquote(rewrite_inner(repo, body, else_))
      end)
    end
  end

  defp rewrite_inner(repo, body, else_) when length(body) > 1 do
    exprs = Enum.map(Enum.slice(body, 0..-2), &rewrite_bind_clause(repo, &1))
    last_expr = Enum.at(body, -1)

    last_expr =
      quote location: :keep do
        Tx.run(unquote(repo), unquote(last_expr))
      end

    else_exprs = rewrite_else(repo, else_)

    do_and_else =
      case else_exprs do
        nil -> [do: last_expr]
        _ -> [do: last_expr, else: else_exprs]
      end

    {:with, [], exprs ++ [do_and_else]}
  end

  defp rewrite_inner(_repo, body, _else), do: body

  # bind operator
  defp rewrite_bind_clause(repo, {:<-, env, [pat, expr]}) do
    expr = rewrite_single_clause_if_and_unless(expr)

    run_expr =
      quote location: :keep do
        Tx.run(unquote(repo), unquote(expr))
      end

    {:<-, env, [pat, run_expr]}
  end

  defp rewrite_bind_clause(_repo, other), do: other

  defp rewrite_single_clause_if_and_unless([op, env, [cond_, do: then]])
       when op in [:if, :unless] do
    [op, env, [cond_, do: then, else: Macro.escape({:ok, nil})]]
  end

  defp rewrite_single_clause_if_and_unless(expr), do: expr

  defp rewrite_else(_repo, nil), do: nil

  defp rewrite_else(repo, xs) do
    Enum.map(xs, &rewrite_else_clause(repo, &1))
  end

  defp rewrite_else_clause(repo, {:->, env, [left, right]}) do
    right =
      quote location: :keep do
        Tx.run(unquote(repo), unquote(right))
      end

    {:->, env, [left, right]}
  end
end