Quick reference for common dextrin tasks. See the
tutorial if anything here doesn't make sense yet, or
the DXN cheatsheet for format-syntax-level
lookups.
Decode / encode
Dextrin.decode(text, opts \\ []) #=> {:ok, value} | {:error, %Dextrin.Error{}}
Dextrin.encode(value, opts \\ []) #=> {:ok, text} | {:error, %Dextrin.Error{}}
Dextrin.decode_binary(bytes, opts \\ []) #=> {:ok, value} | {:error, %Dextrin.Error{}}
Dextrin.encode_binary(value, opts \\ []) #=> {:ok, bytes} | {:error, %Dextrin.Error{}}opts: registry: (a Dextrin.Registry.t(), both directions),
schema: (encode only — validate the top-level value against one
named schema), validate: (encode only, default true — set false
to skip the automatic whole-tree schema check), pretty: (encode/2
only, default false — multi-line, indented output instead of
single-line/compact), indent: (encode/2 only, meaningful with
pretty: true — spaces per nesting level, default 2), trusted:
(decode only, default true — keyword decodes as a real atom;
false for untrusted input decodes it as Dextrin.Keyword.t()
instead, so String.to_atom/1 is never reachable from attacker
-controlled text).
Render an error
{:error, error} = Dextrin.decode(bad_text)
IO.puts(Dextrin.Error.format(error))Pretty-print / reformat
Dextrin.encode(value, pretty: true) #=> {:ok, text} | {:error, %Dextrin.Error{}}
Dextrin.encode(value, pretty: true, indent: 4) # 4 spaces per level instead of the default 2
Dextrin.Text.Formatter.pretty(value, opts \\ []) # what pretty: true calls; same {:ok, _}|{:error, _}
# contract, same indent: optCompact is encode/2's default — the smallest text a value can
round-trip through, no line-wrapping or indentation at all. pretty:
only changes rendering, never what the value decodes back to.
mix dextrin.format data.dxn --mode pretty|condense [--in-place] # pretty keeps comments
Documents: keeping comments
{:ok, doc} = Dextrin.Document.parse(source, opts) # decode/2's opts and errors
Dextrin.Document.to_value(doc) # what decode/2 gives
Dextrin.Document.render(doc) # the source, byte for byte
Dextrin.Document.format(doc) # laid out, comments kept
{:ok, doc} = Dextrin.Document.replace(doc, [:version], "2.0.0")
{:ok, doc} = Dextrin.Document.delete(doc, [:deps, 0]) # with its own comments
{:ok, doc} = Dextrin.Document.insert(doc, [:deps], -1, "new") # index, or a map's new key
{:ok, doc, changes} = Dextrin.Document.update(doc, new_value, on_ambiguity: :safe)Value-type cheatsheet
symbol Dextrin.Symbol{name: string} bare identifier, e.g. `foo`
keyword atom (default) / Dextrin.Keyword `:foo` / `foo:` (key position) — trusted: false for the latter
tuple Dextrin.Tuple{items: [term]} `{1 2 3}` — list-backed, arbitrary length
array Dextrin.Array{items: tuple} `@array[1 2 3]` — tuple-backed, fixed size
ordered-map Dextrin.OrderedMap{pairs: [{k,v}]} `@ordered %{...}` — order is part of identity
sorted-set Dextrin.SortedSet{items: [term]} `@sorted-set @{...}` — always sorted, deduped
struct Dextrin.Struct{name, fields} `%Name{...}` / `%Name[...]`, opaque w/o schema
duration Dextrin.Duration{years, months, ...} `@duration "P1Y2M"` — 7 independent fields
rational Dextrin.Rational{numerator, denom} `22/7` — stored exactly, never auto-reduced
uuid Dextrin.Uuid{bytes: <<_::128>>} `@uuid "..."` — 16 raw bytes, not the text form
uri Dextrin.Uri{value: string} `@uri "..."` — raw string, not parsed
bytes Dextrin.Bytes{data: binary} `@bytes "..."` (base64 in text)
char Dextrin.Char{codepoint: integer} `?a` — distinct from a 1-grapheme string
custom-tag Dextrin.CustomTag{name, value} `@tag value`, no decoder registeredEverything else (nil, boolean, integer, float, Decimal.t(),
String.t(), list(), plain map(), MapSet.t(), Date.t(),
Time.t(), DateTime.t(), Regex.t()) is the obvious native Elixir
value — see Dextrin.Value's moduledoc for the full union.
Registry: custom tags
registry
|> Dextrin.Registry.put_tag(name, fn value -> {:ok, term} | {:error, reason} end)
|> Dextrin.Registry.put_tag_encoder(module, name, fn struct -> {:ok, value} | {:error, reason} end)Registry: schemas
{:ok, registry} = Dextrin.Schema.compile(schema_doc, base_registry \\ Dextrin.Registry.new())
registry
|> Dextrin.Registry.put_struct_materializer(name, fn name, version, fields -> {:ok, term} end) # every version; result checked against the latest
|> Dextrin.Registry.put_struct_module(name, module) # for {:reference, name} checks on hand-built structs
|> Dextrin.Registry.put_resolver(fn name -> {:ok, compiled} | :unknown end) # lazy/on-demand schema loading
|> Dextrin.Registry.put_strict(true) # refuse extra fields (schema/undeclared-field); or decode(..., strict: true)
Dextrin.Registry.fetch_struct_schema(registry, name) # the latest version
Dextrin.Registry.fetch_struct_schema(registry, name, version) # exactly that one
Dextrin.Registry.replace_struct_schema(registry, name, compiled) # hot swap a loaded versionDextrin.Schema.FileResolver.for_paths(paths) builds
a ready-made resolver for Namespace/Name -> paths/Namespace.dxns.
Registry: third-party structs (Dextrin.Schema.Provider)
For a struct defined by a library that doesn't (and shouldn't have to)
depend on dextrin itself — see Dextrin.Schema.Provider's own
moduledoc for the full optional-dependency pattern this is meant to
support:
{:ok, registry} = Dextrin.Schema.register_provider(registry, SomeLib.Money.DXN)SomeLib.Money.DXN implements the Dextrin.Schema.Provider behaviour
(dxn_schema/0, dxn_schema_name/0, dxn_struct/0, and optionally
dxn_materialize/3) — a small, separately-compiled companion module,
not the struct's own module, so the struct itself is never
conditionally compiled.
Schema validation, outside of decode
Dextrin.Schema.validate(value, registry, schema_name) #=> :ok | {:error, reason}
Dextrin.Schema.validate_encode(value, registry, schema_name) # one named schema, encode-side
Dextrin.Schema.validate_encode_tree(value, registry) # automatic whole-tree walk, encode-side.dxns type_expr forms, at a glance
:any any value at all
:integer / :string / ... one keyword per DXN.md §1.3 type name
Address bare symbol — a struct name or a named type
{:list-of T} list whose every element matches T
{:set-of T} set whose every element matches T
{:tuple-of A B C} fixed-arity positional tuple
{:map-of K V} every key matches K, every value matches V
{:enum :a :b :c} value must equal one of the given literals
{:one-of A B} union — matches at least one
{:all-of A B} intersection — matches all
{:nilable T} sugar for {:one-of :nil T}
{:refine T %{constraints}} base type + refine constraints (below)
%schema{} struct/record shaperefine constraints
min / max / exclusive-min / exclusive-max / multiple-of -- integer/float/decimal/rational
min-length / max-length / pattern -- string
min-count / max-count -- list/set/tupleStruct schema options
%schema(2){ # version 2; no header = version 1
required_field: :integer # the body's order is the .dxnb position
optional_field?: :string # trailing `?` = optional
with_default: %field{type: :integer, default: 0, description: "..."}
}Versions and extra fields
%Point(2){x: 1, y: 2} # written with version 2; encoding always writes the latest
%Point{x: 1, y: 2} # no version: read as the latest
%Point[1 2 label: "a"] # extra fields after a positional struct's items
%Point{x: 1, y: 2, label: "a"} # an undeclared name is an extra fieldNewer than loaded: read against the latest (text by name, .dxnb by
position). Older and loaded: the materializer upgrades it, else
schema/no-materializer. Older and not loaded: schema/unknown-version.
Reordering fields misreads silently in older .dxnb readers: append only.
Standard named types (Dextrin.Schema.Std)
{:ok, registry} = Dextrin.Schema.compile(doc, Dextrin.Schema.Std.registry())PositiveInteger, NonNegativeInteger, NegativeInteger, NonPositiveInteger
PositiveFloat, NonNegativeFloat, Percentage
NonEmptyString
NonEmptyList, NonEmptySetMix tasks
mix dextrin.validate data.dxn [--format text|binary] [--schema s.dxns --as Name]
mix dextrin.encode data.dxn [--out data.dxnb] [--share]
mix dextrin.decode data.dxnb [--out data.dxn]
mix dextrin.format data.dxn [--mode pretty|condense] [--in-place]
mix dextrin.compat old-schemas/ schemas/
mix dextrin.gen.schema MyApp.SomeStruct [--out schema.dxns] [--name Name]
mix dextrin.gen.unicode [--file PATH] [--force]