lib/spex/instance_manager/instance_store.ex

defmodule Spex.InstanceManager.InstanceStore do
  @moduledoc """
  Abstraction layer for instance storage using DETS.
  """

  alias Spex.Errors.DetsError
  alias Spex.Errors.FileError
  alias Spex.Errors.InstanceError
  alias Spex.InstanceManager.Instance

  @type instance_hash :: non_neg_integer()

  @doc """
  Initializes the DETS table for storing instances.
  """
  @spec init(:dets.tab_name(), Path.t()) :: :ok | {:error, DetsError.t() | FileError.t()}
  def init(dets_table, dets_dir) do
    with :ok <- ensure_dir_exists(dets_dir),
         :ok <- open_dets_table(dets_table, dets_dir) do
      :ok
    end
  end

  @spec ensure_dir_exists(Path.t()) :: :ok | {:error, FileError.t()}
  defp ensure_dir_exists(dir) do
    case File.mkdir_p(dir) do
      :ok -> :ok
      {:error, reason} -> {:error, %File.Error{action: :mkdir_p, path: dir, reason: reason}}
    end
  end

  @spec open_dets_table(:dets.tab_name(), Path.t()) :: :ok | {:error, DetsError.t()}
  defp open_dets_table(dets_table, dets_dir) do
    dets_file_path =
      dets_dir
      |> Path.join("#{dets_table}.dets")
      |> String.to_charlist()

    case :dets.open_file(dets_table, file: dets_file_path) do
      {:ok, _name} ->
        :ok

      {:error, reason} ->
        {:error, %DetsError{reason: :open_file, context: %{dets_reason: reason}}}
    end
  end

  @doc """
  Gets an instance by hash.
  """
  @spec get(:dets.tab_name(), Instance.instance_identifier()) ::
          {:ok, Instance.t()} | {:error, InstanceError.t()} | {:error, DetsError.t()}
  def get(dets_table, instance_identifier) do
    hash = hash_identifier(instance_identifier)

    case :dets.lookup(dets_table, hash) do
      [{^hash, instance}] ->
        {:ok, instance}

      [] ->
        {:error,
         %InstanceError{
           reason: :instance_identifier_not_found,
           context: %{instance_identifier: instance_identifier}
         }}

      {:error, reason} ->
        {:error, %DetsError{reason: :lookup, context: %{dets_reason: reason}}}
    end
  end

  @doc """
  Stores an instance with the given hash.
  """
  @spec put(:dets.tab_name(), Instance.t()) :: :ok | {:error, DetsError.t()}
  def put(dets_table, instance) do
    hash = hash_identifier(instance.identifier)

    case :dets.insert(dets_table, {hash, instance}) do
      :ok -> :ok
      {:error, reason} -> {:error, %DetsError{reason: :insert, context: %{dets_reason: reason}}}
    end
  end

  @doc """
  Deletes an instance by hash.
  """
  @spec delete(:dets.tab_name(), Instance.instance_identifier()) :: :ok | {:error, DetsError.t()}
  def delete(dets_table, instance_identifier) do
    hash = hash_identifier(instance_identifier)

    case :dets.delete(dets_table, hash) do
      :ok -> :ok
      {:error, reason} -> {:error, %DetsError{reason: :delete, context: %{dets_reason: reason}}}
    end
  end

  @doc """
  Checks if an instance exists for the given hash.
  """
  @spec exists?(:dets.tab_name(), Instance.instance_identifier()) ::
          {:ok, boolean()} | {:error, DetsError.t()}
  def exists?(dets_table, instance_identifier) do
    hash = hash_identifier(instance_identifier)

    case :dets.member(dets_table, hash) do
      {:error, reason} -> {:error, %DetsError{reason: :member, context: %{dets_reason: reason}}}
      exists? -> {:ok, exists?}
    end
  end

  @doc """
  Gets all instances.
  """
  @spec all(:dets.tab_name()) :: {:ok, list()} | {:error, DetsError.t()}
  def all(dets_table) do
    fn {_hash, instance}, acc -> [instance | acc] end
    |> :dets.foldl([], dets_table)
    |> case do
      {:error, reason} -> {:error, %DetsError{reason: :foldl, context: %{dets_reason: reason}}}
      all_instances -> {:ok, all_instances}
    end
  end

  @doc """
  Gets all instances for a specific specification.
  """
  @spec all(:dets.tab_name(), module()) :: {:ok, list()} | {:error, DetsError.t()}
  def all(dets_table, specification) do
    fn
      {_hash, %{specification: ^specification} = instance}, acc -> [instance | acc]
      {_hash, _instance}, acc -> acc
    end
    |> :dets.foldl([], dets_table)
    |> case do
      {:error, reason} -> {:error, %DetsError{reason: :foldl, context: %{dets_reason: reason}}}
      all_instances -> {:ok, all_instances}
    end
  end

  @doc """
  Deletes instances matching the given filter function.
  """
  @spec delete_matching(:dets.tab_name(), (Instance.t() -> as_boolean(term()))) ::
          :ok | {:error, DetsError.t()}
  def delete_matching(dets_table, filter_fun) do
    :dets.traverse(dets_table, fn {hash, instance} ->
      if filter_fun.(instance), do: :dets.delete(dets_table, hash)
      :continue
    end)
    |> case do
      {:error, reason} -> {:error, %DetsError{reason: :traverse, context: %{dets_reason: reason}}}
      _ -> :ok
    end
  end

  @doc """
  Traverses all instances with the given function.
  """
  @spec traverse(:dets.tab_name(), (Instance.t() -> term())) :: :ok | {:error, DetsError.t()}
  def traverse(dets_table, fun) do
    :dets.traverse(dets_table, fn {_hash, instance} ->
      fun.(instance)
      :continue
    end)
    |> case do
      {:error, reason} -> {:error, %DetsError{reason: :traverse, context: %{dets_reason: reason}}}
      _ -> :ok
    end
  end

  @doc """
  Deletes all instances from the DETS table.
  """
  @spec truncate(:dets.tab_name()) :: :ok | {:error, DetsError.t()}
  def truncate(dets_table) do
    case :dets.delete_all_objects(dets_table) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, %DetsError{reason: :delete_all_objects, context: %{dets_reason: reason}}}
    end
  end

  @doc """
  Closes the DETS table.
  """
  @spec close(:dets.tab_name()) :: :ok | {:error, DetsError.t()}
  def close(dets_table) do
    case :dets.close(dets_table) do
      :ok -> :ok
      {:error, reason} -> {:error, %DetsError{reason: :close, context: %{dets_reason: reason}}}
    end
  end

  @spec hash_identifier(Instance.instance_identifier()) :: non_neg_integer()
  defp hash_identifier(instance_identifier), do: :erlang.phash2(instance_identifier)
end