lib/repo/server.ex

defmodule Altex.Repo.Server do
  @moduledoc ~s"""
  Each "table" has it's own `Altex.Repo.Server` started from
  `Altex.Repo.start_repo/1` through the `Altex.Repo.Supervisor`.
  The repo will persist or not, based on the used gateway implementation.

  The supervisor takes care to restart the server in case of failure and
  the server re-loads data from the gateway on init.
  """

  alias Altex.{
    Entity,
    Repo.Gateway
  }

  use GenServer

  @doc ~s"""
  Start a repository server for the "table" `store`.

      ### Example:

      iex> {:ok, pid} = __MODULE__.start_link(:people)

  """
  def start_link(store) when is_atom(store) do
    {:ok, _pid} = GenServer.start_link(__MODULE__, store, name: store)
  end

  @impl true
  def init(store) do
    {:ok, Gateway.load_table(store)}
  end

  # API #############################################################

  @doc ~s"""
  Store the given `entity` to the given `store`. If an entity with the
  same `uuid` exists, it will be updated, otherwise a new entity will
  be created.

  It returns `{:ok, valid_entity}` or `{:error, invalid_entity}`.
  """
  def store(entity, store) do
    GenServer.call(store, {:store, entity})
    |> handle_store()
  end

  @doc ~s"""
  Load the `Entity` with the given `uuid` from the given `store`.
  Returns `{:error, :not_found}` if the given `uuid` doesn't exist
  or `{:ok, entity}` when found.
  """
  def load(store, uuid) do
    with %Entity{} = e <- GenServer.call(store, {:load, uuid}) do
      {:ok, e}
    else
      e -> {:error, e}
    end
  end

  @doc ~s"""
  Return a list of all `Altex.Entity`s of the given `store`.
  """
  def list(store) when is_atom(store) do
    GenServer.call(store, :list)
  end

  @doc """
  Find an entity where the given field is equal to the
  example.
  """
  def find_by(example, field) do
    %type{} = example.data

    list(type)
    |> Enum.find(fn e -> Entity.get(e, field) == Entity.get(example, field) end)
  end

  ### Callbacks ####################################################

  @impl true
  def handle_info({:persist, entity}, store) do
    with table when is_atom(table) <- process_name(self()) do
      Gateway.store_table(table, entity.uuid, entity)
    else
      err -> raise("Can't store #{inspect(entity)} to #{store}. Error: #{inspect(err)}")
    end

    {:noreply, store}
  end

  @impl true
  def handle_call({:store, entity}, _, store) do
    map_key = get_key(entity)
    send(self(), {:persist, entity})
    {:reply, entity, Map.put(store, map_key, entity)}
  end

  @impl true
  def handle_call({:load, uuid}, _, store) do
    map_key = get_key(uuid)
    {:reply, Map.get(store, map_key, :not_found), store}
  end

  @impl true
  def handle_call(:list, _, store) do
    {:reply, Map.values(store), store}
  end

  @impl true
  def handle_call(:drop!, _, store) do
    {:reply, %{}, store}
  end

  # Implementation/Helpers ###################################################

  defp process_name(pid) do
    Process.info(pid)
    |> Keyword.get(:registered_name)
  end

  # ==========================================================================

  defp handle_store(entity)

  defp handle_store(%Entity{errors: []} = valid_entity) do
    {:ok, valid_entity}
  end

  defp handle_store(%Entity{errors: _errors} = invalid_entity) do
    {:error, invalid_entity}
  end

  # ==========================================================================

  defp get_key(uuid)
  defp get_key(uuid) when is_binary(uuid), do: uuid
  defp get_key(%{uuid: uuid}), do: uuid
end