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.
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: []