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

Runtime context passed to `Backpex.Preferences.Adapter` callbacks.

A context captures where a preference read/write originated and gives
adapters the handles they need (session, assigns, scope) without
forcing every adapter to know about `Plug.Conn` or LiveView socket
internals.

Populate via one of the builders:

- `from_mount/2` — LiveView mount / on_mount hook (read path).
- `from_conn/1` — Plug controller (write path over HTTP).
- `from_socket/2` — server-side preference writes from a LiveView.
- `coerce/1` — wraps a bare session map so callers that only have a
  session on hand can still use the dispatcher.

## The `scope` field

`scope` holds the preference namespace returned by the configured scope
resolver (see `Backpex.Preferences`). A resolved scope is a non-empty,
atom-keyed map, for example `%{user_id: user.id, tenant_id: tenant.id}`. It is `nil` before
the dispatcher runs resolution and `:unscoped` when the resolver could not
determine a namespace.

Resolution runs once for each unresolved context. Reusing a context whose
`scope` is already populated reuses the resolved scope. Entry points that
receive a bare session, conn, or socket build a fresh context and therefore
run the resolver again. Keep the resolver cheap.

# `scope`

```elixir
@type scope() :: %{required(atom()) =&gt; term()} | :unscoped | nil
```

# `source`

```elixir
@type source() :: :mount | :controller | :server
```

# `t`

```elixir
@type t() :: %Backpex.Preferences.Context{
  assigns: map(),
  client: %{optional(String.t()) =&gt; term()},
  scope: scope(),
  session: map(),
  source: source()
}
```

# `coerce`

Wrap a bare session map (or pass through an existing context) so call
sites that only have a session map can still call `Preferences.get/3` and
friends.

Accepts:

  * `%Backpex.Preferences.Context{}` — passed through unchanged.
  * A plain Phoenix session map (non-struct map with string keys, or the
    empty map `%{}`) — wrapped via `from_mount/1`.

Raises `ArgumentError` on any other shape. In particular, arbitrary maps
with atom keys, structs (other than `Context`), or non-map terms are
rejected rather than silently wrapped — wrapping them would mask caller
bugs and route a nonsense context into the adapter layer.

# `from_conn`

Build a context from a `%Plug.Conn{}` (write path over HTTP).

Extracts the session and assigns from the conn and discards the conn
itself — adapters receive the extracted values and never see the `conn`
directly, which keeps adapter code free of a `Plug.Conn` dependency.

# `from_mount`

Build a context for a read originating at LiveView mount.

`assigns` defaults to `%{}` for callers that only have a session on hand.

# `from_socket`

Build a context for a server-originated preference write from within a
LiveView (e.g. a `handle_event` that already knows the new value).

# `put_client`

Overlay client-supplied preference values on a context.

Reads through `Backpex.Preferences.get/3` and `get_map/3` prefer these
values over whatever the adapter has stored. They reach the server on
two carriers, both described in `Backpex.Preferences.LiveView`: the connect
params of every websocket join (which carry the writes a tab made *after* it
connected — writes the frozen connect-time session cannot see on a
`live_redirect` re-mount), and the `backpex_prefs` cookie (which carries the
writes the server has not acknowledged yet, so the disconnected mount can
render them before the write's POST has even landed).

Both payloads are written by the browser, so both are untrusted and are
filtered here:

  * keys that fail `Backpex.Preferences.Key.validate/1` are dropped — an
    unknown key would otherwise shadow a read for a prefix no adapter is
    configured to serve;
  * values that fail `Backpex.Preferences.Keys.valid_value?/2` are dropped —
    a wrong-typed value for a built-in key would otherwise reach a render,
    and a render must not raise on browser input (`not "false"` does).

Neither check is an authorization gate: a client may already write any value
it likes through the preferences endpoint. They exist so a planted or
truncated payload degrades to the stored value instead of taking the page
down. Values for keys Backpex does not own pass through unchecked — see
`Backpex.Preferences.Keys.valid_value?/2`.

## Examples

    iex> alias Backpex.Preferences.Context
    iex> ctx = Context.put_client(Context.from_mount(%{}), %{"global.theme" => "dark", "bogus.key" => 1})
    iex> ctx.client
    %{"global.theme" => "dark"}

    iex> alias Backpex.Preferences.Context
    iex> ctx = Context.put_client(Context.from_mount(%{}), %{"global.sidebar_open" => "false"})
    iex> ctx.client
    %{}

    iex> alias Backpex.Preferences.Context
    iex> ctx = Context.put_client(Context.from_mount(%{}), %{"global.sidebar_open" => false})
    iex> ctx.client
    %{"global.sidebar_open" => false}

# `put_scope`

Returns `%{ctx | scope: scope}`.

Called after the configured scope resolver runs. Adapter callbacks receive
the already-resolved scope, and subsequent dispatches that reuse this context
do not run the resolver again.

---

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