Skip to main content

lib/ex_codecs.ex

defmodule ExCodecs do
  @moduledoc """
  An extensible BEAM-native codec framework for Elixir.

  ExCodecs provides a unified API for compression, decompression, hashing,
  checksums, binary encodings, and future content-addressing codecs.

  ## Quick Start

      # Compression
      {:ok, compressed} = ExCodecs.encode(:zstd, my_binary)
      {:ok, original} = ExCodecs.decode(:zstd, compressed)

      # With options
      {:ok, compressed} = ExCodecs.encode(:zstd, my_binary, level: 3)
      {:ok, compressed} = ExCodecs.encode(:blosc2, my_binary, cname: :zstd, clevel: 5, shuffle: :byte)

      # Discovery
      ExCodecs.available_codecs()   #=> [:blosc2, :bzip2, :lz4, :snappy, :zstd]
      ExCodecs.supports?(:zstd)     #=> true
      ExCodecs.codec_info(:zstd)    #=> {:ok, %ExCodecs.Codec{...}}

  ## Supported Codecs

  | Codec    | Category    | Description                          |
  |----------|-------------|--------------------------------------|
  | `:zstd`  | compression | Zstandard - high ratio, good speed   |
  | `:lz4`   | compression | LZ4 - extremely fast                 |
  | `:snappy`| compression | Snappy - fast, low overhead          |
  | `:bzip2` | compression | Bzip2 - high ratio, slower           |
  | `:blosc2`| compression | Blosc2 - meta-compressor for arrays  |

  ## Design Philosophy

  ExCodecs is not a compression library. It is a codec framework.
  Compression is merely the first codec category. The architecture
  supports future expansion into hashing, checksums, binary encodings,
  content addressing, and streaming — without changing the public API.
  """

  alias ExCodecs.{Codec, CodecRegistry, Error}

  @doc """
  Encodes data using the specified codec.

  For compression codecs, this compresses the data. For other codec
  categories, the semantics depend on the codec type.

  ## Arguments

   * `codec` - The codec atom (e.g., `:zstd`, `:lz4`)
   * `data` - The binary data to encode
   * `opts` - Codec-specific options (default: `[]`)

  ## Returns

   * `{:ok, encoded_binary}` - Successfully encoded data
   * `{:error, %ExCodecs.Error{}}` - Encoding failed

  ## Examples

     iex> {:ok, compressed} = ExCodecs.encode(:zstd, "hello world")
     iex> is_binary(compressed)
     true

     iex> {:ok, compressed} = ExCodecs.encode(:zstd, "hello world", level: 3)
     iex> is_binary(compressed)
     true
  """
  @spec encode(atom(), binary(), keyword()) :: {:ok, binary()} | {:error, Error.t()}
  def encode(codec, data, opts \\ [])

  def encode(codec, data, opts) when is_atom(codec) and is_binary(data) and is_list(opts) do
    case CodecRegistry.lookup(codec) do
      {:ok, {module, _category, info}} ->
        case ensure_available(info, codec) do
          :ok -> module.encode(data, opts)
          {:error, %Error{} = error} -> {:error, error}
        end

      {:error, :unsupported_codec} ->
        {:error, Error.new(:unsupported_codec, codec: codec)}
    end
  end

  def encode(_codec, _data, _opts) do
    {:error, Error.new(:invalid_data, message: "Data must be a binary")}
  end

  @doc """
  Decodes data using the specified codec.

  For compression codecs, this decompresses the data.

  ## Arguments

    * `codec` - The codec atom (e.g., `:zstd`, `:lz4`)
    * `data` - The binary data to decode
    * `opts` - Codec-specific options (default: `[]`)

  ## Returns

    * `{:ok, decoded_binary}` - Successfully decoded data
    * `{:error, %ExCodecs.Error{}}` - Decoding failed

  ## Examples

      iex> {:ok, compressed} = ExCodecs.encode(:zstd, "hello world")
      iex> {:ok, original} = ExCodecs.decode(:zstd, compressed)
      iex> original
      "hello world"
  """
  @spec decode(atom(), binary(), keyword()) :: {:ok, binary()} | {:error, Error.t()}
  def decode(codec, data, opts \\ [])

  def decode(codec, data, opts) when is_atom(codec) and is_binary(data) and is_list(opts) do
    case CodecRegistry.lookup(codec) do
      {:ok, {module, _category, info}} ->
        case ensure_available(info, codec) do
          :ok -> module.decode(data, opts)
          {:error, %Error{} = error} -> {:error, error}
        end

      {:error, :unsupported_codec} ->
        {:error, Error.new(:unsupported_codec, codec: codec)}
    end
  end

  def decode(_codec, _data, _opts) do
    {:error, Error.new(:invalid_data, message: "Data must be a binary")}
  end

  @doc """
  Returns a list of all available codec names.

  Only codecs that are loadable and functional are included.

  ## Examples

      iex> :blosc2 in ExCodecs.available_codecs()
      true
  """
  @spec available_codecs() :: [atom()]
  def available_codecs do
    CodecRegistry.available_codecs()
  end

  @doc """
  Checks if a codec is supported and available at runtime.

  Returns `true` only if the codec is both registered and its
  native implementation is loaded.

  ## Examples

      iex> ExCodecs.supports?(:zstd)
      true

      iex> ExCodecs.supports?(:nonexistent)
      false
  """
  @spec supports?(atom()) :: boolean()
  def supports?(codec) when is_atom(codec) do
    CodecRegistry.supports?(codec)
  end

  @doc """
  Returns detailed information about a codec.

  ## Returns

    * `{:ok, %ExCodecs.Codec{}}` - Codec information
    * `{:error, :unsupported_codec}` - Codec not found

  ## Examples

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

      iex> {:ok, info} = ExCodecs.codec_info(:zstd)
      iex> info.category
      :compression
  """
  @spec codec_info(atom()) :: {:ok, Codec.t()} | {:error, :unsupported_codec}
  def codec_info(codec) when is_atom(codec) do
    CodecRegistry.codec_info(codec)
  end

  defp ensure_available(info, codec) do
    if info.module != nil do
      :ok
    else
      {:error, Error.new(:codec_unavailable, codec: codec)}
    end
  end
end