defmodule FSST do @moduledoc """ Fast Static Symbol Tables compression for Elixir. `FSST` is a pure Elixir port with an optional Rustler backend planned around [`fsst-rs`](https://docs.rs/fsst-rs/latest/fsst/). The API follows typical Elixir conventions: `train/2` returns `{:ok, table}` or `{:error, reason}`, while `train!/2`, `compress!/3`, and `decompress!/3` raise on failure. """ alias FSST.Table @type table :: Table.t() @type reason :: :backend_unavailable | :invalid_input | :invalid_sample | :truncated_escape | term() @doc """ Trains an FSST table from representative binary samples. """ @spec train([binary()], keyword()) :: {:ok, table()} | {:error, reason()} def train(samples, opts \\ []) do opts |> backend() |> then(& &1.train(samples, opts)) end @doc """ Trains an FSST table or raises when training fails. """ @spec train!([binary()], keyword()) :: table() def train!(samples, opts \\ []) do case train(samples, opts) do {:ok, table} -> table {:error, reason} -> raise ArgumentError, "could not train FSST table: #{inspect(reason)}" end end @doc """ Compresses a binary with a table returned by `train/2`. """ @spec compress(table(), binary(), keyword()) :: {:ok, binary()} | {:error, reason()} def compress(table, input, opts \\ []) def compress(%Table{} = table, input, opts) do table |> table_backend(opts) |> then(& &1.compress(table, input)) end def compress(_table, _input, _opts), do: {:error, :invalid_input} @doc """ Compresses a binary or raises on failure. """ @spec compress!(table(), binary(), keyword()) :: binary() def compress!(table, input, opts \\ []) do case compress(table, input, opts) do {:ok, compressed} -> compressed {:error, reason} -> raise ArgumentError, "could not compress input: #{inspect(reason)}" end end @doc """ Decompresses a binary with the same table used for compression. """ @spec decompress(table(), binary(), keyword()) :: {:ok, binary()} | {:error, reason()} def decompress(table, input, opts \\ []) def decompress(%Table{} = table, input, opts) do table |> table_backend(opts) |> then(& &1.decompress(table, input)) end def decompress(_table, _input, _opts), do: {:error, :invalid_input} @doc """ Decompresses a binary or raises on failure. """ @spec decompress!(table(), binary(), keyword()) :: binary() def decompress!(table, input, opts \\ []) do case decompress(table, input, opts) do {:ok, decompressed} -> decompressed {:error, reason} -> raise ArgumentError, "could not decompress input: #{inspect(reason)}" end end @doc """ Returns the backend module selected for the current options and runtime. """ @spec backend(keyword()) :: module() def backend(opts \\ []) do case Keyword.get(opts, :backend, :auto) do :auto -> auto_backend() :pure -> FSST.Pure :rust -> FSST.Rust module when is_atom(module) -> module end end @doc false @spec compress_sample([binary()], keyword()) :: {:ok, table()} | {:error, reason()} def compress_sample(samples, opts \\ []), do: train(samples, opts) defp auto_backend do if FSST.Rust.available?(), do: FSST.Rust, else: FSST.Pure end defp table_backend(_table, opts) when opts != [], do: backend(opts) defp table_backend(%Table{backend: backend}, _opts), do: backend end