lib/arrows.ex

defmodule Arrows do
  @moduledoc """
  A handful of (mostly) arrow macros with superpowers.
  """

  defmacro __using__(_options) do
    quote do
      import Kernel, except: [|>: 2]
      import unquote(__MODULE__),
        only: [
          |>: 2, <|>: 2, ~>: 2, <~>: 2,
          ok: 1, to_ok: 1, from_ok: 1
        ]
    end
  end

  import Kernel, except: [|>: 2]

  defp ellipsis(l, arg) do
    Macro.prewalk(arg, 0, fn form, acc ->
      case form do
        {:..., _, c} when not is_list(c) -> {l, acc+1}
        _ -> {form, acc}
      end
    end)
  end

  defp pipe_args(where, l, args) do
    case ellipsis(l, args) do
      {args, 0} when where == :first -> [l | args]
      {args, _} -> args
    end
  end

  defp pipe(where, kind, l, r) do
    v = Macro.var(:ret, __MODULE__)
    case r do
      {name, meta, args} ->
        args = if(is_list(args), do: args, else: [])
        continue = {name, meta, pipe_args(where, v, args)}
        case kind do
          :normal ->
            quote [generated: true] do
              unquote(v) = unquote(l)
              unquote(continue)
            end
          :ok ->
            quote [generated: true] do
              case unquote(l) do
                nil -> nil
                :error -> :error
                {:error, _} = unquote(v) -> unquote(v)
                {:ok, unquote(v)} -> unquote(continue)
                unquote(v) -> unquote(continue)
              end
            end
        end
      _ ->
        case ellipsis(l, r) do
          {arg, 0} -> raise RuntimeError, message: "Can't pipe into #{inspect(r)}: missing ellipsis(`...`) in #{inspect(arg)}"
          {continue, _} ->
            case kind do
              :normal ->
                quote [generated: true] do
                  unquote(v) = unquote(l)
                  unquote(continue)
                end
              :ok ->
                quote [generated: true] do
                  case unquote(l) do
                    nil -> nil
                    :error -> :error
                    {:error, _} = unquote(v) -> unquote(v)
                    {:ok, unquote(v)} -> unquote(continue)
                    unquote(v) -> unquote(continue)
                  end
                end
            end
        end
    end
  end

  defp join(kind, l, r) do
    v = Macro.var(:l, __MODULE__)
    case kind do
      :normal ->
        quote [generated: true] do
          unquote(v) = unquote(l)
          if is_nil(unquote(v)), do: unquote(r), else: unquote(v)
        end
      :ok ->
        quote [generated: true] do
          case unquote(l) do
            nil -> unquote(r)
            :error -> unquote(r)
            {:error, _} -> unquote(r)
            unquote(v) -> unquote(v)
          end
        end
    end
  end

  @doc """
  A more flexible drop-in replacement for the standard elixir pipe operator.

  Special features are unlocked when using the `...` (ellipsis) on the right hand side:

  * The right hand side need not be a function, it can be any expression containing the ellipsis.
  * The ellipsis will be replaced with the result of evaluating the hand side expression.
  * You may use the ellipsis multiple times and the left hand side will be calculated exactly once.

  You can do crazy stuff with the ellipsis, but remember that people have to read it!
  """
  defmacro l |> r,  do: pipe(:first, :normal, l, r)
  @doc "Like `||`, except only defaults if the left is nil (i.e. false is valid)"
  defmacro l <|> r, do: join(:normal, l, r)

  @doc "Like `OK.~>`"
  defmacro l ~> r,  do: pipe(:first, :ok, l, r)

  @doc "Like `||`, except with the logic applied by `~>`"
  defmacro l <~> r, do: join(:ok, l, r)

  def to_ok(x) do
    case x do
      {:ok, _} -> x
      {:error, _} -> x
      :error -> x
      nil -> :error
      x -> {:ok, x}
    end
  end

  def from_ok(x) do
    case x do
      {:ok, x} -> x
      {:error, _} -> nil
      :error -> nil
      x -> x # lenience
    end
  end

  def ok(x={:ok, _}), do: x
  def ok(x={:error, _}), do: x
  def ok(:error), do: :error
  def ok(x), do: {:ok, x}

  def ok_or(x={:ok, _}, _), do: x
  def ok_or(x={:error, _}, _), do: x
  def ok_or(:error, _), do: :error
  def ok_or(nil, err), do: {:error, err}
  def ok_or(ok, _), do: {:ok, ok}

end