Backpex.Preferences.Router (Backpex v0.20.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

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

Types

pattern()

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

route()

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

Functions

normalize(routes)

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(key, routes \\ routes())

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(prefix, routes \\ routes())

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.