Dextrin

CIHex.pmDocumentation

Dextrin is an Elixir implementation of DXN (Data eXchange Notation): a human-writable text format (.dxn), a compact binary format (.dxnb) built on CBOR, and a schema format (.dxns) that's just more DXN data — all three sharing one in-memory value representation and one extension mechanism.

{:ok, value} = Dextrin.decode(~s(%{x: 1, y: 2}))
value
#=> %{x: 1, y: 2}
Dextrin.encode(value)
#=> {:ok, "%{x:1,y:2}"}
{:ok, bytes} = Dextrin.encode_binary(value)
Dextrin.decode_binary(bytes)
#=> {:ok, %{x: 1, y: 2}}

A plain map's shorthand keys (x:) are themselves DXN keywords — %{x: 1} and %{:x => 1} are the exact same value, and keyword's own Elixir type, per DXN.md §1.3, is a real atom, which is what both decode/2 and decode_binary/2 produce by default (trusted: true). Encoding accepts a bare atom the same way, anywhere a keyword is expected — %{x: 1} or Dextrin.encode(:ok) both work with no conversion needed first.

That default assumes a source you control — your own config, your own application's data — not arbitrary untrusted/network input, where an unbounded String.to_atom/1 could exhaust the atom table. Pass trusted: false for that case; keyword then decodes as Dextrin.Keyword.t() instead:

Dextrin.decode(~s(%{x: 1, y: 2}), trusted: false)
#=> {:ok, %{%Dextrin.Keyword{name: "x"} => 1, %Dextrin.Keyword{name: "y"} => 2}}

A schema-backed struct's fields are the one place names do come back as plain strings regardless of trusted: — see the tutorial — since a schema always knows its field names up front.

.dxn's text grammar is compiled by Ichor — write the grammar once (priv/grammar/dxn.aether), get a lexer, parser, and (via Dextrin.Text.Actions) an evaluator with no hand-written parsing code. That compilation happens ahead of time (mix ichor.gen, checked in as generated source), not at dextrin's own build time, so only the small ichor_runtime support library the generated code actually calls ships as a real dependency — ichor proper (the Aether front-end, analysis, codegen) is dev-tooling only. .dxnb has no grammar to speak of — it's a direct, hand-rolled CBOR codec — so it's plain recursive Elixir working over the same shared value type.

Why

Most serialization formats pick one point on a spectrum: JSON is human-writable but loses precision (no distinct int/float boundary, no dates, no bytes) and has no extension story; Protobuf/Avro are compact and typed but need a separate schema-compiler step and aren't meant for a human to read or hand-edit; EDN is expressive and Elixir-friendly in spirit but has no first-party Elixir implementation and no binary counterpart. DXN's premise is that a .dxn text document and a .dxnb binary document should be the same value space — 30 scalar/collection/temporal/extended types (reference §1.3), precise enough for money (Decimal), exact ratios (Rational), and arbitrary precision integers, with struct and custom-tag as first-class, schema-describable extension points — encoded however density or readability happens to matter for a given use.

Components

Installation

Add dextrin to your list of dependencies in mix.exs:

def deps do
[
{:dextrin, "~> 0.1.0"}
]
end

Where to go next

Development

mix deps.get
mix precommit

mix precommit runs the full verification pass this project expects before a commit: mix format, mix compile --warnings-as-errors, mix credo --strict, mix sobelow, mix test, and mix dialyzer, in that order (fast/cheap checks first, dialyzer — the slowest, especially its first PLT build — last).

priv/grammar/dxn.aether's generated Unicode identifier ranges are regenerated with mix dextrin.gen.unicode — a deliberate, reviewed action on a Unicode version bump, never run automatically at build time (see that task's own docs). Either way, changing the grammar itself requires a mix ichor.gen step afterward, since lib/dextrin/text/grammar/native.ex is generated ahead of time, not produced at dextrin's own compile time (that's also what keeps ichor itself, and everything it depends on for parsing/codegen, only: :dev, runtime: false — only the small ichor_runtime package ships in a release) — see CONTRIBUTION.md.

See CONTRIBUTION.md for how to propose changes, and CHANGELOG.md for release history.

License

MIT — see LICENSE.