Skip to main content

lib/ex_codecs/codec_registry.ex

defmodule ExCodecs.CodecRegistry do
  @moduledoc """
  Runtime codec registry for ExCodecs.

  The registry maintains a mapping of codec names to their implementations
  and metadata. Codecs are registered at application startup and can be
  queried at runtime.

  ## Registry Operations

      iex> ExCodecs.available_codecs()
      [:blosc2, :bzip2, :lz4, :snappy, :zstd]

      iex> ExCodecs.supports?(:zstd)
      true

      iex> {:ok, info} = ExCodecs.codec_info(:zstd)
      iex> info.name
      :zstd

  The registry is backed by an ETS table for fast lookups and is
  populated when the application starts.
  """

  use Agent

  @table_name :ex_codecs_registry
  @registry_name __MODULE__

  @doc """
  Starts the registry agent.
  """
  @spec start_link(keyword()) :: Agent.on_start()
  def start_link(_opts \\ []) do
    Agent.start_link(fn -> :ets.new(@table_name, [:set, :public, :named_table]) end,
      name: @registry_name
    )
  end

  @doc """
  Registers a codec with the registry.

  ## Arguments

    * `name` - The atom name of the codec (e.g., `:zstd`)
    * `module` - The module implementing `ExCodecs.Codec`
    * `category` - The codec category (e.g., `:compression`)

  ## Returns

    * `:ok` - Codec registered successfully
    * `{:error, reason}` - Registration failed
  """
  @spec register(atom(), module(), atom()) :: :ok | {:error, term()}
  def register(name, module, category) do
    codec_info = build_codec_info(name, module, category)

    case ExCodecs.Codec.validates?(module) do
      true ->
        :ets.insert(@table_name, {name, {module, category, codec_info}})
        :ok

      false ->
        {:error, {:invalid_codec_module, module}}
    end
  end

  @doc """
  Registers a codec as unavailable (known but not loadable).
  """
  @spec register_unavailable(atom(), atom()) :: :ok
  def register_unavailable(name, category) do
    codec_info = %ExCodecs.Codec{
      name: name,
      category: category,
      module: nil,
      native?: false,
      streaming?: false,
      configurable?: false,
      version: nil
    }

    :ets.insert(@table_name, {name, {nil, category, codec_info}})
    :ok
  end

  @doc """
  Looks up a codec by name.

  ## Returns

    * `{:ok, {module, category, info}}` - Codec found
    * `{:error, :unsupported_codec}` - Codec not found
  """
  @spec lookup(atom()) ::
          {:ok, {module(), atom(), ExCodecs.Codec.t()}} | {:error, :unsupported_codec}
  def lookup(name) when is_atom(name) do
    case :ets.lookup(@table_name, name) do
      [{^name, {module, category, info}}] ->
        {:ok, {module, category, info}}

      [] ->
        {:error, :unsupported_codec}
    end
  end

  @doc """
  Returns a list of all available codec names.
  """
  @spec available_codecs() :: [atom()]
  def available_codecs do
    :ets.tab2list(@table_name)
    |> Enum.filter(fn {_name, {module, _category, _info}} -> module != nil end)
    |> Enum.map(fn {name, _} -> name end)
    |> Enum.sort()
  end

  @doc """
  Returns all registered codec names (including unavailable ones).
  """
  @spec all_codecs() :: [atom()]
  def all_codecs do
    :ets.tab2list(@table_name)
    |> Enum.map(fn {name, _} -> name end)
    |> Enum.sort()
  end

  @doc """
  Checks if a codec is supported and available.
  """
  @spec supports?(atom()) :: boolean()
  def supports?(name) when is_atom(name) do
    case lookup(name) do
      {:ok, {module, _category, _info}} -> module != nil
      {:error, :unsupported_codec} -> false
    end
  end

  @doc """
  Returns detailed information about a codec.
  """
  @spec codec_info(atom()) :: {:ok, ExCodecs.Codec.t()} | {:error, :unsupported_codec}
  def codec_info(name) when is_atom(name) do
    case lookup(name) do
      {:ok, {_module, _category, info}} -> {:ok, info}
      {:error, :unsupported_codec} -> {:error, :unsupported_codec}
    end
  end

  @doc """
  Returns all codecs in a given category.
  """
  @spec codecs_by_category(atom()) :: [ExCodecs.Codec.t()]
  def codecs_by_category(category) when is_atom(category) do
    :ets.tab2list(@table_name)
    |> Enum.filter(fn {_name, {_module, cat, _info}} -> cat == category end)
    |> Enum.map(fn {_name, {_module, _cat, info}} -> info end)
    |> Enum.sort_by(& &1.name)
  end

  defp build_codec_info(name, module, category) do
    info =
      if function_exported?(module, :__codec_info__, 0) do
        module.__codec_info__()
      else
        %ExCodecs.Codec{}
      end

    %ExCodecs.Codec{
      name: name,
      category: category,
      module: module,
      native?: info.native?,
      streaming?: info.streaming?,
      configurable?: info.configurable?,
      version: info.version
    }
  end
end