Skip to main content

lib/image/plug/variant_store/ets.ex

defmodule Image.Plug.VariantStore.ETS do
  @moduledoc """
  In-memory ETS-backed implementation of `Image.Plug.VariantStore`.

  A singleton owner process holds a `:protected` ETS table. Reads
  go straight to ETS; writes go through the owner process to
  serialise inserts and to populate the `:inserted_at` /
  `:updated_at` timestamps.

  ### Configuration

  Started by the `:image_plug` application with a default table
  name of `:image_plug_variants`. Variants are seeded from the
  application environment:

      # config/config.exs
      config :image_plug,
        variants: [
          {"thumbnail", "width=200,height=200,fit=cover,format=webp"},
          {"hero",      "width=1600,format=auto,quality=82"}
        ]

  Each entry value can be:

  * a Cloudflare-style options string (parsed by
    `Image.Plug.Provider.Cloudflare.Options`),

  * a `{provider, options_string, variant_options}` triple,

  * a pre-built `Image.Plug.Pipeline`, or

  * a complete `Image.Plug.Variant` struct.

  The implicit `"public"` variant is always seeded and represents
  Cloudflare's default "no transforms" behaviour. It can be overridden
  by adding an explicit `"public"` entry to the seeds.

  ### Per-call options

  Every callback accepts a `:table` keyword to address a non-default
  table — useful for tests and for hosts that run multiple isolated
  stores.
  """

  @behaviour Image.Plug.VariantStore

  alias Image.Plug.{Variant, VariantStore}

  @impl VariantStore
  def get(name, options \\ []) when is_binary(name) do
    table = Keyword.get(options, :table, default_table())

    case :ets.lookup(table, name) do
      [{^name, %Variant{} = variant}] -> {:ok, variant}
      [] -> {:error, :not_found}
    end
  end

  @impl VariantStore
  def put(%Variant{} = variant, options \\ []) do
    server = Keyword.get(options, :server, default_server())
    GenServer.call(server, {:put, variant})
  end

  @impl VariantStore
  def delete(name, options \\ []) when is_binary(name) do
    server = Keyword.get(options, :server, default_server())
    GenServer.call(server, {:delete, name})
  end

  @impl VariantStore
  def list(options \\ []) do
    table = Keyword.get(options, :table, default_table())

    variants =
      table
      |> :ets.tab2list()
      |> Enum.map(fn {_name, variant} -> variant end)
      |> Enum.sort_by(& &1.name)

    {:ok, variants}
  end

  @doc false
  def default_table, do: Image.Plug.VariantStore.ETS.Server.default_table()

  @doc false
  def default_server, do: Image.Plug.VariantStore.ETS.Server
end