dms-parser 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +78 -6
- data/bin/dms-encoder +22 -3
- data/lib/dms/tier1.rb +61 -28
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 356dcaecf022100fac1eeae5350a76ecfc417f831f64084ffe912cd4da486a80
|
|
4
|
+
data.tar.gz: 74e4bd7721ac3f5c7325ada5145c98ad5eccb7723c6e0eed45fcd99dd0a61b9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39ef8d91e5fdb82746a03ddc8e3b367970b60ff2291021b7a33ed922d3fc93a377f6e0a4c4ddd9093d52f9c7aae87bf3d674ca089bd18c6d6a42fcde2c58358a
|
|
7
|
+
data.tar.gz: d95fec6634f7049576d56e93195eb47f772e54daa49e270ea7afeace93c2ea3021747c9b8e4a8baa17e54779e1d687198f58462da5a7fd1531d6baf05f521cc8
|
data/README.md
CHANGED
|
@@ -7,16 +7,88 @@ multi-line heredocs, and front-matter metadata.
|
|
|
7
7
|
|
|
8
8
|
Two gems live in this repo, both with the same Ruby API and value shape:
|
|
9
9
|
|
|
10
|
-
| gem
|
|
11
|
-
|
|
|
12
|
-
| `dms`
|
|
13
|
-
| `dms-c
|
|
10
|
+
| gem | implementation | when to use |
|
|
11
|
+
| ------------ | ----------------------------------------- | ------------------------------------ |
|
|
12
|
+
| `dms-parser` | pure Ruby (`require "dms"`) | portable; no C toolchain required |
|
|
13
|
+
| `dms-c` | C extension wrapping the dms-c decoder | hot paths; ~2× faster than pure Ruby |
|
|
14
|
+
|
|
15
|
+
> **Gem naming.** The plain `dms` name on RubyGems is taken by an unrelated
|
|
16
|
+
> project, so the pure-Ruby gem ships as `dms-parser`. The require path is
|
|
17
|
+
> still `"dms"` (the install command and the `require` line don't match —
|
|
18
|
+
> a one-time gotcha when adding the dependency).
|
|
19
|
+
|
|
20
|
+
## What DMS looks like
|
|
21
|
+
|
|
22
|
+
A medium-size tier-0 document, exercising every feature you'd touch in a
|
|
23
|
+
real config — front matter, comments (line + trailing), nested tables,
|
|
24
|
+
list-of-tables with the `+` marker, flow forms, distinct types, and a
|
|
25
|
+
heredoc with a trim modifier:
|
|
26
|
+
|
|
27
|
+
```dms
|
|
28
|
+
+++
|
|
29
|
+
title: "DMS feature tour"
|
|
30
|
+
version: "1.0.0"
|
|
31
|
+
updated: 2026-04-24T09:30:00-04:00
|
|
32
|
+
+++
|
|
33
|
+
|
|
34
|
+
# Hash and // line comments both work.
|
|
35
|
+
// Bare keys allow full Unicode; quoted keys take any string.
|
|
36
|
+
|
|
37
|
+
database:
|
|
38
|
+
host: "db.internal"
|
|
39
|
+
port: 5432 # bumped after the LB change
|
|
40
|
+
pool: { size: 10, idle_timeout_s: 30 } # flow table
|
|
41
|
+
|
|
42
|
+
servers:
|
|
43
|
+
+ name: "web1"
|
|
44
|
+
disks:
|
|
45
|
+
+ mount: "/"
|
|
46
|
+
size_gb: 100
|
|
47
|
+
+ mount: "/var"
|
|
48
|
+
size_gb: 500
|
|
49
|
+
+ name: "web2"
|
|
50
|
+
|
|
51
|
+
regions: ["us-east-1", "eu-west-1", "ap-south-1"]
|
|
52
|
+
|
|
53
|
+
sql: """SQL _trim("\n", ">")
|
|
54
|
+
SELECT id, email
|
|
55
|
+
FROM users
|
|
56
|
+
WHERE active = true
|
|
57
|
+
SQL
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tier 1 layers structured decorators on top of the value tree. Sigils bind
|
|
61
|
+
to families published by a dialect; here is `dms+html` carrying an HTML
|
|
62
|
+
fragment as a DMS document:
|
|
63
|
+
|
|
64
|
+
```dms
|
|
65
|
+
+++
|
|
66
|
+
_dms_tier: 1
|
|
67
|
+
_dms_imports:
|
|
68
|
+
+ dialect: "html"
|
|
69
|
+
version: "1.0.0"
|
|
70
|
+
+++
|
|
71
|
+
|
|
72
|
+
+ |html(lang: "en")
|
|
73
|
+
+ |head
|
|
74
|
+
+ |title "DMS feature tour"
|
|
75
|
+
+ |meta(charset: "UTF-8")
|
|
76
|
+
+ |body(class: "main")
|
|
77
|
+
+ |h1 "Welcome to DMS"
|
|
78
|
+
+ |p(class: "lede")
|
|
79
|
+
+ "Click "
|
|
80
|
+
+ |a(href: "/spec.html") "here"
|
|
81
|
+
+ " to read the spec."
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Full feature tour, format comparison, and dialect index on the
|
|
85
|
+
**[DMS website](https://flo-labs.gitlab.io/pub/dms-webpage/)**.
|
|
14
86
|
|
|
15
87
|
## Install
|
|
16
88
|
|
|
17
89
|
```sh
|
|
18
|
-
gem install dms
|
|
19
|
-
gem install dms-c # native (C) extension, same API
|
|
90
|
+
gem install dms-parser # pure Ruby
|
|
91
|
+
gem install dms-c # native (C) extension, same API (not yet published)
|
|
20
92
|
```
|
|
21
93
|
|
|
22
94
|
## Usage
|
data/bin/dms-encoder
CHANGED
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
# Reads DMS source from stdin, writes tagged JSON to stdout.
|
|
6
6
|
|
|
7
7
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../dms-c/lib", __dir__))
|
|
8
9
|
|
|
9
10
|
require "json"
|
|
10
11
|
require "dms"
|
|
11
12
|
|
|
13
|
+
# Feature-detect the native dms-c gem for tier-1 acceleration.
|
|
14
|
+
# Falls back silently to pure-Ruby Dms::Tier1 if unavailable or
|
|
15
|
+
# if the build hasn't been compiled yet.
|
|
16
|
+
DMS_C_AVAILABLE =
|
|
17
|
+
begin
|
|
18
|
+
require "dms_c"
|
|
19
|
+
DmsC.respond_to?(:decode_t1_to_json)
|
|
20
|
+
rescue LoadError
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
# Force UTF-8 output: tagged JSON uses ensure_ascii=False equivalent
|
|
13
25
|
# (we write raw UTF-8) so stdout must accept it. Disable CRLF translation.
|
|
14
26
|
$stdout.set_encoding("UTF-8")
|
|
@@ -188,9 +200,16 @@ end
|
|
|
188
200
|
begin
|
|
189
201
|
# Tier-1 decode path: parse with decorator awareness, emit wrapper JSON.
|
|
190
202
|
if tier_flag == 1
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
if DMS_C_AVAILABLE
|
|
204
|
+
# Native path: dms-c tier1.c returns ready-to-print JSON; pretty-print it.
|
|
205
|
+
raw_hash = DmsC.decode_t1(src)
|
|
206
|
+
$stdout.write(JSON.pretty_generate(raw_hash, indent: " ") + "\n")
|
|
207
|
+
else
|
|
208
|
+
# Pure-Ruby fallback.
|
|
209
|
+
doc_t1 = Dms::Tier1.parse(src)
|
|
210
|
+
out = Dms::Tier1.emit_t1_json(doc_t1, method(:tag))
|
|
211
|
+
$stdout.write(JSON.pretty_generate(out, indent: " ") + "\n")
|
|
212
|
+
end
|
|
194
213
|
exit 0
|
|
195
214
|
end
|
|
196
215
|
|
data/lib/dms/tier1.rb
CHANGED
|
@@ -217,7 +217,7 @@ module Dms
|
|
|
217
217
|
# params: Array of ParamGroup
|
|
218
218
|
# params_dec: Array (always empty in this implementation)
|
|
219
219
|
# sigil: String (the literal sigil string)
|
|
220
|
-
DecoratorCall = Struct.new(:sigil, :family, :fn_name, :ns, :position, :params, :params_dec)
|
|
220
|
+
DecoratorCall = Struct.new(:sigil, :family, :fn_name, :ns, :position, :params, :params_dec, :call_style)
|
|
221
221
|
|
|
222
222
|
# kind: :named or :positional
|
|
223
223
|
# value: Hash (for named) or Array (for positional)
|
|
@@ -463,7 +463,8 @@ module Dms
|
|
|
463
463
|
# Resolve a decorator call's family from imports.
|
|
464
464
|
# Returns [family_name, canonical_fn_name] or raises.
|
|
465
465
|
# Also applies deny-list check.
|
|
466
|
-
|
|
466
|
+
# call_style: :named or :nameless
|
|
467
|
+
def self.resolve_family(sigil, fn_name, ns, imports, call_style: :named)
|
|
467
468
|
# Filter imports by ns if specified
|
|
468
469
|
candidate_imports = if ns
|
|
469
470
|
filtered = imports.select { |imp| imp.ns == ns }
|
|
@@ -475,6 +476,24 @@ module Dms
|
|
|
475
476
|
imports
|
|
476
477
|
end
|
|
477
478
|
|
|
479
|
+
# Nameless call: fn_name is empty; resolve to the single bound family name
|
|
480
|
+
if call_style == :nameless
|
|
481
|
+
bound_families = []
|
|
482
|
+
candidate_imports.each do |imp|
|
|
483
|
+
(imp.bind[sigil] || []).each { |fam| bound_families << fam }
|
|
484
|
+
end
|
|
485
|
+
if bound_families.empty?
|
|
486
|
+
raise DecodeError.new(0, 0, "unbound sigil '#{sigil}'")
|
|
487
|
+
end
|
|
488
|
+
if bound_families.size > 1
|
|
489
|
+
raise DecodeError.new(0, 0,
|
|
490
|
+
"nameless call on sigil '#{sigil}' is ambiguous between families " \
|
|
491
|
+
+ bound_families.map { |f| "'#{f}'" }.join(", "))
|
|
492
|
+
end
|
|
493
|
+
family = bound_families.first
|
|
494
|
+
return [family, family]
|
|
495
|
+
end
|
|
496
|
+
|
|
478
497
|
# For each import, check families bound to this sigil
|
|
479
498
|
# Apply aliases and allow/deny rules
|
|
480
499
|
accepted = [] # [family_name, canonical_fn_name]
|
|
@@ -773,8 +792,8 @@ module Dms
|
|
|
773
792
|
# But we don't know the next key yet — so we store them as pending.
|
|
774
793
|
def collect_leading_decorators(path_prefix)
|
|
775
794
|
while !eof? && sigil_at?(@pos)
|
|
776
|
-
sigil, fn_name, ns, params = parse_decorator_call
|
|
777
|
-
@pending_leading << { sigil: sigil, fn_name: fn_name, ns: ns, params: params }
|
|
795
|
+
sigil, fn_name, ns, params, cs = parse_decorator_call
|
|
796
|
+
@pending_leading << { sigil: sigil, fn_name: fn_name, ns: ns, params: params, call_style: cs }
|
|
778
797
|
skip_trivia_no_consume_leading
|
|
779
798
|
break if eof?
|
|
780
799
|
# Check if next line also starts with a sigil
|
|
@@ -824,8 +843,8 @@ module Dms
|
|
|
824
843
|
if sigil_at?(@pos)
|
|
825
844
|
# Collect leading decorators for next value
|
|
826
845
|
loop do
|
|
827
|
-
sigil, fn_name, ns, params = parse_decorator_call
|
|
828
|
-
@pending_leading << { sigil: sigil, fn_name: fn_name, ns: ns, params: params }
|
|
846
|
+
sigil, fn_name, ns, params, cs = parse_decorator_call
|
|
847
|
+
@pending_leading << { sigil: sigil, fn_name: fn_name, ns: ns, params: params, call_style: cs }
|
|
829
848
|
# After decorator call, consume rest of line
|
|
830
849
|
skip_inline_ws
|
|
831
850
|
if consume_eol || eof?
|
|
@@ -860,8 +879,8 @@ module Dms
|
|
|
860
879
|
pending = @pending_leading.dup
|
|
861
880
|
@pending_leading.clear
|
|
862
881
|
pending.each do |dec|
|
|
863
|
-
family, canonical_fn = resolve_call(dec[:sigil], dec[:fn_name], dec[:ns])
|
|
864
|
-
add_decorator_call(current_path, dec[:sigil], family, canonical_fn, dec[:ns], :leading, dec[:params])
|
|
882
|
+
family, canonical_fn = resolve_call(dec[:sigil], dec[:fn_name], dec[:ns], call_style: dec[:call_style] || :named)
|
|
883
|
+
add_decorator_call(current_path, dec[:sigil], family, canonical_fn, dec[:ns], :leading, dec[:params], call_style: dec[:call_style] || :named)
|
|
865
884
|
end
|
|
866
885
|
end
|
|
867
886
|
|
|
@@ -1029,9 +1048,9 @@ module Dms
|
|
|
1029
1048
|
def parse_t1_inner_and_value(path)
|
|
1030
1049
|
# Collect all consecutive inner decorator calls
|
|
1031
1050
|
while !eof? && sigil_at?(@pos)
|
|
1032
|
-
sigil, fn_name, ns, params = parse_decorator_call
|
|
1033
|
-
family, canonical_fn = resolve_call(sigil, fn_name, ns)
|
|
1034
|
-
add_decorator_call(path, sigil, family, canonical_fn, ns, :inner, params)
|
|
1051
|
+
sigil, fn_name, ns, params, cs = parse_decorator_call
|
|
1052
|
+
family, canonical_fn = resolve_call(sigil, fn_name, ns, call_style: cs || :named)
|
|
1053
|
+
add_decorator_call(path, sigil, family, canonical_fn, ns, :inner, params, call_style: cs || :named)
|
|
1035
1054
|
skip_inline_ws
|
|
1036
1055
|
end
|
|
1037
1056
|
|
|
@@ -1059,9 +1078,9 @@ module Dms
|
|
|
1059
1078
|
|
|
1060
1079
|
def parse_t1_trailing_decorators(path)
|
|
1061
1080
|
while !eof? && sigil_at?(@pos)
|
|
1062
|
-
sigil, fn_name, ns, params = parse_decorator_call
|
|
1063
|
-
family, canonical_fn = resolve_call(sigil, fn_name, ns)
|
|
1064
|
-
add_decorator_call(path, sigil, family, canonical_fn, ns, :trailing, params)
|
|
1081
|
+
sigil, fn_name, ns, params, cs = parse_decorator_call
|
|
1082
|
+
family, canonical_fn = resolve_call(sigil, fn_name, ns, call_style: cs || :named)
|
|
1083
|
+
add_decorator_call(path, sigil, family, canonical_fn, ns, :trailing, params, call_style: cs || :named)
|
|
1065
1084
|
skip_inline_ws
|
|
1066
1085
|
end
|
|
1067
1086
|
end
|
|
@@ -1081,15 +1100,28 @@ module Dms
|
|
|
1081
1100
|
|
|
1082
1101
|
raise DecodeError.new(@line, col, "empty decorator sigil") if sigil.empty?
|
|
1083
1102
|
|
|
1084
|
-
# Parse name (bare identifier)
|
|
1085
|
-
#
|
|
1086
|
-
|
|
1087
|
-
|
|
1103
|
+
# Parse name (bare identifier) — may be empty for nameless calls
|
|
1104
|
+
# Nameless: sigil followed by '(', whitespace, EOL, ',', ']', '}'
|
|
1105
|
+
# Error: sigil followed by '.' (dotted form requires name before '.')
|
|
1106
|
+
next_byte = @src.getbyte(@pos)
|
|
1107
|
+
call_style = :named
|
|
1108
|
+
if next_byte == DOT
|
|
1109
|
+
raise DecodeError.new(@line, col, "dotted form requires a name before '.'")
|
|
1110
|
+
elsif next_byte == LPAREN || next_byte == SP || next_byte == TAB ||
|
|
1111
|
+
next_byte == LF || next_byte == CR || next_byte == COMMA ||
|
|
1112
|
+
next_byte == RBRACK || next_byte == RBRACE || next_byte.nil? || @pos >= @src.bytesize
|
|
1113
|
+
# Nameless call
|
|
1114
|
+
call_style = :nameless
|
|
1115
|
+
name1 = ""
|
|
1116
|
+
else
|
|
1117
|
+
name1 = parse_bare_ident
|
|
1118
|
+
raise DecodeError.new(@line, col, "expected decorator name after sigil '#{sigil}'") if name1.empty?
|
|
1119
|
+
end
|
|
1088
1120
|
|
|
1089
1121
|
# Check for '.' => namespace qualifier
|
|
1090
1122
|
ns = nil
|
|
1091
1123
|
fn_name = name1
|
|
1092
|
-
if @src.getbyte(@pos) == DOT
|
|
1124
|
+
if call_style == :named && @src.getbyte(@pos) == DOT
|
|
1093
1125
|
@pos += 1 # consume '.'
|
|
1094
1126
|
name2 = parse_bare_ident
|
|
1095
1127
|
if name2.empty?
|
|
@@ -1122,7 +1154,7 @@ module Dms
|
|
|
1122
1154
|
# If no params at all, treat as one empty named group
|
|
1123
1155
|
params << ParamGroup.new(:named, {}) if params.empty?
|
|
1124
1156
|
|
|
1125
|
-
[sigil, fn_name, ns, params]
|
|
1157
|
+
[sigil, fn_name, ns, params, call_style]
|
|
1126
1158
|
end
|
|
1127
1159
|
|
|
1128
1160
|
# Parse a param group (between parens, after '(' was consumed).
|
|
@@ -1244,9 +1276,9 @@ module Dms
|
|
|
1244
1276
|
# Inner decorator in flow array
|
|
1245
1277
|
loop do
|
|
1246
1278
|
break unless sigil_at?(@pos)
|
|
1247
|
-
sigil, fn_name, ns, params = parse_decorator_call
|
|
1248
|
-
family, canonical_fn = resolve_call(sigil, fn_name, ns)
|
|
1249
|
-
add_decorator_call(current_path, sigil, family, canonical_fn, ns, :inner, params)
|
|
1279
|
+
sigil, fn_name, ns, params, cs = parse_decorator_call
|
|
1280
|
+
family, canonical_fn = resolve_call(sigil, fn_name, ns, call_style: cs || :named)
|
|
1281
|
+
add_decorator_call(current_path, sigil, family, canonical_fn, ns, :inner, params, call_style: cs || :named)
|
|
1250
1282
|
skip_inline_ws
|
|
1251
1283
|
end
|
|
1252
1284
|
# Now parse the actual value
|
|
@@ -1326,20 +1358,20 @@ module Dms
|
|
|
1326
1358
|
val
|
|
1327
1359
|
end
|
|
1328
1360
|
|
|
1329
|
-
def add_decorator_call(path, sigil, family, fn_name, ns, position, params)
|
|
1361
|
+
def add_decorator_call(path, sigil, family, fn_name, ns, position, params, call_style: :named)
|
|
1330
1362
|
path_key = path_to_key(path)
|
|
1331
1363
|
entry = @dec_entries[path_key]
|
|
1332
1364
|
if entry.nil?
|
|
1333
1365
|
entry = DecoratorEntry.new(path.dup, {}, [])
|
|
1334
1366
|
@dec_entries[path_key] = entry
|
|
1335
1367
|
end
|
|
1336
|
-
call = DecoratorCall.new(sigil, family, fn_name, ns, position, params, [])
|
|
1368
|
+
call = DecoratorCall.new(sigil, family, fn_name, ns, position, params, [], call_style)
|
|
1337
1369
|
entry.calls[sigil] ||= []
|
|
1338
1370
|
entry.calls[sigil] << call
|
|
1339
1371
|
end
|
|
1340
1372
|
|
|
1341
|
-
def resolve_call(sigil, fn_name, ns)
|
|
1342
|
-
Tier1.resolve_family(sigil, fn_name, ns, @imports)
|
|
1373
|
+
def resolve_call(sigil, fn_name, ns, call_style: :named)
|
|
1374
|
+
Tier1.resolve_family(sigil, fn_name, ns, @imports, call_style: call_style)
|
|
1343
1375
|
end
|
|
1344
1376
|
|
|
1345
1377
|
def path_to_key(path)
|
|
@@ -1732,7 +1764,8 @@ module Dms
|
|
|
1732
1764
|
"ns" => call.ns,
|
|
1733
1765
|
"position" => call.position.to_s,
|
|
1734
1766
|
"params" => params_json,
|
|
1735
|
-
"params_dec" => []
|
|
1767
|
+
"params_dec" => [],
|
|
1768
|
+
"call_style" => (call.call_style || :named).to_s
|
|
1736
1769
|
}
|
|
1737
1770
|
end
|
|
1738
1771
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dms-parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Filip Lopes
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Ruby parser for DMS, a data syntax with strong typing, ordered maps,
|
|
14
14
|
multi-line heredocs, and front-matter metadata.
|