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

Parsing and construction helpers for preference keys.

Preference keys are string identifiers made of path segments. Two forms are
understood:

- Dot-separated: `"global.theme"` — the default form for keys with no
  embedded module names.
- Colon-separated: `"resource:MyApp.MyLive:columns"` — used when a segment
  itself contains dots (typically because it embeds a module name). Colon is
  a safe secondary separator that avoids dot-collision inside that segment.

The colon form takes precedence: if the key contains `":"` anywhere, the
parser splits on `":"`. Otherwise it splits on `"."`.

## Why two forms?

Module names in Elixir already contain dots (`Elixir.DemoWeb.PostLive`). Using
them inside dot-separated keys creates accidental nesting:
`"resource.Elixir.DemoWeb.PostLive.columns"` splits into five path segments,
making stored preferences hard to reason about. Switching the whole key to
colons lets the module live as a single atomic segment.

## Separator precedence

A single `":"` anywhere in the key flips the whole key to colon-split
parsing. There is no "mixed" mode. Concretely:

- `"global.theme"` — no colon → dot-split → `["global", "theme"]`
- `"resource:Backpex.Users:columns"` — colon present → colon-split →
  `["resource", "Backpex.Users", "columns"]`
- `"custom.bad:key"` — stray colon wins → colon-split →
  `["custom.bad", "key"]` (the `.` inside `"custom.bad"` is *not* split)

The last example is almost certainly not what the caller intended. Prefer
`Backpex.Preferences.Key.resource_key/2` when building keys that embed a
module name so the colon form is applied deliberately.

Route patterns (`Backpex.Preferences.Router`) are segmented by the same
rule, so a pattern and the keys it is written to cover always agree on
where the segment boundaries fall — `"resource:MyApp.MyLive:*"` covers the
module as one segment, exactly as `resource_key/2` emits it.

## Edge cases

`parse/1` is intentionally lenient: it never raises for any binary input and
applies the separator rule uniformly. As a result, leading, trailing, or
consecutive separators produce empty string segments (e.g. `":foo"` →
`["", "foo"]`, `"resource:Foo:"` → `["resource", "Foo", ""]`), and the empty
string parses to `[""]` rather than `[]`. Non-ASCII module names pass through
unchanged because the function splits on byte-level delimiters without
normalization. See `test/preferences/key_test.exs` for the pinned corner
cases.

## Key validation

`validate/1` checks a key's first segment against the top-level prefixes
Backpex serves: `"global"`, `"resource"`, and `"custom"`. App-owned keys
belong under `"custom.<your-domain>.<key>"`.

Its job is to filter *untrusted* keys at the client trust boundary:
`Backpex.Preferences.Context.put_client/2` drops browser-supplied keys
that fail this check, so a planted key cannot shadow a read for a prefix
no adapter is configured to serve. Validation is pure — it never logs and
never raises.

# `encode_module`

Encodes a module atom for use as a single path segment.

Pair with the colon-separated key form so dots inside the module name do not
create accidental nesting.

## Examples

    iex> Backpex.Preferences.Key.encode_module(Backpex.Preferences)
    "Backpex.Preferences"

    iex> Backpex.Preferences.Key.resource_key(Backpex.Preferences, "columns")
    "resource:Backpex.Preferences:columns"

# `match?`

Returns true when `pattern` matches `key`.

A pattern is either an exact key, which matches by string equality, or a
wildcard ending in `"*"` (see `wildcard_prefix/1`), which matches every key
whose leading segments are the wildcard's segments — including the key that
is the prefix itself.

Both sides are segmented with `parse/1`, so the wildcard's separator does
not have to match the key's: `"resource.*"` and `"resource:*"` cover the
same keys.

## Examples

    iex> Backpex.Preferences.Key.match?("resource.*", "resource:MyApp.MyLive:columns")
    true

    iex> Backpex.Preferences.Key.match?("resource:MyApp.MyLive:*", "resource:MyApp.MyLive:columns")
    true

    iex> Backpex.Preferences.Key.match?("resource:MyApp.MyLive:*", "resource:MyApp.OtherLive:columns")
    false

    iex> Backpex.Preferences.Key.match?("global.*", "global.theme")
    true

    iex> Backpex.Preferences.Key.match?("global.*", "resource.foo")
    false

    iex> Backpex.Preferences.Key.match?("global.theme", "global.theme")
    true

# `parse`

Splits a key into path segments.

## Examples

    iex> Backpex.Preferences.Key.parse("global.theme")
    ["global", "theme"]

    iex> Backpex.Preferences.Key.parse("resource:MyApp.MyLive:columns")
    ["resource", "MyApp.MyLive", "columns"]

    iex> Backpex.Preferences.Key.parse("global")
    ["global"]

# `resource_key`

Builds a `resource:<module>:<suffix>` key.

## Examples

    iex> Backpex.Preferences.Key.resource_key(Backpex.Preferences, "metrics_visible")
    "resource:Backpex.Preferences:metrics_visible"

# `validate`

Validates a preference key.

Returns `:ok` when the key is non-empty, parseable, and its first segment
is one of the built-in prefixes (`"global"`, `"resource"`, `"custom"`).
Otherwise returns one of:

  * `{:error, :empty}` — the key is `""`.
  * `{:error, :malformed}` — the key parses to nothing usable (e.g. the
    first segment is empty, like `":foo"` or `".foo"`).
  * `{:error, :unknown_prefix}` — the first segment is not built-in.

Only the first segment is checked: this is a routing/trust check, not a
spell-checker for the segments after it.

Pure: never logs, never raises. Used by
`Backpex.Preferences.Context.put_client/2` to drop browser-supplied keys
before they reach a read.

## Examples

    iex> Backpex.Preferences.Key.validate("global.theme")
    :ok

    iex> Backpex.Preferences.Key.validate("resource:MyApp.UserLive:columns")
    :ok

    iex> Backpex.Preferences.Key.validate("custom.dashboard.view_mode")
    :ok

    iex> Backpex.Preferences.Key.validate("globl.theme")
    {:error, :unknown_prefix}

    iex> Backpex.Preferences.Key.validate("")
    {:error, :empty}

# `wildcard_prefix`

Returns the segments a wildcard pattern covers, or `nil` when `pattern` is
not a wildcard.

A wildcard is a pattern whose **final segment** is `"*"`, with at least one
leading segment. The pattern is segmented with `parse/1` — the same rule
keys are segmented with — so a pattern and the keys it is meant to cover
always agree on where the segment boundaries are. This is what lets a
colon-form pattern address a colon-form key: `"resource:MyApp.MyLive:*"`
covers `["resource", "MyApp.MyLive"]`, keeping the module a single segment
instead of splitting it on its dots.

## Examples

    iex> Backpex.Preferences.Key.wildcard_prefix("global.*")
    ["global"]

    iex> Backpex.Preferences.Key.wildcard_prefix("resource:MyApp.MyLive:*")
    ["resource", "MyApp.MyLive"]

    iex> Backpex.Preferences.Key.wildcard_prefix("global.theme")
    nil

    iex> Backpex.Preferences.Key.wildcard_prefix("*")
    nil

---

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