Modules
Public API for DXN (Data eXchange Notation) — .dxn text and
.dxnb binary, per DXN.md (the normative format specification).
DXN array (@array[ ... ]) — fixed-size, indexed. Wraps an
Elixir tuple, the one DXN collection type where Elixir's own
fixed-arity tuple is actually the right fit — unlike Dextrin.Tuple,
which backs DXN's arbitrary-length tuple with a list instead.
.dxnb decoder — the mirror image of Dextrin.Binary.Encoder.
Envelope (magic version cbor_item) checked once at the top;
everything below that is a single recursive decode_item/2, keyed
on CBOR major type first, then on tag — matching DXN.md §2.2's
table row order the same way the encoder does.
.dxnb encoder — a recursive walk over CBOR's major types plus a
fixed tag dispatch table, matching DXN.md §2.2's table row order.
Pure constants — CBOR tag numbers and bit-layout tables, mirroring
DXN.md §2.3 verbatim. No behavior lives here.
DXN bytes (@bytes "...", base64 in text, raw binary in .dxnb).
Wraps a plain Elixir binary() — needed because string is also
a plain Elixir binary() (String.t() is not a distinct runtime
type), so a bare binary can't otherwise tell "UTF-8 text" and "raw
bytes that happen to be valid UTF-8" apart on re-encode. The same
kind of ambiguity Dextrin.Char/Dextrin.Symbol/Dextrin.Keyword
each wrap a bare value to avoid.
DXN char (?c) — a single Unicode codepoint. Kept distinct from a
1-grapheme String.t() so char and string (?a vs "a", two
distinct DXN types) never collapse to the same Elixir value and
silently fail to round-trip.
Opaque DXN custom tag (@tag value), produced when no decoder is
registered for its name (Dextrin.Registry.put_tag/3) — the open
extension point DXN.md §1.2 reserves for any tag name that isn't one
of the built-ins (@uuid, @duration, ...). Prefer a schema-backed
struct over a custom tag for anything with real field structure;
reserve custom tags for simple scalar-wrapping.
DXN duration (@duration "P..."). Each field is
integer() | nil — only fields actually present in the source set
a bit in .dxnb's bitmask (DXN.md §2.3). years/months are
calendar-relative (variable length) and not reducible to a single
elapsed-time scalar, so all seven fields are tracked independently
rather than folded into one (DXN.md §1.4).
One error struct for both the text and binary pipelines, so a caller
never has to special-case which one produced it. Wraps Ichor.Error
verbatim for the text side (including its caret-annotated
context_lines, useful for CLI/log output); the binary side has no
source text to annotate, so it only ever carries a message, a byte
offset, and a stage.
DXN keyword (:name in value position). Wraps a plain
String.t(), never an Elixir atom, for the same reason as
Dextrin.Symbol: atoms are never garbage collected on the BEAM, and
decoding untrusted, attacker-controlled data must never be able to
exhaust the atom table.
DXN ordered-map (@ordered %{ ... }) — order is part of the
value's identity, unlike plain map. Wraps an ordered list of
{key, value} pairs rather than a plain Elixir map, so insertion
order survives structural == comparison.
DXN rational (int/uint) — an exact ratio, stored exactly as
given and never silently reduced: 22/7 and 44/14 are distinct DXN
values (DXN.md calls it "exact ratio," not "exact reduced ratio"),
so reducing on parse would be a silent, opinionated transformation.
reduce/1 is offered but never called implicitly.
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.
Public entry point for .dxns schema compilation and validation.
The result of compiling one %schema{} entry from a .dxns
document (Dextrin.Schema.Compiler.compile/3) — enough to both
validate a decoded value and convert it both directions between
.dxnb's always-positional wire shape and a named field map.
Compiles a parsed .dxns document (an ordinary decoded DXN value —
a .dxns file is valid .dxn, no new grammar; what makes it a
schema document is purely the shape of the value it parses to: a
map from name to type expression) into Dextrin.Schema.Compiled
entries.
One compiled field spec inside a Dextrin.Schema.Compiled struct.
required comes from the ?-suffixed key convention (a field key
ending in ? is optional; no separate flag exists in .dxns itself
— reusing DXN's own identifier grammar rather than adding one);
default/description only ever come from the %field{...} escape
hatch, since optionality is already fully covered by the key suffix.
An optional convenience struct_resolver resolving Namespace/Name
references to .dxns files on disk.
Lets a struct's own library ship a DXN schema for it — field
names, types, required/optional, closed/forbidden, refinements, a
materializer — without that library ever taking a hard dependency on
dextrin. This is the struct (schema-backed) counterpart to what
Dextrin.Registry.put_tag_encoder/4 already does for simple
scalar-wrapping custom-tag values: an extension point owned by the
consuming application, not forced onto every struct-defining
library it might want to use with dextrin.
dextrin's standard library of named types (priv/schema/std.dxns)
— common refinements like PositiveInteger or NonEmptyString, so
a schema author doesn't redefine them by hand. Built the same way
any consumer's own named types would be — nothing about them is
special-cased in Dextrin.Schema.Compiler. Deliberately excludes
anything domain-specific (email, phone number, URL-shaped string):
what counts as a valid one is an application decision this library
shouldn't guess at.
Opt-in: pass Std.registry/1's result as compile/3's
base_registry to make these names available to your own document.
Internal, compiled representation of a .dxns type expression — the
13-form vocabulary (any, primitive, reference, list-of,
set-of, tuple-of, map-of, enum, one-of, all-of,
nilable, refine, struct) Dextrin.Schema.Compiler turns a
parsed .dxns value into, and what matches?/2 below checks a
decoded value against. This vocabulary is fixed, not extensible from
outside an actual library change — what schema authors extend
instead is composing these forms into a reusable named type (see
Dextrin.Schema.Compiler's own moduledoc), which needs no new form
at all.
Internal only — never appears in a value handed back to
Dextrin.decode/2/decode_binary/2's caller.
Checks a value's fields against a Dextrin.Schema.Compiled schema
(required/closed/forbidden/refine) — shared by decode (materialize/4,
which also produces a materialized result) and encode-time validation
(validate_for_encode/3/validate_tree_for_encode/2, which never
transforms value). Both directions reuse the exact same
resolve_fields/3/TypeExpr.matches?/3 field-checking — encode's
own values are wrapped in the same internal Dextrin.Schema. Validated marker decode already uses, via wrap_and_check/2, so
there's one type-checking implementation, not two that could drift.
DXN sorted-set (@sorted-set @{ ... }). Wraps a list kept sorted
(and deduplicated) as a hard invariant at every construction site —
new/1 is the only way to build one, precisely so that structural
== between two Dextrin.SortedSets is valid set-equality rather
than something that happens to work only when both were built the
same way — every construction site (decode, encode, public API)
goes through new/1, so the sorted/deduplicated invariant can never
be bypassed.
Opaque DXN struct value, produced when no schema is compiled for
its name (DXN.md §1.4: "a reader lacking the schema... returns an
opaque tagged value rather than failing").
DXN symbol — a bare, unevaluated identifier reference. Wraps a
plain String.t(), never an Elixir atom: atoms are never garbage
collected on the BEAM, and a decoder fed adversarial or merely large
third-party input must not be able to exhaust the atom table by
decoding enough distinct symbols. A caller who trusts their input
and wants a real atom can always convert explicitly.
Ichor.Actions implementation for Dextrin.Text.Grammar — turns
the raw capture tree Grammar.Native produces while matching
priv/grammar/dxn.aether into actual Dextrin.Value.t() values.
context is a Dextrin.Registry.t() — text parsing only ever reads
it (to resolve struct schemas and custom tags), never mutates it.
Shared escape-decoding for DXN's string/char/quoted-keyword
bodies (DXN.md §1.1's escape production) — one implementation so
none of the token handlers in Dextrin.Text.Actions duplicate it.
Multi-line, indented .dxn rendering — Dextrin.encode/2's
pretty: true opt, mix dextrin.format's --mode pretty, and mix dextrin.decode's default output (a CLI decode is a human reading
the result, so encode/2's own single-line default isn't the right
default there). --mode condense needs no separate implementation
at all: it's exactly what Dextrin.encode/2/Dextrin.Text.Printer
already produce with pretty: false (the default).
Compiles priv/grammar/dxn.aether via Ichor's Grammar.Native
backend, not Grammar.VM's interpreted backend — the grammar is
fixed at dextrin's own build time (there's no scenario where a
caller supplies a different grammar at runtime), so there's no
reason to pay for bytecode interpretation when a compiled parser is
available for free.
.dxn printer — the reverse of Dextrin.Text.Actions. Single-line,
minimal-whitespace output: a printer, not a formatter (no
line-wrapping or indentation policy is specified anywhere in
DXN.md) — Dextrin.Text.Formatter.pretty/2 owns multi-line,
human-readable rendering, built on top of this module.
DXN tuple ({ ... }) — ordered, heterogeneous, arbitrary length.
Wraps a list, not an Elixir tuple: DXN tuples have no fixed arity in
the type system the way Elixir's do, and Dextrin.Array already
claims the Elixir tuple for the one DXN type that actually is
fixed-size/indexed.
Pure text-processing core for turning a Unicode Character Database
DerivedCoreProperties.txt into the generated IDENT_START/
IDENT_CONT/IDENTIFIER block spliced into priv/grammar/dxn.aether.
No file or network I/O lives here — that's
Mix.Tasks.Dextrin.Gen.Unicode's job — so this module can be tested
directly against small in-memory fixtures instead of the real,
~1MB UCD file.
DXN uri (@uri "...", RFC 3986). Wraps the raw string exactly as
given, not parsed into URI.t() — URI.parse/1 is lenient in ways
RFC 3986 isn't, and URI.to_string/1 doesn't always reproduce the
original text byte-for-byte (e.g. component re-escaping), which would
risk breaking round-trip. Parsing is left to the caller, who can
always call URI.parse/1 on value themselves.
The union of every shape a decoded DXN value can take. A typespec aid only — no functions, no runtime behavior.
Mix Tasks
Fetches the latest Unicode Character Database DerivedCoreProperties.txt,
compares its version against the one last processed
(priv/unicode/VERSION), and — if newer — regenerates the
IDENT_START/IDENT_CONT/IDENTIFIER ranges spliced into
priv/grammar/dxn.aether (see Dextrin.Unicode.RangeGenerator for
the pure text-processing logic this task wraps with file/network I/O).
Decodes PATH (format sniffed from its extension, or forced with
--format text|binary), reporting success or a rendered
Dextrin.Error — non-zero exit on failure, so this is meant for CI
as much as interactive use.