lib/pathex/combinator.ex

defmodule Pathex.Combinator do
  @moduledoc """
  Combinator for lenses
  Read `Pathex.Combinator.combine/1` documentation
  """

  @doc """
  This function creates a recursive path from path defined in `path_func`
  Consider this example

      iex> import Pathex; import Pathex.Lenses
      iex> recursive_xpath = combine(fn recursive_xpath ->
      iex>   path(:x)            # Takes by :x key
      iex>   ~> recursive_xpath  # If taken, calls itself
      iex>   ||| matching(_)     # Otherwise returns current structure
      iex> end)
      iex>
      iex> Pathex.view!(%{x: %{x: %{x: %{x: 1}}}}, recursive_xpath)
      1
      iex> Pathex.set!(%{x: %{x: %{x: %{x: 1}}}}, recursive_xpath, 2)
      %{x: %{x: %{x: %{x: 2}}}}

  The second argument of this function specifies the maximum depth. It's infinity be default,
  but you can specify this as any positive integer. It is useful when you're developing lens
  and you're not sure whether the lens will or won't loop.

  For example
  ```elixir
  # Combinator lens with limit
  limited = combine(fn rec -> path(:x) ~> rec end, 100_000)
  :error = Pathex.force_set(%{x: 1}, limited, 123)

  # And this is without limit
  unlimited = combine(fn rec -> path(:x) ~> rec end)
  Pathex.force_set(%{x: 1}, unlimited, 123) # Inifinite loop
  ```
  """
  @spec combine((Pathex.t() -> Pathex.t()), pos_integer() | :infinity) :: Pathex.t()
  def combine(path_func, max_depth \\ :infinity)

  def combine(path_func, :infinity) do
    fn
      :inspect, _ ->
        inner = path_func.(inner_rec(path_func)).(:inspect, [])

        quote do
          combine(fn recursive -> unquote(inner) end)
        end

      op, argtuple ->
        path_func.(inner_rec(path_func)).(op, argtuple)
    end
  end

  def combine(path_func, max_depth) when is_integer(max_depth) and max_depth > 0 do
    fn
      :inspect, _ ->
        inner = path_func.(inner_rec(path_func, 1, max_depth)).(:inspect, [])

        quote do
          combine(fn recursive -> unquote(inner) end)
        end

      op, argtuple ->
        path_func.(inner_rec(path_func, 1, max_depth)).(op, argtuple)
    end
  end

  defp inner_rec(path_func, depth, max_depth) when depth <= max_depth do
    fn
      :inspect, _ ->
        {:recursive, [], nil}

      op, argtuple ->
        path_func.(inner_rec(path_func, depth, max_depth)).(op, argtuple)
    end
  end

  defp inner_rec(_, _, _) do
    fn _, _ -> :error end
  end

  defp inner_rec(path_func) do
    fn
      :inspect, _ ->
        {:recursive, [], nil}

      op, argtuple ->
        path_func.(inner_rec(path_func)).(op, argtuple)
    end
  end
end