dms-parser 0.5.0
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 +7 -0
- data/LICENSE-APACHE +15 -0
- data/LICENSE-MIT +21 -0
- data/README.md +166 -0
- data/bin/dms-encoder +234 -0
- data/lib/dms/emitter.rb +674 -0
- data/lib/dms/parser.rb +3007 -0
- data/lib/dms/tier1.rb +1750 -0
- data/lib/dms/types.rb +129 -0
- data/lib/dms.rb +161 -0
- metadata +56 -0
data/lib/dms/types.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dms
|
|
4
|
+
# Marker subclass of Hash for unordered tables (SPEC §"Unordered tables").
|
|
5
|
+
# Built only by `Dms.decode_document_unordered` /
|
|
6
|
+
# `Dms.decode_lite_document_unordered` (and the `--ignore-order` CLI flag).
|
|
7
|
+
# Iteration order is **arbitrary** — keys are shuffled at end-of-build so
|
|
8
|
+
# callers cannot accidentally depend on insertion order.
|
|
9
|
+
#
|
|
10
|
+
# Full-mode `Dms.encode` raises a `RuntimeError` on any tree containing an
|
|
11
|
+
# `UnorderedHash`; lite-mode `Dms.encode_lite` accepts them and emits in
|
|
12
|
+
# whatever iteration order they expose.
|
|
13
|
+
class UnorderedHash < Hash; end
|
|
14
|
+
|
|
15
|
+
# Raised by `Dms.decode*` on malformed source. Carries the (line,
|
|
16
|
+
# column) of the first offending byte plus the bare error text.
|
|
17
|
+
class DecodeError < StandardError
|
|
18
|
+
attr_reader :line, :column, :text
|
|
19
|
+
|
|
20
|
+
def initialize(line, column, message)
|
|
21
|
+
@line = line
|
|
22
|
+
@column = column
|
|
23
|
+
@text = message
|
|
24
|
+
super("#{line}:#{column}: #{message}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Deprecated alias. Use `Dms::DecodeError`. Referencing this constant
|
|
29
|
+
# emits a one-shot warning via `deprecate_constant`.
|
|
30
|
+
ParseError = DecodeError
|
|
31
|
+
deprecate_constant :ParseError
|
|
32
|
+
|
|
33
|
+
# Raised by `Dms.encode` (and `Dms.encode_lite`) when the value tree
|
|
34
|
+
# cannot be emitted — e.g. full-mode round-trip refusing a Document
|
|
35
|
+
# that contains a `Dms::UnorderedHash`. See SPEC §encode.
|
|
36
|
+
class EncodeError < StandardError; end
|
|
37
|
+
|
|
38
|
+
# Marker classes for datetime variants. The inner string is already
|
|
39
|
+
# spec-validated by the parser.
|
|
40
|
+
class LocalDate
|
|
41
|
+
attr_reader :value
|
|
42
|
+
def initialize(value); @value = value; end
|
|
43
|
+
def ==(o); o.is_a?(LocalDate) && @value == o.value; end
|
|
44
|
+
alias eql? ==
|
|
45
|
+
def hash; [LocalDate, @value].hash; end
|
|
46
|
+
def to_s; @value; end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class LocalTime
|
|
50
|
+
attr_reader :value
|
|
51
|
+
def initialize(value); @value = value; end
|
|
52
|
+
def ==(o); o.is_a?(LocalTime) && @value == o.value; end
|
|
53
|
+
alias eql? ==
|
|
54
|
+
def hash; [LocalTime, @value].hash; end
|
|
55
|
+
def to_s; @value; end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class LocalDateTime
|
|
59
|
+
attr_reader :value
|
|
60
|
+
def initialize(value); @value = value; end
|
|
61
|
+
def ==(o); o.is_a?(LocalDateTime) && @value == o.value; end
|
|
62
|
+
alias eql? ==
|
|
63
|
+
def hash; [LocalDateTime, @value].hash; end
|
|
64
|
+
def to_s; @value; end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class OffsetDateTime
|
|
68
|
+
attr_reader :value
|
|
69
|
+
def initialize(value); @value = value; end
|
|
70
|
+
def ==(o); o.is_a?(OffsetDateTime) && @value == o.value; end
|
|
71
|
+
alias eql? ==
|
|
72
|
+
def hash; [OffsetDateTime, @value].hash; end
|
|
73
|
+
def to_s; @value; end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A captured comment.
|
|
77
|
+
# `content` is raw source incl. delimiters.
|
|
78
|
+
# `kind` is :line or :block.
|
|
79
|
+
Comment = Struct.new(:content, :kind)
|
|
80
|
+
|
|
81
|
+
# An attached comment.
|
|
82
|
+
# `position` is :leading, :trailing, :floating, or :inner.
|
|
83
|
+
# `path` is an Array of String (key) / Integer (index).
|
|
84
|
+
AttachedComment = Struct.new(:comment, :position, :path)
|
|
85
|
+
|
|
86
|
+
# Round-trip original-form records (SPEC §to_dms).
|
|
87
|
+
HeredocModifierCall = Struct.new(:name, :args)
|
|
88
|
+
|
|
89
|
+
class StringForm
|
|
90
|
+
attr_reader :kind, :flavor, :label, :modifiers
|
|
91
|
+
def initialize(kind, flavor: nil, label: nil, modifiers: nil)
|
|
92
|
+
@kind = kind; @flavor = flavor; @label = label; @modifiers = modifiers
|
|
93
|
+
end
|
|
94
|
+
def self.basic; new(:basic); end
|
|
95
|
+
def self.literal; new(:literal); end
|
|
96
|
+
def self.heredoc(flavor, label, modifiers)
|
|
97
|
+
new(:heredoc, flavor: flavor, label: label, modifiers: modifiers.to_a)
|
|
98
|
+
end
|
|
99
|
+
def ==(o)
|
|
100
|
+
o.is_a?(StringForm) && @kind == o.kind && @flavor == o.flavor &&
|
|
101
|
+
@label == o.label && @modifiers == o.modifiers
|
|
102
|
+
end
|
|
103
|
+
alias eql? ==
|
|
104
|
+
def hash; [StringForm, @kind, @flavor, @label, @modifiers].hash; end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class OriginalLiteral
|
|
108
|
+
attr_reader :kind, :integer_lit, :string_form
|
|
109
|
+
def initialize(kind, integer_lit: nil, string_form: nil)
|
|
110
|
+
@kind = kind; @integer_lit = integer_lit; @string_form = string_form
|
|
111
|
+
end
|
|
112
|
+
def self.integer(lit); new(:integer, integer_lit: lit); end
|
|
113
|
+
def self.string(form); new(:string, string_form: form); end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parsed DMS document.
|
|
117
|
+
class Document
|
|
118
|
+
attr_accessor :meta, :body, :comments, :original_forms
|
|
119
|
+
|
|
120
|
+
def initialize(meta, body, comments = [], original_forms = [])
|
|
121
|
+
@meta = meta
|
|
122
|
+
@body = body
|
|
123
|
+
@comments = comments
|
|
124
|
+
@original_forms = original_forms
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def front_matter; @meta; end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/dms.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DMS decoder/encoder - Ruby port of the Rust/Python reference.
|
|
4
|
+
#
|
|
5
|
+
# Public API (SPEC v0.14):
|
|
6
|
+
# Dms.decode(src) -> body value
|
|
7
|
+
# Dms.decode_document(src) -> Document(body, meta, comments, original_forms)
|
|
8
|
+
# Dms.encode(doc) -> DMS source string (round-trip emitter)
|
|
9
|
+
# Dms.decode_lite(src) -> body value, lite mode
|
|
10
|
+
# Dms.encode_lite(doc) -> DMS source string, canonical-form emit
|
|
11
|
+
#
|
|
12
|
+
# Legacy aliases (deprecated, removed next release):
|
|
13
|
+
# Dms.parse / Dms.parse_document / Dms.parse_lite / Dms.to_dms /
|
|
14
|
+
# Dms.to_dms_lite — emit a one-shot deprecation warning and forward.
|
|
15
|
+
#
|
|
16
|
+
# Spec: https://gitlab.com/flo-labs/pub/dms/-/blob/main/SPEC.md
|
|
17
|
+
|
|
18
|
+
require "dms/types"
|
|
19
|
+
require "dms/parser"
|
|
20
|
+
require "dms/emitter"
|
|
21
|
+
require "dms/tier1"
|
|
22
|
+
|
|
23
|
+
module Dms
|
|
24
|
+
VERSION = "0.3.0"
|
|
25
|
+
|
|
26
|
+
# Capability flag — this port ships lite-mode decode + lite-mode encode_lite.
|
|
27
|
+
# See SPEC §Decoding modes — full and lite.
|
|
28
|
+
SUPPORTS_LITE_MODE = true
|
|
29
|
+
|
|
30
|
+
# Capability flag — this port ships unordered-table decode mode.
|
|
31
|
+
# See SPEC §Unordered tables.
|
|
32
|
+
SUPPORTS_IGNORE_ORDER = true
|
|
33
|
+
|
|
34
|
+
# ---------- Canonical decode/encode (SPEC v0.14) ----------
|
|
35
|
+
|
|
36
|
+
# Decode a DMS source string and return the body value tree.
|
|
37
|
+
def self.decode(src)
|
|
38
|
+
Parser.parse_document(src).body
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Decode a DMS source string and return the full Document
|
|
42
|
+
# (body, meta, comments, original_forms).
|
|
43
|
+
def self.decode_document(src)
|
|
44
|
+
Parser.parse_document(src)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Lite-mode decode: same data tree, no comment AST, no original_forms.
|
|
48
|
+
# Not suitable for `encode` round-trip. SPEC §Decoding modes — full and lite.
|
|
49
|
+
def self.decode_lite(src)
|
|
50
|
+
Parser.parse_lite_document(src).body
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.decode_lite_document(src)
|
|
54
|
+
Parser.parse_lite_document(src)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Unordered full-mode decode (SPEC §"Unordered tables"). Body tables are
|
|
58
|
+
# built as `Dms::UnorderedHash` (a Hash subclass) with keys shuffled at
|
|
59
|
+
# end-of-build so callers cannot rely on insertion order. Front matter
|
|
60
|
+
# remains insertion-ordered.
|
|
61
|
+
#
|
|
62
|
+
# `Dms.encode` (full-mode round-trip) refuses Documents containing
|
|
63
|
+
# unordered tables — use `Dms.encode_lite` for canonical emit.
|
|
64
|
+
def self.decode_document_unordered(src)
|
|
65
|
+
Parser.parse_document_unordered(src)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Unordered lite-mode decode. The fastest read-only path: no comment AST,
|
|
69
|
+
# no original_forms, body tables built as `Dms::UnorderedHash` with
|
|
70
|
+
# shuffled key order. SPEC §"Unordered tables".
|
|
71
|
+
def self.decode_lite_document_unordered(src)
|
|
72
|
+
Parser.parse_lite_document_unordered(src)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Front-matter-only decode (SPEC §Front-matter-only decode). Scans
|
|
76
|
+
# leading trivia + `+++` + contents + `+++` and stops; the body is
|
|
77
|
+
# not tokenized. Returns the front-matter Hash (empty when the FM
|
|
78
|
+
# block is empty), or nil when the document has no FM at all.
|
|
79
|
+
# In-FM diagnostics are byte-identical to a full decode and raise
|
|
80
|
+
# `Dms::DecodeError`. Required at tier 0 — there is no capability
|
|
81
|
+
# flag for this entry point.
|
|
82
|
+
def self.decode_front_matter(src)
|
|
83
|
+
Parser.parse_front_matter_only(src)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Encode a parsed Document back to DMS source (SPEC §encode). Refuses
|
|
87
|
+
# Documents containing `Dms::UnorderedHash` — use `encode_lite` for
|
|
88
|
+
# canonical emit of unordered Documents.
|
|
89
|
+
def self.encode(doc)
|
|
90
|
+
Emitter._encode_full(doc)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Canonical-form emit. Drops comments and original_forms. Accepts
|
|
94
|
+
# both full-mode and lite-mode Documents. SPEC §encode.
|
|
95
|
+
def self.encode_lite(doc)
|
|
96
|
+
Emitter._encode_lite(doc)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Capability flag (SPEC §"Unordered tables"). True iff this port ships
|
|
100
|
+
# `decode_document_unordered` / `decode_lite_document_unordered` with
|
|
101
|
+
# spec-correct semantics. Callers probe before opting in.
|
|
102
|
+
def self.supports_ignore_order
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ---------- Deprecated aliases (parse/to_dms era, SPEC < 0.14) ----------
|
|
107
|
+
#
|
|
108
|
+
# Removed in the next release. Keep dispatch trivial — they exist
|
|
109
|
+
# purely as a compatibility shim over the canonical names.
|
|
110
|
+
|
|
111
|
+
@_deprecation_warned = {}
|
|
112
|
+
|
|
113
|
+
def self._warn_deprecated(old, new_name)
|
|
114
|
+
return if @_deprecation_warned[old]
|
|
115
|
+
@_deprecation_warned[old] = true
|
|
116
|
+
warn "[DEPRECATION] `Dms.#{old}` is deprecated and will be removed " \
|
|
117
|
+
"in the next release. Use `Dms.#{new_name}` instead.",
|
|
118
|
+
uplevel: 2
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.parse(src)
|
|
122
|
+
_warn_deprecated(:parse, :decode)
|
|
123
|
+
decode(src)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.parse_document(src)
|
|
127
|
+
_warn_deprecated(:parse_document, :decode_document)
|
|
128
|
+
decode_document(src)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.parse_lite(src)
|
|
132
|
+
_warn_deprecated(:parse_lite, :decode_lite)
|
|
133
|
+
decode_lite(src)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.parse_lite_document(src)
|
|
137
|
+
_warn_deprecated(:parse_lite_document, :decode_lite_document)
|
|
138
|
+
decode_lite_document(src)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.parse_document_unordered(src)
|
|
142
|
+
_warn_deprecated(:parse_document_unordered, :decode_document_unordered)
|
|
143
|
+
decode_document_unordered(src)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.parse_lite_document_unordered(src)
|
|
147
|
+
_warn_deprecated(:parse_lite_document_unordered,
|
|
148
|
+
:decode_lite_document_unordered)
|
|
149
|
+
decode_lite_document_unordered(src)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.to_dms(doc)
|
|
153
|
+
_warn_deprecated(:to_dms, :encode)
|
|
154
|
+
encode(doc)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.to_dms_lite(doc)
|
|
158
|
+
_warn_deprecated(:to_dms_lite, :encode_lite)
|
|
159
|
+
encode_lite(doc)
|
|
160
|
+
end
|
|
161
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dms-parser
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Filip Lopes
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby parser for DMS, a data syntax with strong typing, ordered maps,
|
|
14
|
+
multi-line heredocs, and front-matter metadata.
|
|
15
|
+
email:
|
|
16
|
+
executables:
|
|
17
|
+
- dms-encoder
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE-APACHE
|
|
22
|
+
- LICENSE-MIT
|
|
23
|
+
- README.md
|
|
24
|
+
- bin/dms-encoder
|
|
25
|
+
- lib/dms.rb
|
|
26
|
+
- lib/dms/emitter.rb
|
|
27
|
+
- lib/dms/parser.rb
|
|
28
|
+
- lib/dms/tier1.rb
|
|
29
|
+
- lib/dms/types.rb
|
|
30
|
+
homepage: https://gitlab.com/flo-labs/pub/dms-rb
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT OR Apache-2.0
|
|
33
|
+
metadata:
|
|
34
|
+
source_code_uri: https://gitlab.com/flo-labs/pub/dms-rb
|
|
35
|
+
bug_tracker_uri: https://gitlab.com/flo-labs/pub/dms-rb/-/issues
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: 3.0.0
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.5.22
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: Ruby parser for DMS, a data syntax with strong typing, ordered maps, multi-line
|
|
55
|
+
heredocs, and front-matter metadata.
|
|
56
|
+
test_files: []
|