Skip to main content

lib/image/plug/variant_store/persistence.ex

defmodule Image.Plug.VariantStore.Persistence do
  @moduledoc """
  Behaviour for persisting variants across application restarts.

  A persistence backend is responsible for two things:

  * `c:load/1` — called once at boot when the variant store
    initialises. Returns the list of previously-stored variants.
    The store seeds them into its table before applying any
    application-env seeds.

  * `c:write/4` — called after every successful `put` or `delete`.
    Write-through, fire-and-forget: failures are logged at `:warn`
    but do not fail the originating CRUD operation.

  ### Built-in backends

  * `Image.Plug.VariantStore.Persistence.File` — JSON-on-disk.

  ### Configuration

  Configure on the ETS variant store:

      plug Image.Plug,
        ...
        variant_store: {Image.Plug.VariantStore.ETS, [
          persistence:
            {Image.Plug.VariantStore.Persistence.File,
             path: "/var/lib/image_plug/variants.json"}
        ]}

  ### Persistable variants

  Only variants whose `:options` field is set (i.e. variants
  created from a CDN-grammar options string) are persisted. Variants
  built programmatically from an `Image.Plug.Pipeline` struct (no
  options string) cannot be round-tripped without a pipeline codec
  and are skipped with a warning. If you build pipelines
  programmatically and want them persisted, also set the
  `:options` field with an equivalent options string.

  ### Provider

  Persisted variants store the original options string, not the
  parsed pipeline. At load time the string is re-parsed via the
  `:provider` option (defaulting to `Image.Plug.Provider.Cloudflare`).
  This dodges the JSON-serialisation problem for pipeline ops,
  which embed atoms and struct types that don't survive a
  round-trip cleanly.
  """

  alias Image.Plug.Variant

  @doc """
  Loads previously-persisted variants.

  ### Arguments

  * `options` is the keyword list configured on the persistence
    entry.

  ### Returns

  * `{:ok, [variant]}` on success. Empty list is fine — first run.

  * `{:error, reason}` to abort boot. Use sparingly; a fresh
    install with no persisted state is the normal first-boot
    condition and should return `{:ok, []}`, not an error.
  """
  @callback load(options :: keyword()) :: {:ok, [Variant.t()]} | {:error, term()}

  @doc """
  Writes a single variant change.

  Called after every successful `put` or `delete` on the variant
  store. Receives the action (`:put` or `:delete`), the variant
  name, and either the new variant struct (for `:put`) or `nil`
  (for `:delete`).

  ### Returns

  * `:ok` on success.

  * `{:error, reason}` on failure. The store logs the error at
    `:warn` but does not fail the originating CRUD call —
    persistence is fire-and-forget.
  """
  @callback write(
              action :: :put | :delete,
              name :: String.t(),
              variant :: Variant.t() | nil,
              options :: keyword()
            ) :: :ok | {:error, term()}
end