lib/needlework.ex

defmodule Needlework do
  @moduledoc """
  Needlework brings additional operators to Elixir that allows you to "thread" results of your functions into other function calls. Basically extending the `Kernel.|>/2` operator.

  Just `use Needlework` in your modules and thread away!

  Example:
  ```
  defmodule MyModule do
  use Needlework

    @spec foo(func :: fun()) :: list()
    def foo(func) do
      func
      ~> Enum.map([1, 2, 3])
    end
  end
  ```
  """
  defmacro __using__(_) do
    quote do
      import Needlework, only: :macros
    end
  end

  @type ok :: {:ok, any()}
  @type error :: {:error, any()}

  @doc """
  Wraps the value in `t:Needlework.ok/0` tuple.

  Example:
      iex> 5 |> Needlework.ok_unit()
      {:ok, 5}
      iex> {:ok, 5} |> Needlework.ok_unit()
      {:ok, 5}
      iex> {:error, ""} |> Needlework.ok_unit()
      {:error, ""}
  """
  @spec ok_unit(any) :: {:ok | :error, any}
  def ok_unit({:ok, _} = value), do: value
  def ok_unit({:error, _} = value), do: value
  def ok_unit(value), do: {:ok, value}

  @doc """
  Bind operator.

  If value on the left is a plain value -> converts it to `t:Needlework.ok/0` | `t:Needlework.error/0` tuple
  then desctructures the tuple. If it was `t:Needlework.ok/0` tuple -> passes the value for evaluation.
  If it was `t:Needlework.ok/0` tuple -> skips the evaluation

  Example:
      iex> import Needlework
      iex> foo = fn x -> {:ok, x * 2} end
      iex> 2 <|> foo.() <|> foo.() <|> foo.()
      {:ok, 16}
      iex> bar = fn _ -> {:error, "impossible"} end
      iex> 2 <|> foo.() <|> bar.() <|> foo.()
      {:error, "impossible"}
  """
  defmacro left <|> right do
    quote do
      unquote(left)
      |> Needlework.ok_unit()
      |> (fn
            {:ok, value} -> value |> unquote(right)
            {:error, reason} -> {:error, reason}
          end).()
    end
  end

  @doc """
  Same as `Needlework.<|>/2` but places the value instead of `_`.

  If no `_` present works like a `Needlework.<|>/2`

  Examples:
      iex> import Needlework
      iex> foo = fn x, y -> {:ok, x ++ y} end
      iex> [1, 2, 3] <~> foo.(_, [1, 2, 3]) <~> foo.([4, 5, 6], _)
      {:ok, [4, 5, 6, 1, 2, 3, 1, 2, 3]}
      iex> [1, 2, 3] <~> foo.([1, 2, 3]) <~> foo.([4, 5, 6])
      {:ok, [1, 2, 3, 1, 2, 3, 4, 5, 6]}
      iex> bar = fn _, _ -> {:error, "reason"} end
      iex> [1, 2, 3] <~> bar.([1, 2, 3]) <~> foo.([4, 5, 6])
      {:error, "reason"}
  """
  defmacro left <~> right do
    quote do
      unquote(left)
      |> Needlework.ok_unit()
      |> (fn
            {:ok, value} -> value ~>> unquote(right)
            {:error, reason} -> {:error, reason}
          end).()
    end
  end

  @doc """
  Allows to thread the value on the left as the last argument

  Example:
      iex> import Needlework
      iex> [1, 2, 3] ~> Kernel.++([4, 5, 6])
      [4, 5, 6, 1, 2, 3]
      iex> fn x -> x*2 end ~> Enum.map([1, 2, 3])
      [2, 4, 6]
  """
  defmacro left ~> right do
    new_right = add_last_arg(right, left)

    quote do
      unquote(new_right)
    end
  end

  @doc """
  Allows to thread the value on the left to a specific spot on the right.

  Value from the left will be placed instead of `_`.
  If no `_` present works like a regular `Kernel.|>/2`

  Example:
      iex> import Needlework
      iex> [1, 2, 3] ~>> Kernel.++([4, 5, 6], _)
      [4, 5, 6, 1, 2, 3]
      iex> [1, 2, 3] ~>> Kernel.++([4, 5, 6])
      iex> [] ~>> Enum.reduce([1, 2, 3], _, fn x, acc -> [x | acc] end)
      [3, 2, 1]
  """
  defmacro left ~>> right do
    case replace_underscore_args(right, left) do
      {:ok, new_args} ->
        new_right = put_elem(right, 2, new_args)

        quote do
          unquote(new_right)
        end

      {:error, _} ->
        quote do
          unquote(left)
          |> unquote(right)
        end
    end
  end

  defp replace_underscore_args({_, _, args}, replacement) do
    Enum.reduce(args, {:error, []}, fn
      {:_, _, _}, {_, args} -> {:ok, args ++ [replacement]}
      val, {res, args} -> {res, args ++ [val]}
    end)
  end

  defp add_last_arg({name, context, args}, argument), do: {name, context, args ++ [argument]}
end