Extension point for both struct and custom-tag decoding/encoding.
No entry for a given name → decode falls back to an opaque
Dextrin.Struct/Dextrin.CustomTag — never a hard failure, since
neither type requires a registration to be representable at all.
Struct entries only ever come from a compiled .dxns schema
(Dextrin.Schema.compile/3) plus an optional materializer layered on
top (put_struct_materializer/3) — there is no put_struct/3 that
hand-writes a struct's shape directly, because struct is
schema-dependent by design: the schema is what supplies field
names/order/types, a materializer only decides what nicer decoded
shape to produce from an already-schema-validated field map.
Every put_*/fetch_* pair here is a plain, immutable map update —
Registry.t() is ordinary data threaded explicitly through
Dextrin.decode/2, Dextrin.Schema.compile/3, etc.; nothing about
it is a process, an ETS table, or otherwise global/mutable state.
Summary
Functions
The reverse of fetch_struct_module/2: given an application
struct's own module, finds the schema name it was registered under
(if any) — what lets automatic encode-time validation
(Dextrin.Schema.validate_encode_tree/2) recognize a plain Elixir
struct as "this is a Foo" without being told so explicitly, the same
way a Dextrin.Struct's own name already identifies it.
Looks up a compiled schema by struct name, consulting the lazy
resolver (if any) on a miss and caching the result. Whether schemas
are all compiled up front (Dextrin.Schema.compile/3 once, at
startup) or resolved on demand (a resolver reading a file, calling a
schema service, whatever the caller wants) is entirely the caller's
choice — this function is what makes both paths look identical to
the rest of Dextrin, which never needs to know which one is in play.
Declares which Elixir module a schema's data is expected to be —
used only for {:reference, name} checks during encode-time
validation (Dextrin.Schema.validate_encode/3). Decode-time
reference checks don't need this: they carry an
internal provenance marker regardless of materializer shape
(Dextrin.Schema.Validated). Encode-time has no wire data to carry
that marker before anything's even been encoded — __struct__ is
the one signal actually available pre-encode, and this is what a
reference check compares it against. Independent of
put_struct_materializer/3: you can declare a module here even if
you use the default plain-map materialization for something else.
The reverse of put_tag/3: registers how to turn an application
struct (keyed by its module) back into @name value on encode.
put_tag/3 alone lets you decode @my-app/money 100 into
%MyApp.Money{}, but never re-encode it, since dispatch on the way
out has to happen by Elixir module, not by wire-format tag name — the
two directions need separate lookup tables, not one shared one.
Marks (or unmarks) this registry as decoding a trusted source —
the default — vs. an untrusted one. Currently the one thing this
affects is keyword: trusted (the default) decodes it as a real
Elixir atom (DXN.md §1.3's own type table: keyword's Elixir type
is "Elixir atom"); untrusted decodes it as Dextrin.Keyword.t()
instead, so a String.to_atom/1 call is never reachable from
attacker-controlled text (which could otherwise exhaust the atom
table). Pass trusted: false to Dextrin.decode/2/decode_binary/2
— or put_trusted(registry, false) on a reused registry — for any
source you don't fully control; the default assumes you do.
Registers a named type_expr — a reusable name for a combination of
the fixed type_expr vocabulary (Dextrin.Schema.TypeExpr), defined
entirely in .dxns data, never Elixir code. Dextrin.Schema.compile/3
populates this automatically from a document's non-%schema{}
entries; put_type_alias/3 itself is only needed to seed a base
registry by hand (e.g. sharing one vocabulary across several
compile/3 calls without redeclaring it each time).
Types
@type struct_resolver() :: (String.t() -> {:ok, Dextrin.Schema.Compiled.t()} | :unknown)
@type t() :: %Dextrin.Registry{ materializers: %{optional(String.t()) => struct_materializer()}, resolver: struct_resolver() | nil, struct_modules: %{optional(String.t()) => module()}, structs: %{optional(String.t()) => Dextrin.Schema.Compiled.t()}, tag_encoders: %{optional(module()) => {String.t(), tag_encoder()}}, tags: %{optional(String.t()) => tag_decoder()}, trusted: boolean(), type_aliases: %{optional(String.t()) => Dextrin.Schema.TypeExpr.t()} }
@type tag_decoder() :: (Dextrin.Value.t() -> {:ok, term()} | {:error, term()})
@type tag_encoder() :: (struct() -> {:ok, Dextrin.Value.t()} | {:error, term()})
Functions
@spec fetch_materializer(t(), String.t()) :: {:ok, struct_materializer()} | :error
The reverse of fetch_struct_module/2: given an application
struct's own module, finds the schema name it was registered under
(if any) — what lets automatic encode-time validation
(Dextrin.Schema.validate_encode_tree/2) recognize a plain Elixir
struct as "this is a Foo" without being told so explicitly, the same
way a Dextrin.Struct's own name already identifies it.
@spec fetch_struct_schema(t(), String.t()) :: {:ok, Dextrin.Schema.Compiled.t(), t()} | {:unknown, t()}
Looks up a compiled schema by struct name, consulting the lazy
resolver (if any) on a miss and caching the result. Whether schemas
are all compiled up front (Dextrin.Schema.compile/3 once, at
startup) or resolved on demand (a resolver reading a file, calling a
schema service, whatever the caller wants) is entirely the caller's
choice — this function is what makes both paths look identical to
the rest of Dextrin, which never needs to know which one is in play.
@spec fetch_tag(t(), String.t()) :: {:ok, tag_decoder()} | :error
@spec fetch_tag_encoder(t(), module()) :: {:ok, {String.t(), tag_encoder()}} | :error
@spec fetch_type_alias(t(), String.t()) :: {:ok, Dextrin.Schema.TypeExpr.t()} | :error
@spec new() :: t()
@spec put_resolver(t(), struct_resolver()) :: t()
@spec put_struct_materializer(t(), String.t(), struct_materializer()) :: t()
Declares which Elixir module a schema's data is expected to be —
used only for {:reference, name} checks during encode-time
validation (Dextrin.Schema.validate_encode/3). Decode-time
reference checks don't need this: they carry an
internal provenance marker regardless of materializer shape
(Dextrin.Schema.Validated). Encode-time has no wire data to carry
that marker before anything's even been encoded — __struct__ is
the one signal actually available pre-encode, and this is what a
reference check compares it against. Independent of
put_struct_materializer/3: you can declare a module here even if
you use the default plain-map materialization for something else.
@spec put_struct_schema(t(), String.t(), Dextrin.Schema.Compiled.t()) :: t()
@spec put_tag(t(), String.t(), tag_decoder()) :: t()
@spec put_tag_encoder(t(), module(), String.t(), tag_encoder()) :: t()
The reverse of put_tag/3: registers how to turn an application
struct (keyed by its module) back into @name value on encode.
put_tag/3 alone lets you decode @my-app/money 100 into
%MyApp.Money{}, but never re-encode it, since dispatch on the way
out has to happen by Elixir module, not by wire-format tag name — the
two directions need separate lookup tables, not one shared one.
Marks (or unmarks) this registry as decoding a trusted source —
the default — vs. an untrusted one. Currently the one thing this
affects is keyword: trusted (the default) decodes it as a real
Elixir atom (DXN.md §1.3's own type table: keyword's Elixir type
is "Elixir atom"); untrusted decodes it as Dextrin.Keyword.t()
instead, so a String.to_atom/1 call is never reachable from
attacker-controlled text (which could otherwise exhaust the atom
table). Pass trusted: false to Dextrin.decode/2/decode_binary/2
— or put_trusted(registry, false) on a reused registry — for any
source you don't fully control; the default assumes you do.
symbol is deliberately unaffected either way — it stays
Dextrin.Symbol.t() regardless of trusted, mirroring encode/2's
own choice to accept a bare atom as a stand-in for keyword, never
for symbol.
@spec put_type_alias(t(), String.t(), Dextrin.Schema.TypeExpr.t()) :: t()
Registers a named type_expr — a reusable name for a combination of
the fixed type_expr vocabulary (Dextrin.Schema.TypeExpr), defined
entirely in .dxns data, never Elixir code. Dextrin.Schema.compile/3
populates this automatically from a document's non-%schema{}
entries; put_type_alias/3 itself is only needed to seed a base
registry by hand (e.g. sharing one vocabulary across several
compile/3 calls without redeclaring it each time).