Skip to main content

lib/squidie/workflow/registry_helpers.ex

defmodule Squidie.Workflow.RegistryHelpers do
  @moduledoc false

  @doc """
  Normalizes map or keyword-list registries into key-entry pairs.
  """
  @spec registry_pairs(term(), (term() -> map())) :: {:ok, list()} | {:error, map()}
  def registry_pairs(registry, _invalid_error) when is_map(registry),
    do: {:ok, Enum.to_list(registry)}

  def registry_pairs(registry, invalid_error) when is_list(registry) do
    if Keyword.keyword?(registry) do
      {:ok, registry}
    else
      {:error, invalid_error.(registry)}
    end
  end

  def registry_pairs(registry, invalid_error), do: {:error, invalid_error.(registry)}

  @doc """
  Fetches a registry entry from a map or atom-keyed keyword registry.
  """
  @spec fetch_registry_entry(term(), atom() | String.t()) :: {:ok, term()} | :error
  def fetch_registry_entry(registry, key) when is_map(registry), do: Map.fetch(registry, key)

  def fetch_registry_entry(registry, key) when is_list(registry) do
    if Keyword.keyword?(registry) and is_atom(key) do
      Keyword.fetch(registry, key)
    else
      :error
    end
  end

  def fetch_registry_entry(_registry, _key), do: :error

  @doc """
  Normalizes a value into a JSON-safe representation or returns the failing path.
  """
  @spec json_value(term(), [term()]) :: {:ok, term()} | {:error, [term()]}
  def json_value(nil, _path), do: {:ok, nil}
  def json_value(value, _path) when is_boolean(value), do: {:ok, value}
  def json_value(value, _path) when is_integer(value), do: {:ok, value}
  def json_value(value, _path) when is_float(value), do: {:ok, value}
  def json_value(value, _path) when is_binary(value), do: {:ok, value}
  def json_value(value, _path) when is_atom(value), do: {:ok, Atom.to_string(value)}

  def json_value(value, path) when is_tuple(value) do
    value
    |> Tuple.to_list()
    |> json_value(path)
  end

  def json_value([], _path), do: {:ok, []}

  def json_value(value, path) when is_list(value) do
    if Keyword.keyword?(value), do: json_map(value, path), else: json_list(value, path)
  end

  def json_value(value, path) when is_map(value), do: json_map(Map.to_list(value), path)
  def json_value(_value, path), do: {:error, path}

  defp json_map(pairs, path) do
    Enum.reduce_while(pairs, {:ok, %{}}, fn {key, item}, {:ok, acc} ->
      with {:ok, key} <- json_key(key, path),
           {:ok, item} <- json_value(item, [key | path]) do
        {:cont, {:ok, Map.put(acc, key, item)}}
      else
        {:error, path} -> {:halt, {:error, path}}
      end
    end)
  end

  defp json_key(key, _path) when is_atom(key), do: {:ok, Atom.to_string(key)}
  defp json_key(key, _path) when is_binary(key), do: {:ok, key}
  defp json_key(key, _path) when is_integer(key), do: {:ok, Integer.to_string(key)}
  defp json_key(_key, path), do: {:error, path}

  defp json_list(list, path) do
    list
    |> Stream.with_index()
    |> Enum.reduce_while({:ok, []}, fn {item, index}, {:ok, acc} ->
      case json_value(item, [index | path]) do
        {:ok, item} -> {:cont, {:ok, [item | acc]}}
        {:error, path} -> {:halt, {:error, path}}
      end
    end)
    |> case do
      {:ok, items} -> {:ok, Enum.reverse(items)}
      {:error, path} -> {:error, path}
    end
  end
end