# `Backpex.Preferences.Adapter`
[🔗](https://github.com/naymspace/backpex/blob/0.20.0/lib/backpex/preferences/adapter.ex#L1)

Behavior implemented by Backpex preference storage adapters.

`Backpex.Preferences` dispatches each call to an adapter selected by the
key's prefix. Backpex ships with `Backpex.Preferences.Adapters.Session` by default and
configure others per prefix:

    config :backpex, Backpex.Preferences,
      adapters: [
        {"global.*",   Backpex.Preferences.Adapters.Session, []},
        {"resource.*", Backpex.Preferences.Adapters.Ecto,
         repo: MyApp.Repo, schema: MyApp.Preference, scope_fields: [:user_id, :tenant_id]},
        {:default,     Backpex.Preferences.Adapters.Session, []}
      ],
      scope: {MyAppWeb.PreferencesScope, :resolve, []}

## Return semantics

The three atoms in the return types deserve a note:

- `{:ok, :not_found}` — the adapter successfully determined that no value is
  stored for this key. `Backpex.Preferences.get/3` callers fall back to
  their `:default` option.
- `{:error, :unscoped}` — the adapter needs a resolved preference scope (see
  `Backpex.Preferences.Context.scope`) and does not have one. Reads
  should be treated as "not found"; writes surface `{ok: false}` to the
  caller without crashing.
- `{:error, :requires_http}` — the adapter can only write via a
  `%Plug.Conn{}` (e.g. the Session adapter writing to the session cookie)
  and was invoked from a context that has no conn (mount / socket). The
  server-side helper in `Backpex.Preferences` catches this and falls back to
  a `push_event/3` round-trip so the browser can retry via the HTTP
  endpoint.
- `{:error, :too_large}` — the value would push the adapter's store past a
  size limit it cannot exceed (e.g. the ~4KB cookie session store). The
  write is refused whole; the previously stored value is untouched.
  `Backpex.PreferencesController` surfaces this as a `422`. An adapter with
  no meaningful size ceiling never returns it.

## Side-effect protocol

A write describes what the caller should do rather than mutating the conn
itself. `c:put/4` returns exactly one of:

- `{:ok, :persisted}` — the adapter stored the value on its own (a database
  write) and needs nothing from the caller.
- `{:ok, {:put_session, key, map}}` — the adapter needs the caller to put
  `map` under the given Phoenix session key.

Keeping adapters side-effect-free this way lets them be exercised in unit
tests without a conn and supports server-side writes that do not have one.

`{:put_session, _, _}` can only be honored on a `%Plug.Conn{}`;
`Plug.Session` is HTTP-only. An adapter that stores in the session must
therefore return `{:error, :requires_http}` when it is called outside a
controller, so the dispatcher can round-trip the write through the browser.

## Retry semantics

`c:put/4` must be idempotent for the same key and value. Browser writes may
be coalesced into a batch. If one entry is rejected, Backpex retries the
other entries because the controller discards accumulated Session effects
and does not dispatch entries after the failure. An eager adapter may
therefore receive the same successful put twice. Implement durable writes
as assignment/upsert operations; do not attach non-idempotent work such as
counters, notifications, or one-shot external calls directly to `put/4`.

# `put_result`

```elixir
@type put_result() :: :persisted | side_effect()
```

The outcome of a successful `c:put/4`.

Either the adapter persisted the value itself (`:persisted`) or it needs
the caller to apply a single side effect.

# `side_effect`

```elixir
@type side_effect() :: {:put_session, key :: String.t(), value :: map()}
```

Work the caller is responsible for applying after `c:put/4`.

Asks the caller to put `value` under `key` in the Phoenix session.

# `client_namespace`
*optional* 

```elixir
@callback client_namespace(ctx :: Backpex.Preferences.Context.t(), opts :: keyword()) ::
  {:ok, term()} | {:error, :unscoped | term()}
```

Returns the adapter namespace used to scope browser-carried preference values.

Backpex signs this value together with the adapter module and the Phoenix
session identity. The resulting opaque token lets the browser carry values
across LiveView joins without leaking a value into a different storage
namespace. Return only terms that are stable for the lifetime of the
underlying preference namespace.

This callback is optional. Adapters that omit it conservatively fall back to
the complete application preference scope and their route options. Implement
it when the adapter uses a projection of that scope or ignores it entirely.

# `get`

```elixir
@callback get(
  ctx :: Backpex.Preferences.Context.t(),
  key :: String.t(),
  opts :: keyword()
) ::
  {:ok, term()} | {:ok, :not_found} | {:error, :unscoped | term()}
```

Read a single key.

Return `{:ok, :not_found}` when no value is stored; callers fall back to
their `:default` option.

# `get_map`

```elixir
@callback get_map(
  ctx :: Backpex.Preferences.Context.t(),
  prefix :: String.t(),
  opts :: keyword()
) ::
  {:ok, map()} | {:error, :unscoped | term()}
```

Read every value under `prefix` and return them as a nested map.

The returned map mirrors the structure that a `get/3` at that prefix would
have produced if there were a single nested value — i.e. it is keyed by the
path segments that come after `prefix`, not by full dotted/coloned keys.

# `put`

```elixir
@callback put(
  ctx :: Backpex.Preferences.Context.t(),
  key :: String.t(),
  value :: term(),
  opts :: keyword()
) ::
  {:ok, put_result()}
  | {:error, :unscoped | :requires_http | :too_large | term()}
```

Persist a value.

Return `{:ok, :persisted}` when the adapter stored the value itself (a DB
write), or `{:ok, {:put_session, key, map}}` to ask the caller to apply the
one side effect the adapter cannot perform on its own (see the module docs).

Refuse rather than emit a write the store cannot hold: an adapter with a
size ceiling returns `{:error, :too_large}` when the value would breach it,
leaving the stored value untouched.

This callback must be idempotent for the same key and value; see "Retry
semantics" in the module documentation.

# `deep_put`

```elixir
@spec deep_put(map(), [String.t(), ...], term()) :: map()
```

Writes `value` at `path` in a nested map, creating intermediate maps.

A non-map sitting at an intermediate segment is replaced: preference keys
form a tree, and a key deeper than an existing leaf wins over it.

    iex> Backpex.Preferences.Adapter.deep_put(%{}, ["a", "b"], 1)
    %{"a" => %{"b" => 1}}

    iex> Backpex.Preferences.Adapter.deep_put(%{"a" => 1}, ["a", "b"], 2)
    %{"a" => %{"b" => 2}}

# `nest`

```elixir
@spec nest([{String.t(), term()}], String.t()) :: map()
```

Builds the nested map `c:get_map/3` must return from flat `{key, value}` rows.

Stores that keep one entry per full key — a database table, Redis, ETS —
cannot answer `c:get_map/3` directly: the callback is specified in terms of
the nested shape a `c:get/3` at that prefix would have produced. Hand it
every row whose key starts with `prefix` and it does the rest.

Rows that are not descendants of `prefix` are dropped, so an over-broad
fetch is safe. That matters more than it looks: SQL `LIKE` treats `_` as a
wildcard, so a prefix such as `global.sidebar_section` also matches a stored
`global.sidebarXsection.blog`. Matching happens on parsed key segments
(`Backpex.Preferences.Key.parse/1`), never on raw string prefixes, so a key
is only included when it shares a whole-segment boundary with `prefix`.

A row whose key *is* `prefix` has nothing below it and is dropped too.

    iex> alias Backpex.Preferences.Adapter
    iex> Adapter.nest([{"global.sidebar_section.blog", true}], "global.sidebar_section")
    %{"blog" => true}

    iex> alias Backpex.Preferences.Adapter
    iex> Adapter.nest([{"global.sidebar_section.blog", true}], "global")
    %{"sidebar_section" => %{"blog" => true}}

    iex> alias Backpex.Preferences.Adapter
    iex> Adapter.nest([{"global.sidebarXsection.blog", true}], "global.sidebar_section")
    %{}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
