API Reference Dextrin v#0.3.3

Copy Markdown View Source

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.

bits (DXN.md §1.4): a run of bits that is not whole bytes, written as its bytes and their bit count -- @bits {"oA==" 3} in text, T_BITS around [bytes, count] in .dxnb.

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.

The canonical form of a value (DXN.md §1.5): one .dxn rendering, fixed down to the byte, so that values from different implementations can be compared as text.

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.

This implementation's answers to the Dextrin Conformance Suite.

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.

A .dxn document with everything it holds: its value, and every comment, blank line and comma around it (design/comment-retention.md).

A document's syntax tree laid out the way Dextrin.Text.Formatter lays out a value -- the top-level collection over lines, one entry per line, indented; a nested one on one line when that line fits the width (:width, 80) -- with every comment kept beside the entry it belongs to (design/comment-retention.md, "The rules"). A nested collection holding a comment, or a blank line between its entries, is laid out over lines whatever its width: the comment needs its line, and the blank line is the author's grouping.

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). Seconds and their fraction are one field, nanoseconds (Jan, 2026-10-20).

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.

The three forms' names on the wire and on disk, and a cheap test of whether a body is worth decoding as one -- DXN.md §6, so every implementation answers the same.

DXN ip (@ip "...", Jan, 2026-10-20): an IPv4 or IPv6 address, or a prefix -- an address and a length. address holds the raw bytes, 4 or 16; prefix is the length, or nil for an address. A prefix's host bits are zero (RFC 9164). In .dxnb it is CBOR's tag 52 or 54, raw bytes.

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.

Which values may be map keys (DXN.md §1.6): a string, keyword, integer, uuid, objectid, date, bytes, symbol or char. Anything else is refused as key/not-allowed -- in text, in binary and when encoding.

A time, datetime or local-datetime finer than a microsecond (DXN.md §1.3, Jan, 2026-10-20: dates and times carry nanoseconds). Elixir's own Time, DateTime and NaiveDateTime stop at the microsecond, so a value that needs more is held as the native value to the microsecond and the nanoseconds past it, 1 to 999. A value that fits a microsecond is the native one, as before.

DXN objectid (@objectid "...", Jan, 2026-10-20): BSON's 12-byte ObjectId, built in like uuid -- readable in text, its raw bytes in .dxnb. The value holds the bytes; the 24 hex digits exist only at the text boundary.

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 version discipline check (DXNS.md §8): compares the schemas of a previous release with the current ones.

One version of one schema, compiled from a %schema(N){...} entry of a .dxns document (Dextrin.Schema.Compiler.compile/2) -- 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.

HTML documentation for a directory of .dxns schema documents: a small static site -- an index of the namespaces, and a page for each, every schema, shape and named type on it anchored, every reference a link, every version of a schema shown (Jan, 2026-10-20).

Which comment belongs to which definition, in a .dxns document's lossless tree (Jan, 2026-10-20)

The pages of Dextrin.Schema.Docs: index.html at the top -- a section per root namespace, then the names with no namespace -- an index.html in a directory per namespace, and the stylesheet they share, style.css. Everything is plain HTML and CSS, so the site opens from disk; one small script only switches light and dark.

The Markdown a schema's comments are written in, as HTML -- a small, fixed subset, so that every implementation renders a comment the same

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.

A struct_resolver finding a struct's schema in .dxns files on disk, by the convention DXNS.md §6 states for every implementation -- so one set of schema files serves them all. Swappable like any resolver (Dextrin.Registry.put_resolver/2): anyone with another convention writes their own.

The formats a refine's format: names (DXNS.md §2a, Jan, 2026-10-20): a closed set, each checked the same way in every implementation. A format DXN has a tag for is read by that tag's reader; every other one by a grammar written as a regular expression that PCRE, JavaScript (with the u flag) and PHP all read alike.

The names a .dxns file's entries have, from where the file lies under its root (DXNS.md §6) -- one rule for every reader of a tree: the docs generator, Dextrin.Schema.Tree and the file resolver.

Lets a struct's own library ship a DXN schema for it — field names, types, required/optional, 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.

Which schema a decoded struct is read with, given the version it was written with and the versions loaded (DXNS.md §8). Shared by the text and binary decoders, so both answer every case the same way.

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.

A tree of .dxns files read as one (Jan, 2026-10-20): every entry named from where its file lies (DXNS.md §6, Dextrin.Schema.Layout), the files compiled in the order their references need -- so a reference to another file's named type resolves whatever order the files sort in -- and every problem reported with its file and line

Internal, compiled representation of a .dxns type expression — the vocabulary of DXNS.md §1 (any, never, primitive, reference, list-of, list-items, set-of, tuple-of, tuple-of-all, map-of, array-of, tagged, doc, if, ordered-map-of, sorted-set-of, enum, one-of, any-of, all-of, not, nilable, refine, struct, and shape and case, design/shapes-and-case.md) 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 fields and declared types) — shared by decode (resolve/3, whose result Dextrin.Schema.Reader builds a value from) 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.

DXN's dates and times as text and as numbers (DXN.md §1.2, §1.5, §2.2; Jan, 2026-10-20): one place that reads and writes the ISO 8601 strings under @date, @time, @datetime, @local-datetime and @duration, and turns a value into the nanoseconds .dxnb holds and back.

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.

DXN uuid (@uuid "...", RFC 4122). Wraps 16 raw bytes, not the 36-char text form — the text form exists only at the .dxn/.dxnb encoding boundary (parse/1/format/1 below convert between them); the in-memory value stays the compact, comparison-cheap byte form.

The union of every shape a decoded DXN value can take. A typespec aid only — no functions, no runtime behavior.

Mix Tasks

Checks a tree of .dxns schemas (Dextrin.Schema.Tree): every file under SCHEMAS -- files or directories, searched recursively -- read, every entry named from its path (DXNS.md §6) and compiled in the order its references need. Every problem is printed as file:line: message and the task exits non-zero, so CI can run it

The version discipline check (Dextrin.Schema.Compat, DXNS.md §8): compares the schemas in OLD with those in NEW, each a .dxns file or a directory searched for them. A version whose content changed without a new version, or a version that went down, is reported and exits non-zero, so CI can run it against the previous release's schema files. What the latest version's materializer has to upgrade is listed either way.

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.