lib/io/ansi/table/spec_server.ex

defmodule IO.ANSI.Table.SpecServer do
  @moduledoc """
  A server process that holds a table `spec` struct as its state.
  """

  use GenServer, restart: :transient
  use PersistConfig

  alias __MODULE__
  alias IO.ANSI.Table.{Log, Spec}

  @ets get_env(:ets_name)
  @reg get_env(:registry)
  @wait 50

  @doc """
  Spawns a new table spec server process to be registered via a `spec name`.
  """
  @spec start_link(Spec.t()) :: GenServer.on_start()
  def start_link(spec) do
    GenServer.start_link(SpecServer, spec, name: via(spec.spec_name))
  end

  @doc """
  Allows to register or look up a table spec server process via a `spec name`.
  """
  @spec via(String.t()) :: {:via, Registry, tuple}
  def via(spec_name), do: {:via, Registry, {@reg, key(spec_name)}}

  ## Private functions

  @spec key(String.t()) :: tuple
  defp key(spec_name), do: {SpecServer, spec_name}

  @spec extend(Spec.t()) :: Spec.t()
  defp extend(spec) do
    case :ets.lookup(@ets, key(spec.spec_name)) do
      [] ->
        :ok = Log.info(:spawned, {spec})
        Spec.extend(spec) |> save()

      [{_key, spec}] ->
        :ok = Log.info(:restarted, {spec})
        spec
    end
  end

  @spec save(Spec.t()) :: Spec.t()
  defp save(spec) do
    :ok = Log.info(:save, {spec, __ENV__})
    true = :ets.insert(@ets, {key(spec.spec_name), spec})
    spec
  end

  ## Callbacks

  @spec init(Spec.t()) :: {:ok, Spec.t()}
  def init(spec), do: {:ok, extend(spec)}

  @spec handle_cast(term, Spec.t()) :: {:noreply, Spec.t()}
  def handle_cast({:write, maps, options} = request, spec) do
    :ok = Log.info(:handle_cast, {spec, request, __ENV__})
    :ok = Spec.write_table(maps, spec, options)
    {:noreply, spec}
  end

  @spec handle_call(term, GenServer.from(), Spec.t()) ::
          {:reply, :ok | Spec.t(), Spec.t()}
  def handle_call({:format, maps, options} = request, {from_pid, _tag}, spec) do
    group_leader = Process.info(from_pid)[:group_leader]
    if group_leader, do: self() |> Process.group_leader(group_leader)
    :ok = Log.info(:handle_call, {spec, request, __ENV__})
    :ok = Spec.write_table(maps, spec, options)
    {:reply, :ok, spec}
  end

  def handle_call(:get = request, _from, spec) do
    :ok = Log.info(:handle_call, {spec, request, __ENV__})
    {:reply, spec, spec}
  end

  @spec terminate(term, Spec.t()) :: :ok
  def terminate(:shutdown = reason, spec) do
    :ok = Log.info(:terminate, {reason, spec, __ENV__})
    true = :ets.delete(@ets, key(spec.spec_name))
    # Ensure message logged before exiting...
    Process.sleep(@wait)
  end

  def terminate(reason, spec) do
    :ok = Log.error(:terminate, {reason, spec, __ENV__})
    true = :ets.delete(@ets, key(spec.spec_name))
    # Ensure message logged before exiting...
    Process.sleep(@wait)
  end
end