dms-rb
Ruby parser for DMS, a data syntax with strong typing, ordered maps, multi-line heredocs, and front-matter metadata.
Two gems live in this repo, both with the same Ruby API and value shape:
| gem | implementation | when to use |
|---|---|---|
dms |
pure Ruby | portable; no C toolchain required |
dms-c |
C extension wrapping the dms-c decoder | hot paths; ~2× faster than pure Ruby |
Install
gem install dms # pure Ruby
gem install dms-c # native (C) extension, same API
Usage
require "dms" # or: require "dms_c"
src = File.read("config.dms")
# Body-only (drops front matter and comments after decode).
body = Dms.decode(src) # or: DmsC.decode(src)
# Full document (preserves comments + literal forms for encode round-trip).
doc = Dms.decode_document(src)
doc. # Hash | nil — nil when there is no `+++` block
doc.body # decoded root value
doc.comments # Array of Dms::AttachedComment
doc.original_forms # Array of [path, Dms::OriginalLiteral]
# Re-emit DMS source.
output = Dms.encode(doc)
Migrating from
parse/to_dms? SPEC v0.14 renamed the canonical entry points. The old names (Dms.parse,Dms.parse_document,Dms.parse_lite,Dms.to_dms,Dms.to_dms_lite, and the matchingDmsC.parse*) still work as deprecated aliases — each emits a one-shot warning on first call, then forwards to the canonical name. They will be removed in the next release.
Tables are insertion-ordered Hashes (Ruby Hashes preserve insertion
order since 1.9). Lists are Arrays. Datetimes are wrapped types: the
pure module returns Dms::LocalDate / Dms::LocalTime /
Dms::LocalDateTime / Dms::OffsetDateTime class instances; the C
extension returns plain { __dms_type:, value: } hashes with the same
data. Encoders that detect via __dms_type + value work unchanged
across both gems.
Working with comments and heredocs
DMS preserves comments through decode → mutate → re-emit (SPEC
§Comments). Attach a comment to a value after decoding and have it
round-trip through Dms.encode:
require "dms"
doc = Dms.decode_document("db:\n port: 8080\n")
# Mutate a value in place.
doc.body["db"]["port"] = 5432
# Attach a leading line comment to db.port.
doc.comments << Dms::AttachedComment.new(
Dms::Comment.new("# bumped after LB change", :line),
:leading,
["db", "port"],
)
puts Dms.encode(doc)
Forcing a heredoc on emit
Strings parse and re-emit in their source form. To switch a basic-quoted
string to a heredoc (or to construct one from scratch), append an
OriginalLiteral.string record to doc.original_forms keyed by the
value's path:
doc.body["db"]["greeting"] = "Hello, friend.\nWelcome aboard.\n"
doc.original_forms << [
["db", "greeting"],
Dms::OriginalLiteral.string(
Dms::StringForm.heredoc(
:basic_triple, # or :literal_triple for '''
nil, # nil = unlabeled (terminator is """ / ''')
[], # _trim(...), _fold_paragraphs(), …
),
),
]
Round-trip rules (SPEC §Round-trip semantics): comments stick to
still-present nodes; deleting a node drops its comments; newly
inserted nodes start with no comments. The first original_forms
entry per path wins, so override a parser-recorded form by replacing
rather than appending if the key is already present.
Performance
50,000-key flat document (~700 KB), best-of-5, startup-subtracted, Ruby 3.3 on Windows 11:
| tier | DMS gem | time | JSON peer | time | YAML peer | time | DMS / JSON | DMS / YAML |
|---|---|---|---|---|---|---|---|---|
| pure Ruby | dms |
115.8 ms | n/a | — | n/a | — | n/a | n/a |
| native (C) | dms-c |
56.5 ms | json |
21.4 ms | psych |
260.4 ms | 2.63× | 0.22× — DMS ~4.6× faster |
Ruby's stdlib json and psych are both C-backed; there's no
widely-used pure-Ruby alternative for either, so JSON and YAML peers
only appear in the FFI tier (same situation as Node). The pure-Ruby
DMS port is reported on its own — no fair pure-vs-pure peer to
compare against.
The C extension is ~2× faster than pure Ruby; against C-backed peers DMS is ~2.6× the JSON cost (the cost of carrying comments, ordered keys, and source-form metadata) and ~5× faster than libyaml.
Reproduce with:
ruby bench/run_formats.rb
Build & test
# pure gem:
bundle install
bundle exec rake test
# native (C) gem:
cd dms-c/ext/dms_c && ruby extconf.rb && make
The C-extension build needs Ruby's MSYS toolchain on Windows or a
standard cc + make on Unix; mkmf handles the platform detection.
Conformance
The fixture corpus lives in dms-tests (4500+ pairs). Clone it once as a sibling:
cd ..
git clone https://gitlab.com/flo-labs/pub/dms-tests.git
The dms-encoder binary reads DMS from stdin and writes tagged JSON to
stdout, matching the format the conformance runner consumes.
License
Dual-licensed: MIT or Apache-2.0, your choice.
