dixscript
DixScript core runtime for Rust — load, access, build, and convert .mdix files.
DixScript is a data interchange format with compile-time functions,
built-in capabilities for AES-256 encryption, and optional compression. This crate is
the Rust runtime: it compiles .mdix source, resolves all QuickFuncs
at compile time, and exposes a flat dotted-path API for reading the
resulting data at runtime.
Format documentation and language reference:
DixScript-Docs.pages.dev·github.com/Mid-D-Man/DixScript-RustModule and API index for contributors: see
APICATALOG.md
Quick start
[]
= "1.0.0"
use ;
config.mdix:
@DATA(
server: host = "api.example.com", port = 443, ssl = true
)
What this crate provides
| Module | What it does |
|---|---|
Runtime::DixLoader |
Compile and load .mdix files from disk, string, or encrypted bytes |
Runtime::DixData |
O(1) flat dotted-path access to loaded data |
Runtime::DixValue |
Runtime value type — 15 variants covering all DixScript types |
Runtime::DixDataBuilder |
Fluent builder for creating save data at runtime without a template |
Runtime::DixSerialize / DixDeserialize |
Convert between DixData and plain Rust structs directly, no intermediate hashmap |
Runtime::SchemaBuilder |
Validate loaded data against an expected shape, collecting every violation instead of stopping at the first |
Runtime::DixQuery |
LINQ-style where_/order_by/select chaining over array fields |
Runtime::MdixMerger |
AST-level merge of multiple sources — weight-based or strict conflict resolution |
Runtime::HotReloadWatcher |
Poll-based file-change watcher for reloading config without a restart |
Runtime::DixConverter |
Convert between DixScript, JSON, TOML, and HashMap<String, DixValue> — see from_dix_data vs from_hashmap below for which one to reach for |
Runtime::DixCompactor |
Minify and compact .mdix source text |
Runtime::DixLoadOptions |
Configure loading: passwords, key files, output directories |
Runtime::DixFormatOptions |
Configure serialization: indentation, minification, section inclusion |
Runtime::KeyResolver |
Derive or extract AES/ChaCha20 key bytes for encrypted files |
Loading files
Plain .mdix from disk
use ;
let loader = new;
let data = loader.load_text?;
From a string — useful for Unity TextAssets or embedded configs
let source = include_str!;
let data = loader.load_from_str?;
Encrypted file with a key file
use DixLoadOptions;
let opts = with_key_file;
let data = loader.load_encrypted?;
Encrypted file with a password
let opts = with_password;
let data = loader.load_encrypted?;
Encrypted bytes in memory — for platforms without filesystem access
let encrypted_bytes: & = /* from network, asset bundle, etc. */;
let key_content: &str = /* .mdix.key file contents as a string */;
let data = loader.load_from_encrypted_bytes?;
Reading data
DixData stores everything in a flat HashMap<String, DixValue> keyed
by dotted paths. Nested structures from the .mdix source are flattened
at load time, so all access is O(1).
Typed getters via TryFrom
// Returns Err if the path does not exist or the type does not match
let port: i32 = data.get?;
let host: String = data.get?;
let enabled: bool = data.get?;
let ratio: f64 = data.get?;
// Returns a default instead of Err
let timeout: i32 = data.get_or_default;
Raw value access
use DixValue;
match data.get_value
Checking existence
if data.exists
Array access
@DATA(
tags:: "alpha", "beta", "v1"
)
// The array itself
let tags: = data.get?;
// Individual indexed elements
let first: String = data.get?;
let second: String = data.get?;
Wildcard selection
// Matches tags.0.name, tags.1.name, tags.2.name, ...
let names: = data.select_many;
Key navigation
// Top-level keys
let top: = data.get_keys;
// Children of a prefix
let server_keys: = data.get_keys;
// → ["host", "port", "ssl"]
Metadata
println!;
println!;
println!;
if let Some = &data.config
Enums
DixScript enums are resolved at compile time. At runtime you get the enum name, field name, and integer value — no string parsing required.
@ENUMS(
AIType { PASSIVE = 0, AGGRESSIVE = 1, BOSS = 2 }
)
@DATA(
enemy_type<enum> = AIType.BOSS
)
use DixValue;
match data.get_value
// Or just get the integer
let ai_type: i32 = data.get?; // → 2
Building data at runtime
DixDataBuilder lets you construct save data, user preferences, or
any runtime-generated config without needing a template .mdix file.
use DixDataBuilder;
let data = new
.config
.enums
.data
.build?;
// Read it back the same way as loaded data
let name: String = data.get?;
let vol: i32 = data.get?;
Two-tier ordering rule: flat properties (with_string, with_int,
etc.) must be added before any table properties or group arrays. Adding
a flat property after grouped data returns Err from build() with a
descriptive message — it does not panic.
Struct (de)serialization
Convert between DixData and plain Rust structs directly, without an
intermediate HashMap round-trip. Implement DixDeserialize/
DixSerialize once per struct and reuse it everywhere DixData shows up.
use ;
// Deserialize a nested table straight into a struct.
let config: ServerConfig = data.deserialize_at?;
// Serialize a struct back into a fresh DixData.
let rebuilt = new
.serialize_at
.build?;
Schema validation
Validate a loaded DixData against an expected shape without stopping at
the first violation — ValidationReport collects everything wrong in one
pass, which matters when the input might be a modder-supplied or
hand-edited config: better to report every problem at once than make
someone fix-and-rerun repeatedly.
use SchemaBuilder;
let report = data.validate_schema;
if !report.is_valid
Querying
LINQ-style chaining over an array field's elements — filter, sort, and
project without hand-writing the loop. query(path) covers a plain
Array literal or a GroupArray's items alike; query_many(pattern)
matches across sibling paths that share shape via a wildcarded segment.
@DATA(
tasks::
{ name = "Fix bug", priority = 3 },
{ name = "Write docs", priority = 1 },
{ name = "Ship it", priority = 3 }
)
use DixValue;
let high_priority = data.query
.expect
.where_
.order_by_desc;
let names: = high_priority
.select;
// → ["Fix bug", "Ship it"]
Merging
AST-level merge of two or more DixScript sources — combine a base config
with environment overrides, or a shipped default with a player's local
save, without hand-rolling a deep merge over HashMaps. Conflicts (the
same key present in more than one source with a different value) are
resolved per the chosen MdixMergeStrategy; MergeConflict records
exactly what was decided, and why.
use ;
// File-path convenience — loads, compiles, merges, returns DixData directly.
let data = new.merge_files?;
// Explicit per-file weights — higher weight wins on conflict.
let data = new.merge_files_weighted?;
// Full control: pre-parsed ASTs, labels for readable conflict reports, and
// a strategy that refuses to silently pick a winner.
let result = new
.with_strategy
.merge_all;
for conflict in &result.conflicts
mdix diff (in mdix-cli) is built directly on this — it runs
ThrowOnConflict specifically to enumerate every disagreement between
files without picking a winner, then reports result.conflicts as-is.
Hot reload
A poll-based file-change watcher for Rust consumers — call
check_and_reload() once per game loop tick / server poll cycle; it only
does real work (re-reading and re-compiling the file) when the
modification time has actually changed.
use HotReloadWatcher;
let mut watcher = new;
// in your game loop / tick / update:
match watcher.check_and_reload
Each language binding (WASM/Python/C#/...) implements its own native
filesystem-event mechanism instead (inotify, FSEvents,
ReadDirectoryChangesW) rather than polling — this Rust-only watcher is
the simple, dependency-free default for direct dixscript consumers.
Converting formats
use DixConverter;
let converter = new;
// Load a .mdix file and export as JSON
let loader = new;
let data = loader.load_text?;
let ast = converter.from_dix_data?;
let json = converter.to_json?;
// Parse JSON and convert to .mdix
let ast2 = converter.from_json?;
let mdix = converter.to_mdix?;
// Round-trip through TOML
let toml = converter.to_toml?;
let ast3 = converter.from_toml?;
from_dix_data vs from_hashmap
DixConverter has two ways to turn loaded data back into a DixScript
AST — pick based on what you actually have on hand:
from_dix_data(&data)— use this whenever you already have a realDixData(the common case: anything that came out ofDixLoader). It reads the genuine@CONFIGand@ENUMSstraight fromDixData::config/DixData::enums, so the round trip is a faithful reconstruction, not a guess.from_hashmap(map)— use this only when all you have is a bareHashMap<String, DixValue>with no other context — e.g. a map you built by hand, or the internals offrom_json/from_toml(JSON and TOML have no config/enum concept of their own, so there's nothing extra to pull from). It still reconstructs a usable@ENUMSsection by scanning the map forDixValue::Enumusage, but the emitted@CONFIGis a synthetic placeholder (version = "1.0.0"only).
Note: JSON and TOML have no native enum type, so from_json/from_toml
round trips always lose the symbolic enum name — the integer survives,
the EnumName.FIELD identity doesn't. That's an inherent limitation of
those formats, not something either from_* method can recover.
Compacting and minifying source
use DixCompactor;
let source = read_to_string?;
// Remove all unnecessary whitespace — smallest output
let minified = minify;
// Remove trailing whitespace and collapse blank lines — keeps readability
let compacted = compact;
// Strip comments only
let no_comments = remove_comments;
// How much smaller?
let ratio = get_compression_ratio;
println!;
Format options
use ;
let converter = new;
let ast = /* ... */;
// Default: indented, 2-space, with @CONFIG section
let readable = converter.to_mdix?;
// Pretty: 4-space, sorted keys, with type annotations
let verbose = converter.to_mdix?;
// Compact: no indentation, no comments, no @CONFIG
let small = converter.to_mdix?;
// Minified: single line, no whitespace
let tiny = converter.to_mdix?;
// Custom
let mut opts = new;
opts.indent_size = 4;
opts.use_tabs = false;
opts.sort_keys = true;
let custom = converter.to_mdix?;
Load options reference
use DixLoadOptions;
// Default — no encryption, validates checksums
let opts = new;
// Password decryption
let opts = with_password;
// Explicit key file path
let opts = with_key_file;
// Key file content from a secrets manager (e.g. HashiCorp Vault)
let opts = with_key_content?;
// HTTPS URL key loading (trusted internal service only)
let opts = with_key_url?;
// Custom output directory for generated .enc/.key files
let opts = with_output_directory;
// Additional directories to search for key files automatically
let opts = with_key_search_paths;
Use cases
Game configuration (Unity / Bevy / Godot)
Define weapon stats, enemy AI types, shop items, and camo configs with
a single function call. CamoAvailableInSeason across 60 weapons is
one line. QuickFuncs eliminate all structural boilerplate at compile
time — the binary contains only resolved data.
@QUICKFUNCS(
~weapon<object>(id, class<enum>, baseDamage<int>) {
return {
id = id,
class = class,
damage = baseDamage,
critChance = 0.15f,
range = baseDamage * 2
}
}
)
@DATA(
weapons::
weapon("AK47", WeaponClass.ASSAULT, 35),
weapon("SHOTGUN", WeaponClass.HEAVY, 80),
weapon("PISTOL", WeaponClass.SIDEARM, 18)
)
let damage: i32 = data.get?; // 80
let range: i32 = data.get?; // 160
Multi-environment server config
@ENUMS(
Env { DEV = 1, STAGING = 2, PROD = 3 }
)
@QUICKFUNCS(
~db<object>(host, port<int>, ssl<bool>) {
return { host = host, port = port, ssl = ssl }
}
)
@DATA(
current_env<enum> = Env.PROD
database: db("db.prod.internal", 5432, true)
cache: host = "redis.prod.internal", port = 6379
)
let host: String = data.get?;
let ssl: bool = data.get?;
Encrypted secrets bundle
@DLM(DCompressor.gzip, DEncryptor.aes256)
@DATA(
stripe_secret = "sk_live_..."
jwt_secret = "hs512_..."
db_password = "..."
)
@SECURITY(
encryption -> { mode = "keyfile", algorithm = "aes256-gcm" }
)
# Produces: dist/secrets.mdix.enc + dist/secrets.mdix.key
let opts = with_key_file;
let data = loader.load_encrypted?;
let secret: String = data.get?;
Runtime save data (games, apps)
use DixDataBuilder;
// Build player save data
let save = new
.data
.build?;
// Reload the same save
let x: f64 = save.get?;
DixValue variants
| Variant | Rust type | DixScript literal |
|---|---|---|
Null |
— | null |
Bool(bool) |
bool |
true / false |
Int(i32) |
i32 |
42 |
Long(i64) |
i64 |
9_000_000_000L |
Float(f32) |
f32 |
3.14f |
Double(f64) |
f64 |
3.14159 |
String(String) |
String |
"hello" |
Date(String) |
String |
2025-12-31 |
Timestamp(String) |
String |
2025-12-31T10:30:00Z |
HexColor(String) |
String |
#FF5733 |
Blob(String) |
base64 String |
b:("...") |
Regex(String) |
pattern String |
r:("^[a-z]+$") |
Array(Vec<DixValue>) |
Vec<DixValue> |
:: a, b, c |
Object(HashMap<String, DixValue>) |
HashMap |
{ x = 1, y = 2 } |
Tuple(Vec<DixValue>) |
Vec<DixValue> |
t:(1, "a", true) |
Enum { enum_name, field_name, value } |
i32 via TryFrom |
MyEnum.VALUE |
Error handling
Every public function that can fail returns Result<T, String>.
The error string describes what went wrong and where — path not found,
type mismatch, parse failure, decryption error, and so on.
match loader.load_text
match data.
DixDataBuilder::build() collects all violations before returning
Err so you see every problem at once rather than fixing them one at a
time.
Feature flags
[]
= "1.0.0"
pulls in everything below by default — existing behavior is unchanged if you don't touch this. To trim what you don't need:
= { = "1.0.0", = false, = ["xz-support"] }
| Feature | Default | What it adds |
|---|---|---|
cloud-import |
on | HTTP/HTTPS @IMPORTS resolution (reqwest + rustls-tls) |
bzip2-support |
on | bzip2 compression for @DLM(DCompressor.bzip2) |
xz-support |
on | XZ/LZMA compression for @DLM(DCompressor.lzma) |
rayon-support |
on | Parallel section parsing/(de)serialization for large files |
Building with a feature off and then loading a .mdix file that actually
needs it (e.g. xz-support disabled but the file specifies
DCompressor.lzma) returns a clear Err naming the missing feature —
never a panic.
Platform notes
- gzip, bzip2, and XZ compression all work identically on every
target — native,
wasm32-unknown-unknown, and Android. All three backends are pure Rust (bzip2 vialibbz2-rs-sys, XZ vialzma-rust2, a real ported encoder, not a "compiles but barely compresses" placeholder) — no C toolchain, no NDK cross-compile pain, no wasm build failures. This wasn't always true; the platform notes here used to say bzip2/lzma were excluded on wasm32 — that was accurate for older versions and is no longer accurate as of this release. - All encryption algorithms (AES-128, AES-256-GCM, ChaCha20-Poly1305)
work on every target including
wasm32and Android — pure Rust RustCrypto primitives throughout, no exceptions. rayon-supportparallelizes on native targets when enabled (on by default) and always falls back to sequential processing onwasm32regardless of the feature flag — there's no real thread pool available there to parallelize onto in the first place.cloud-importdoes not actually fetch anything onwasm32. There's no way to make a real, safe synchronous network request from inside a wasm module — the@IMPORTScloud path returns a clear error on that target instead of silently failing. The working pattern on wasm is: the host (JS) does a normalfetch()itself, then seeds a cache the synchronous resolver checks first — seemdix-wasm'sprefetchImport()binding. Local (non-cloud)@IMPORTSfile paths have the same limitation on wasm32 for the same underlying reason (no real filesystem) — the host is expected to hand fully-assembled source toloadStr()rather than DixScript resolving imports itself.
MSRV
Rust 1.85 or later. (bzip2 0.6's pure-Rust backend needs 1.82;
lzma-rust2 needs 1.85 — the higher of the two is the real floor.)
License
MIT — see LICENSE.