lib/runbox/utils/traversal.ex

defmodule Runbox.Utils.Traversal do
  @moduledoc group: :utilities
  @doc """
  Performs a depth-first pre-order traversal on the given `value`.

  Transforms each subterm via the given `fun`, before descending into its descendants.

  ## Example

      iex> Traversal.prewalk(
      ...>    [1, 2, :pi, 4, %{5 => [6, 7]}],
      ...>    fn
      ...>      x when is_integer(x) -> Integer.to_string(x)
      ...>      x when is_list(x) -> Enum.reverse(x)
      ...>      x -> x
      ...>    end
      ...> )
      [%{"5" => ["7", "6"]}, "4", :pi, "2", "1"]
  """
  def prewalk(value, fun) do
    value
    |> fun.()
    |> prewalk_children(fun)
  end

  defp prewalk_children(list, fun) when is_list(list) do
    Enum.map(list, &prewalk(&1, fun))
  end

  defp prewalk_children(tuple, fun) when is_tuple(tuple) do
    tuple
    |> Tuple.to_list()
    |> Enum.map(&prewalk(&1, fun))
    |> List.to_tuple()
  end

  defp prewalk_children(%module{} = struct, fun) do
    struct
    |> Map.from_struct()
    |> Map.new(fn {key, value} ->
      {prewalk(key, fun), prewalk(value, fun)}
    end)
    |> Map.put(:__struct__, module)
  end

  defp prewalk_children(map, fun) when is_map(map) do
    Map.new(map, fn {key, value} ->
      {prewalk(key, fun), prewalk(value, fun)}
    end)
  end

  defp prewalk_children(scalar, _fun) do
    scalar
  end
end