lib/nostrum/cache/channel_cache/ets.ex

defmodule Nostrum.Cache.ChannelCache.ETS do
  @table_name :nostrum_channels
  @moduledoc """
  An ETS-based cache for channels.

  The supervisor defined by this module will set up the ETS table associated
  with it.

  The default table name under which channels are cached is `#{@table_name}`.
  In addition to the cache behaviour implementations provided by this module,
  you can also call regular ETS table methods on it, such as `:ets.info`.

  Note that users should not call the functions not related to this specific
  implementation of the cache directly. Instead, call the functions of
  `Nostrum.Cache.ChannelCache` directly, which will dispatch to the configured
  cache.
  """
  @moduledoc since: "0.5.0"

  @behaviour Nostrum.Cache.ChannelCache

  alias Nostrum.Cache.ChannelCache
  alias Nostrum.Cache.GuildCache
  alias Nostrum.Struct.Channel
  alias Nostrum.Util
  import Nostrum.Snowflake, only: [is_snowflake: 1]
  use Supervisor

  @doc "Start the supervisor."
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @doc "Set up the cache's ETS table."
  @impl Supervisor
  def init(_init_arg) do
    :ets.new(tabname(), [:set, :public, :named_table])
    Supervisor.init([], strategy: :one_for_one)
  end

  @doc "Retrieve the ETS table name used for the cache."
  @spec tabname :: atom()
  def tabname, do: @table_name

  # IMPLEMENTATION
  @doc "Retrieve a channel from the cache by ID."
  @impl ChannelCache
  @spec get(Channel.id() | Nostrum.Struct.Message.t()) ::
          {:ok, Channel.t()} | {:error, ChannelCache.reason()}
  def get(%Nostrum.Struct.Message{channel_id: channel_id}), do: get(channel_id)

  def get(id) when is_snowflake(id) do
    case lookup(id) do
      {:ok, channel} -> {:ok, convert(channel)}
      error -> error
    end
  end

  @doc "Same as `get/1`, but raises `Nostrum.Error.CacheError` in case of a failure."
  @impl ChannelCache
  @spec get!(Channel.id() | Nostrum.Struct.Message.t()) :: no_return | Channel.t()
  def get!(%Nostrum.Struct.Message{channel_id: channel_id}), do: get!(channel_id)
  def get!(id) when is_snowflake(id), do: id |> get |> Util.bangify_find(id, __MODULE__)

  @doc "Converts and creates the given map as a channel in the cache."
  @impl ChannelCache
  @spec create(map) :: Channel.t()
  def create(channel) do
    :ets.insert(tabname(), {channel.id, channel})
    convert(channel)
  end

  @doc "Update the given channel in the cache."
  @impl ChannelCache
  @spec update(Channel.t()) :: :noop | {Channel.t(), Channel.t()}
  def update(channel) do
    case lookup(channel.id) do
      {:ok, old_channel} ->
        {convert(old_channel), convert(channel)}

      _ ->
        :noop
    end
  end

  @doc "Delete the channel from the cache by ID."
  @impl ChannelCache
  @spec delete(Channel.id()) :: :noop | Channel.t()
  def delete(id) do
    case lookup(id) do
      {:ok, channel} ->
        :ets.delete(tabname(), id)
        convert(channel)

      _ ->
        :noop
    end
  end

  @doc "Lookup a channel from the cache by ID and if it does not exist try to get from GuildCache with select_by"
  @impl ChannelCache
  @spec lookup(Channel.id()) :: {:error, :channel_not_found} | {:ok, map}
  def lookup(id) do
    case :ets.lookup(tabname(), id) do
      [] ->
        [channel_id: id]
        |> GuildCache.select_by(fn %{channels: channels} ->
          Map.get(channels, id, {:error, :id_not_found})
        end)
        |> case do
          {:error, :id_not_found} ->
            {:error, :channel_not_found}

          res ->
            res
        end

      [{^id, channel}] ->
        {:ok, channel}
    end
  end

  # Converts a map into a Channel
  defp convert(%{__struct__: _} = struct), do: struct
  defp convert(map), do: Channel.to_struct(map)
end