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

Maps a preference key to the adapter configured to handle it.

## Route format

A route is `{pattern, adapter_module}` or `{pattern, adapter_module,
adapter_opts}`, where `pattern` is one of:

- an **exact key** like `"global.theme"`, matched by equality;
- a **wildcard** like `"resource.*"` — a prefix followed by a trailing
  `"*"`, matching every key under that prefix (and the prefix itself);
- the atom **`:default`**, the fallback used when nothing else matches.

Wildcards are segmented by `Backpex.Preferences.Key.parse/1`, the same
function that segments keys. A pattern therefore addresses exactly the
segments a key is built from, including the colon-separated form used for
per-resource keys:

    Backpex.Preferences.Keys.columns(MyApp.UserLive)
    #=> "resource:MyApp.UserLive:columns"

    # covered by any of:
    "resource.*"                    # every resource
    "resource:MyApp.UserLive:*"     # just this resource
    "resource:MyApp.UserLive:columns"

`"*"` is only meaningful as the final segment. Any other placement (`"*"`
alone, `"resource.*.columns"`, `"res*"`) is a configuration error and
raises when routes are first normalized (normally on the first preference
resolution) — it could never match a key, and a pattern that silently matches
nothing is worse than one that is rejected.

## Match strategy

Longest-prefix-first: among the matching patterns the one with the most
segments wins, and an exact pattern beats a wildcard at the same depth.
Specificity alone decides, so a narrow route overrides a broad one no
matter which order they appear in config. `:default` wins only when
nothing else matches.

## Configuration

    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, []}

With no `:adapters` config the router falls back to a single
`{:default, Backpex.Preferences.Adapters.Session, []}` route, so the
zero-config behavior routes every key to the Session adapter. Once an
`:adapters` list is configured there is no implicit fallback; add an explicit
`:default` route unless the configured patterns cover every key.

# `pattern`

```elixir
@type pattern() :: String.t() | :default
```

# `route`

```elixir
@type route() :: {pattern(), module(), keyword()}
```

# `normalize`

Normalizes a raw route list, canonicalizing two-tuple entries to three-tuple
form and validating shape.

Raises `ArgumentError` with a descriptive message for malformed entries: a
bad adapter module, an unusable pattern, or a wildcard that could never
match a key.

# `resolve`

Returns the matching `{module, opts}` for `key`, or raises if no route
(including `:default`) matches.

This is the resolution entry point for point reads and writes. Subtree reads
use `resolve_subtree/2` because exact routes and nested wildcards can carve
keys beneath a broader prefix into another adapter.

## Examples

    iex> routes = [
    ...>   {"global.*", Backpex.Preferences.Adapters.Session, []},
    ...>   {:default, Backpex.Preferences.Adapters.Session, []}
    ...> ]
    iex> Backpex.Preferences.Router.resolve("global.theme", routes)
    {Backpex.Preferences.Adapters.Session, []}

    iex> routes = [
    ...>   {"resource:MyApp.UserLive:*", MyApp.EctoAdapter, repo: MyApp.Repo},
    ...>   {"resource.*", Backpex.Preferences.Adapters.Session, []}
    ...> ]
    iex> Backpex.Preferences.Router.resolve("resource:MyApp.UserLive:columns", routes)
    {MyApp.EctoAdapter, [repo: MyApp.Repo]}

# `resolve_subtree`

Returns the routes that can own `prefix` or keys beneath it, ordered from
broadest to most specific.

A subtree can span adapters when an exact route or nested wildcard carves a
key out of a broader route. `Backpex.Preferences.get_map/3` reads these
routes in order so the more specific route wins for the part it owns.

## Examples

    iex> routes = [
    ...>   {"global.sidebar_section.blog", MyApp.DatabaseAdapter, []},
    ...>   {"global.*", Backpex.Preferences.Adapters.Session, []}
    ...> ]
    iex> Backpex.Preferences.Router.resolve_subtree("global.sidebar_section", routes)
    [
      {"global.*", Backpex.Preferences.Adapters.Session, []},
      {"global.sidebar_section.blog", MyApp.DatabaseAdapter, []}
    ]

# `routes`

Loads the configured routes, falling back to a Session-adapter default when
no config is set.

---

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