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
Types
A schema's materializer (DXN.md §4.8): called for every decoded struct
of its schema, whatever the version, with the schema's name, the
version the fields were read with and the fields keyed by string. It
answers the application's latest-version value, which is checked
against the latest schema, or {:error, reason}.
A lazy source of schemas, asked on a miss for a name: every version it
finds -- one compiled schema, or a list of them, all loaded -- or
:unknown, or why the schemas it found cannot be used (two files with
one version, a file that is not the version its name says, ...).
Functions
Loads compiled as a new version of the schema name.
struct as it is written here: carrying the latest loaded version of
its schema (DXN.md §4.8), whatever version it says itself. A struct
whose name has no schema keeps its own version, or none.
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.
Looks up version version of the schema name -- the one a struct
written %Name(3){...} is read with. Unlike fetch_struct_schema/2, a
miss does not fall back to another version: a struct is only ever read
with the schema it was written against.
The registry a decode's opts describe: :registry (a new one when
absent), with :trusted and :strict applied over it when given --
what Dextrin.decode/2, Dextrin.decode_binary/2 and
Dextrin.Document.parse/2 all read.
Marks this registry as decoding strictly: a schema-backed struct
that carries a field its schema does not declare is refused
(schema/undeclared-field, DXN.md §4.7) instead of keeping it as an
extra field. A config loader decodes strictly, to catch a typo in a
hand-written file; a message consumer does not, so a newer producer's
fields pass through. Dextrin.decode/2/decode_binary/2 take it as
strict: true.
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.
Sets compiled as version compiled.version of the schema name,
whether or not that version was there already.
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, vs.
an untrusted one -- the default. Currently the one thing this
affects is keyword: untrusted (the default) decodes it as
Dextrin.Keyword.t(), so a String.to_atom/1 call is never reachable
from the input (which could otherwise exhaust the atom table); trusted
decodes it as a real Elixir atom (DXN.md §1.3's own type table:
keyword's Elixir type is "Elixir atom"). Pass trusted: true to
Dextrin.decode/2/decode_binary/2 -- or put_trusted(registry, true) on a reused registry -- only for a source you fully control.
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).
Replaces version compiled.version of the schema name -- the hot swap
of a running system.
Types
@type struct_materializer() :: (String.t(), pos_integer(), %{required(String.t()) => term()} -> {:ok, term()} | {:error, term()})
A schema's materializer (DXN.md §4.8): called for every decoded struct
of its schema, whatever the version, with the schema's name, the
version the fields were read with and the fields keyed by string. It
answers the application's latest-version value, which is checked
against the latest schema, or {:error, reason}.
@type struct_resolver() :: (String.t() -> {:ok, Dextrin.Schema.Compiled.t() | [Dextrin.Schema.Compiled.t()]} | :unknown | {:error, Dextrin.Error.t()})
A lazy source of schemas, asked on a miss for a name: every version it
finds -- one compiled schema, or a list of them, all loaded -- or
:unknown, or why the schemas it found cannot be used (two files with
one version, a file that is not the version its name says, ...).
@type t() :: %Dextrin.Registry{ materializers: %{optional(String.t()) => struct_materializer()}, resolver: struct_resolver() | nil, strict: boolean(), struct_modules: %{optional(String.t()) => module()}, structs: %{ optional(String.t()) => %{ optional(pos_integer()) => 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 add_struct_schema(t(), String.t(), Dextrin.Schema.Compiled.t()) :: {:ok, t()} | {:error, Dextrin.Error.t()}
Loads compiled as a new version of the schema name.
Loading a version that is already loaded is a schema/duplicate-version
error (DXN.md §4.8): two documents both claiming to be User v3 is a
mistake, not an update. Replacing a version on purpose -- a hot swap in
a running system -- is replace_struct_schema/3.
@spec as_written(t() | nil, Dextrin.Struct.t()) :: Dextrin.Struct.t()
struct as it is written here: carrying the latest loaded version of
its schema (DXN.md §4.8), whatever version it says itself. A struct
whose name has no schema keeps its own version, or none.
Every writer goes through this -- text, the formatter and binary -- so encoding never writes a version other than the latest, and never writes one no schema stands behind.
@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()} | {:error, Dextrin.Error.t(), 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_struct_schema(t(), String.t(), pos_integer()) :: {:ok, Dextrin.Schema.Compiled.t(), t()} | {:unknown, t()} | {:error, Dextrin.Error.t(), t()}
Looks up version version of the schema name -- the one a struct
written %Name(3){...} is read with. Unlike fetch_struct_schema/2, a
miss does not fall back to another version: a struct is only ever read
with the schema it was written against.
@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
The registry a decode's opts describe: :registry (a new one when
absent), with :trusted and :strict applied over it when given --
what Dextrin.decode/2, Dextrin.decode_binary/2 and
Dextrin.Document.parse/2 all read.
@spec new() :: t()
@spec put_resolver(t(), struct_resolver()) :: t()
Marks this registry as decoding strictly: a schema-backed struct
that carries a field its schema does not declare is refused
(schema/undeclared-field, DXN.md §4.7) instead of keeping it as an
extra field. A config loader decodes strictly, to catch a typo in a
hand-written file; a message consumer does not, so a newer producer's
fields pass through. Dextrin.decode/2/decode_binary/2 take it as
strict: true.
@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()
Sets compiled as version compiled.version of the schema name,
whether or not that version was there already.
Not how schemas are loaded. Dextrin.Schema.compile/2 uses
add_struct_schema/3, which refuses a version already loaded, and a
deliberate swap is replace_struct_schema/3. This is the unchecked
setter underneath both, also used to cache what a resolver answers.
@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, vs.
an untrusted one -- the default. Currently the one thing this
affects is keyword: untrusted (the default) decodes it as
Dextrin.Keyword.t(), so a String.to_atom/1 call is never reachable
from the input (which could otherwise exhaust the atom table); trusted
decodes it as a real Elixir atom (DXN.md §1.3's own type table:
keyword's Elixir type is "Elixir atom"). Pass trusted: true to
Dextrin.decode/2/decode_binary/2 -- or put_trusted(registry, true) on a reused registry -- only for a source you fully control.
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).
@spec replace_struct_schema(t(), String.t(), Dextrin.Schema.Compiled.t()) :: {:ok, t()} | {:error, Dextrin.Error.t()}
Replaces version compiled.version of the schema name -- the hot swap
of a running system.
Replace only replaces. Naming a version that is not loaded is a
schema/not-loaded error, so a typo in a hot swap never quietly adds a
version instead.