# `UXID.Registry`
[🔗](https://github.com/riddler/uxid-ex/blob/v2.9.1/lib/uxid/registry.ex#L1)

A compile-time registry of an application's UXID prefixes.

Prefixes only deliver their value - "the ID names its resource on sight" - if
they are globally unique and well-formed across an app. `use UXID.Registry`
moves that governance out of hand-rolled CI tests and into the compiler, and
turns the same declarations into a runtime routing table (prefix → schema)
that powers authorization checks, admin auto-linking, and Relay-style global
object identification.

## Declaring a registry

    defmodule MyApp.IDs do
      use UXID.Registry,
        prefix_format: ~r/^[a-z][a-z0-9_]{1,7}$/,
        delimiter: "_",
        default_size: :medium,
        default_validate: true

      defid :org,     prefix: "org",    schema: MyApp.Org,         category: :account
      defid :contact, prefix: "contact", size: :large, schema: MyApp.CRM.Contact, allow_uuid: true
      defid :lead,    prefix: "lead",   schema: MyApp.CRM.Lead
      defid :event,   prefix: "evt",    size: :small, monotonic: true
      retired "usr" # reserve a prefix so it counts for uniqueness, never reused
    end

## Compile-time guarantees

  * every `:prefix` is checked against `:prefix_format` - a malformed prefix is
    a compile error;
  * all prefixes (active *and* `retired`) are checked for uniqueness - a
    duplicate is a compile error. This replaces the per-app runtime uniqueness
    test.

## Generated API

By key (minting and schema configuration):

    MyApp.IDs.generate!(:org)   # => "org_01h..."
    MyApp.IDs.generate!(:export, from: phone)  # deterministic - see generate!/2
    MyApp.IDs.prefix(:org)      # => "org"
    MyApp.IDs.size(:org)        # => :medium
    MyApp.IDs.schema(:org)      # => MyApp.Org
    MyApp.IDs.monotonic(:event) # => true
    MyApp.IDs.field_opts(:org)  # => [prefix: "org", size: :medium, validate: true, allow_uuid: true, delimiter: "_"]
    MyApp.IDs.all()             # => [%{key: :org, prefix: "org", ...}, ...]

Cross-source single source of truth - emit a JSON manifest so a database
function or a mobile/JS generator mints prefixes the same way the app does:

    MyApp.IDs.manifest()        # => [%{"key" => "org", "prefix" => "org", "size" => "medium", ...}]
    MyApp.IDs.manifest_json()   # => ~s([{"key":"org",...,"deterministic":false,"monotonic":null,...},...])

The manifest's `deterministic` flag tells a non-Elixir generator *which scheme*
a key uses, not how to implement it - see `guides/deterministic.md` for the
hash rule such a generator must reproduce.

`field_opts/1` is the single-source-of-truth hook - a schema spreads it instead
of restating anything:

    @primary_key {:id, UXID, [autogenerate: true] ++ MyApp.IDs.field_opts(:org)}

By ID string (runtime routing - see "Parsing" below):

    MyApp.IDs.known?("org_01h...")      # => true   (cheap prefix-only membership)
    MyApp.IDs.key_for("org_01h...")     # => :org
    MyApp.IDs.schema_for("org_01h...")  # => MyApp.Org
    MyApp.IDs.resolve("org_01h...")     # => %{key: :org, schema: MyApp.Org, ...}

## Parsing

Lookups split an ID into `{prefix, body}` on the **last** delimiter. This is
unambiguous without consulting the registry, because a UXID body is Crockford
Base32 and therefore never contains the delimiter: in `in_ref_01h...` every `_`
belongs to the prefix except the final joining one. The registry is consulted
only *after* the split, to answer membership/type - an unregistered but
well-formed string like `nope_01h...` parses fine yet resolves to `nil`.

Because of that, the `:delimiter` must be a character that cannot appear in a
Base32 body (`"_"` or `"-"`); a letter or digit is rejected at compile time.
An underscore is preferred for compound prefixes since it does not break
double-click-to-select-the-whole-id.

## Routing in a layered app (self-registration)

A `schema:` literal points the registry *up* at a schema module. In a flat app
that is fine, and `schema_for/1` resolves it with no further setup. In a
**layered** app the registry usually lives at the base layer (so every layer
can depend down on it to mint IDs and read `field_opts/1`), while the schemas
it routes to live above it - so naming them inverts the dependency direction.

To keep the direction correct, omit `schema:` and let each schema register
itself under its key with `UXID.Registered`. The reference then points *down*
(schema → registry key):

    defmodule MyApp.CRM.Contact do
      use Ecto.Schema
      use UXID.Registered, key: :contact
      @primary_key {:id, UXID, [autogenerate: true] ++ MyApp.IDs.field_opts(:contact)}
    end

    defmodule MyApp.IDs do
      use UXID.Registry
      defid :contact, prefix: "contact", route: true  # filled at boot, no schema literal
    end

At boot, `verify!/1` scans the given OTP apps for the marker, assembles the
prefix → schema routing table into `:persistent_term`, and validates it -
raising if a marker names an unregistered key, two modules claim one key, or a
`route: true` key resolves to no schema. Wire it into your top app's `start/2`
so every boot (prod, dev, and CI's `mix test`) re-verifies:

    def start(_type, _args) do
      MyApp.IDs.verify!(otp_apps: [:my_app])
      # ...
    end

Because the registry discovers schemas by runtime reflection rather than a
compile-visible module reference, no base-layer code ever names an upper-layer
module, so `Boundary`/`xref` see nothing pointing the wrong way. `known?/1` and
`key_for/1` need no schema at all and work regardless.

# `default_prefix_format`

The built-in default `:prefix_format` (permits an internal underscore).

# `defid`
*macro* 

Registers an id under `key`. Requires `:prefix`; `:size`, `:schema`,
`:category`, `:validate`, `:allow_uuid`, and `:route` are optional and fall
back to the registry defaults.

Three options describe how the *body* is generated. They belong on the key for
the same reason `:size` does - they change the ID's shape, so every call site
and every schema field must agree on them:

  * `:monotonic` - `true`, `false`, or a list of sizes. Opts the key into (or
    out of) monotonic generation without consulting the global policy. See
    `guides/monotonic.md`.
  * `:compact_time` - `true`/`false`. Spends 40 rather than 48 bits on the
    timestamp, moving the freed byte into the random field.
  * `:rand_size` - an explicit random-byte count, overriding the width implied
    by `:size`.

Left unset (the default), each falls through to the global application
configuration exactly as `UXID.generate!/1` does - so declaring nothing keeps
today's behavior. Set them and they flow into both `generate!/2` and
`field_opts/1`, so an Ecto `autogenerate: true` field mints the same shape as
an explicit call.

Two further options carry no shape information:

  * `:deterministic` - when `true`, the key is derived by contract and
    `generate!/2` raises unless given `from:`. See `guides/deterministic.md`.
  * `:legacy` - opaque app metadata (any term), surfaced on `all/0` so a
    migration backlog lives in the registry rather than a moduledoc.

An unrecognized option is a compile error.

# `retired`
*macro* 

Reserves a prefix so it participates in the uniqueness check but is never
generated - mirroring "identifiers are never reused". Accepts a string or atom.

# `split_last`

```elixir
@spec split_last(String.t(), String.t()) :: {String.t() | nil, String.t()}
```

Splits a UXID string into `{prefix, body}` on the last occurrence of
`delimiter`. Returns `{nil, string}` when the delimiter is absent.

    iex> UXID.Registry.split_last("in_ref_01h2x...", "_")
    {"in_ref", "01h2x..."}

    iex> UXID.Registry.split_last("01h2x...", "_")
    {nil, "01h2x..."}

---

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