lib/cache/dets.ex

defmodule Cache.DETS do
  @opts_definition [
    ram_file: [
      type: :boolean,
      default: false,
      doc: "Enable RAM File"
    ],

    type: [
      type: {:in, [:bag, :duplicate_bag, :set]},
      default: :set,
      doc: "Data type of DETS cache"
    ],

    file_path: [
      type: :string,
      default: "./",
      doc: "File path to save DETS file at"
    ]
  ]

  @moduledoc """
  DETS adapter so that we can use dets as a cache

  ## Options
  #{NimbleOptions.docs(@opts_definition)}
  """

  use Task, restart: :permanent

  @behaviour Cache

  @impl Cache
  def opts_definition, do: @opts_definition

  @impl Cache
  def start_link(opts) do
    Task.start_link(fn ->
      table_name = opts[:table_name]
      file_path = opts[:file_path]
        |> to_string
        |> create_file_name(table_name)
        |> String.to_charlist

      opts =
        opts
        |> Keyword.drop([:table_name, :file_path])
        |> Kernel.++([access: :read_write, file: file_path])

      {:ok, _} = :dets.open_file(table_name, opts)

      Process.hibernate(Function, :identity, [nil])
    end)
  end

  defp create_file_name(file_path, table_name) do
    if File.dir?(file_path) do
      Path.join(file_path, "#{table_name}.dets")
    else
      file_path
    end
  end

  @impl Cache
  def child_spec({cache_name, opts}) do
    %{
      id: "#{cache_name}_elixir_cache_dets",
      start: {Cache.DETS, :start_link, [Keyword.put(opts, :table_name, cache_name)]}
    }
  end

  @impl Cache
  def get(cache_name, key, _opts \\ []) do
    case :dets.lookup(cache_name, key) do
      [{^key, value}] -> {:ok, value}
      [] -> {:ok, nil}
    end
  end

  @impl Cache
  def put(cache_name, key, _ttl \\ nil, value, _opts \\ []) do
    :dets.insert(cache_name, {key, value})
  end

  @impl Cache
  def delete(cache_name, key, _opts \\ []) do
    :dets.delete(cache_name, key)
  end
end