yeptris 0.1.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.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ module Psych
5
+ # The event-level API (TODO.impl/15 phase E): the abstract base
6
+ # Psych::Parser dispatches onto. Every event a YAML stream can
7
+ # produce is a method; the arg shapes are Psych's own (verified
8
+ # differentially against the stdlib recorder in the specs).
9
+ class Handler
10
+ # Scalar styles — the same integers Psych::Nodes::Scalar uses,
11
+ # and the same values YeptrisEventRecord.style carries.
12
+ ANY = 0
13
+ PLAIN = 1
14
+ SINGLE_QUOTED = 2
15
+ DOUBLE_QUOTED = 3
16
+ LITERAL = 4
17
+ FOLDED = 5
18
+
19
+ # Collection styles (Psych::Nodes::Sequence / Mapping).
20
+ BLOCK = 1
21
+ FLOW = 2
22
+
23
+ class DumperOptions
24
+ attr_accessor :line_width, :indentation, :canonical
25
+
26
+ def initialize
27
+ @line_width = 0
28
+ @indentation = 2
29
+ @canonical = false
30
+ end
31
+ end
32
+
33
+ OPTIONS = DumperOptions.new
34
+
35
+ EVENTS = %i[alias empty end_document end_mapping end_sequence end_stream
36
+ scalar start_document start_mapping start_sequence start_stream].freeze
37
+
38
+ # Called once per stream; +encoding+ is a Psych::Parser
39
+ # encoding constant (we always parse to UTF-8).
40
+ def start_stream(encoding); end
41
+
42
+ # +version+ is [major, minor] when %YAML declared, else nil
43
+ # (yeptris does not carry the directive into the event).
44
+ # +tag_directives+ is a list of [handle, prefix] pairs ([] for
45
+ # us: same directive omission). +implicit+ true without ---.
46
+ def start_document(version, tag_directives, implicit); end
47
+
48
+ def end_document(implicit); end
49
+
50
+ def alias(anchor); end
51
+
52
+ # +style+ is one of the scalar constants above; +plain+ /
53
+ # +quoted+ are the implicit-typing flags (plain: resolvable
54
+ # without a tag).
55
+ def scalar(value, anchor, tag, plain, quoted, style); end
56
+
57
+ def start_sequence(anchor, tag, implicit, style); end
58
+
59
+ def end_sequence; end
60
+
61
+ def start_mapping(anchor, tag, implicit, style); end
62
+
63
+ def end_mapping; end
64
+
65
+ def end_stream; end
66
+
67
+ # libyaml artifact (its NO_EVENT after stream end); the
68
+ # yeptris engine never produces one, so this never fires.
69
+ def empty; end
70
+ end
71
+
72
+ module Handlers
73
+ # Captures every event as [name, args] — Psych's own Recorder
74
+ # shape, so recordings replay through any Handler-compatible
75
+ # emitter.
76
+ class Recorder < Handler
77
+ attr_reader :events
78
+
79
+ def initialize
80
+ @events = []
81
+ super()
82
+ end
83
+
84
+ Handler::EVENTS.each do |event|
85
+ define_method(event) { |*args| @events << [event, args] }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ module Psych
5
+ # The event parser (TODO.impl/15 phase E): drives a Handler with
6
+ # Psych's exact event shapes over the yeptris recorder — one bulk
7
+ # record drain (the Materializer's seam), then a pure-Ruby
8
+ # dispatch loop. No callback marshaling, no per-event FFI calls.
9
+ class Parser
10
+ attr_accessor :handler
11
+ attr_reader :filename
12
+
13
+ ANY = 0
14
+ UTF8 = 1
15
+ UTF16LE = 2
16
+ UTF16BE = 3
17
+
18
+ def initialize(handler = Handler.new)
19
+ @handler = handler
20
+ @filename = nil
21
+ end
22
+
23
+ def parse(yaml, filename = nil)
24
+ yaml = yaml.read if yaml.respond_to?(:read)
25
+ yaml = yaml.to_s
26
+ @filename = filename
27
+ flat, arena = Materializer.drain(yaml, schema: :compat_11)
28
+ dispatch(flat, arena)
29
+ self
30
+ rescue ParseError => e
31
+ raise SyntaxError, e.message
32
+ end
33
+
34
+ private
35
+
36
+ STREAM_START = Materializer::STREAM_START
37
+ STREAM_END = Materializer::STREAM_END
38
+ DOCUMENT_START = Materializer::DOCUMENT_START
39
+ DOCUMENT_END = Materializer::DOCUMENT_END
40
+ SEQUENCE_START = Materializer::SEQUENCE_START
41
+ MAPPING_START = Materializer::MAPPING_START
42
+ SCALAR = Materializer::SCALAR
43
+ ALIAS = Materializer::ALIAS
44
+
45
+ EF_FLOW = 1 << 0
46
+ EF_EXPLICIT = 1 << 1
47
+ STYLE_PLAIN = 1
48
+ STYLE_SINGLE_QUOTED = 2
49
+ STYLE_DOUBLE_QUOTED = 3
50
+
51
+ def dispatch(flat, arena)
52
+ h = @handler
53
+ i = 0
54
+ while i < flat.length
55
+ type = flat[i]
56
+ style = flat[i + 1]
57
+ flags = flat[i + 2]
58
+ value = field(flat, arena, i + 6)
59
+ anchor = field(flat, arena, i + 8)
60
+ tag = field(flat, arena, i + 10)
61
+ case type
62
+ when STREAM_START then h.start_stream(UTF8)
63
+ when STREAM_END then h.end_stream
64
+ # version: [] when no %YAML directive (Psych's own shape);
65
+ # a declared directive's [major, minor] is not carried by
66
+ # the record — the one documented divergence
67
+ when DOCUMENT_START then h.start_document([], [], (flags & EF_EXPLICIT).zero?)
68
+ when DOCUMENT_END then h.end_document((flags & EF_EXPLICIT).zero?)
69
+ when SEQUENCE_START
70
+ h.start_sequence(anchor, tag, tag.nil?, (flags & EF_FLOW).zero? ? Handler::BLOCK : Handler::FLOW)
71
+ when Materializer::SEQUENCE_END then h.end_sequence
72
+ when MAPPING_START
73
+ h.start_mapping(anchor, tag, tag.nil?, (flags & EF_FLOW).zero? ? Handler::BLOCK : Handler::FLOW)
74
+ when Materializer::MAPPING_END then h.end_mapping
75
+ when SCALAR
76
+ # A synthesized empty scalar (empty document, '?' empty
77
+ # key) carries style ANY(0); Psych's C emits plain(1)
78
+ # with an empty String value — normalized here, at the
79
+ # boundary, so the emitter's style chooser is untouched
80
+ if style.zero?
81
+ style = STYLE_PLAIN
82
+ end
83
+ value = +"" if value.nil?
84
+ # libyaml's plain_implicit: a plain scalar that carried
85
+ # no explicit tag (an explicit tag kills implicit typing)
86
+ plain = style == STYLE_PLAIN && tag.nil?
87
+ # quoted_implicit covers every explicit style — single,
88
+ # double, literal, folded — when the tag was absent
89
+ quoted = !plain && tag.nil? && style > STYLE_PLAIN
90
+ h.scalar(value, anchor, tag, plain, quoted, style)
91
+ when ALIAS then h.alias(value)
92
+ end
93
+ i += Materializer::FIELDS
94
+ end
95
+ end
96
+
97
+ def field(flat, arena, at)
98
+ len = flat[at + 1]
99
+ return nil if len.zero?
100
+
101
+ arena.byteslice(flat[at], len)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require "set"
6
+
7
+ module Yeptris
8
+ module Psych
9
+ module Visitors
10
+ # The dump-side visitor (Psych's YAMLTree core): an arbitrary
11
+ # Ruby object graph becomes DOM nodes — anchors for shared
12
+ # objects, aliases for repeats, encode_with support, and the
13
+ # !ruby/... tags Psych's own emitter produces.
14
+ class YAMLTree
15
+ def initialize
16
+ @tree = ::Yeptris::Document.create
17
+ @names = {} # object_id -> anchor name
18
+ @counter = 0
19
+ end
20
+
21
+ def push(obj)
22
+ @refs = Hash.new(0)
23
+ @refs[obj.object_id] += 1 # the root counts as a reference
24
+ count_refs(obj, {})
25
+ root = visit(obj)
26
+ @tree.set_root(root) if root
27
+ self
28
+ end
29
+
30
+ def finish
31
+ @tree.serialize
32
+ end
33
+
34
+ def visit(obj)
35
+ case obj
36
+ when nil, true, false, Integer, Float, String then scalar(obj)
37
+ when Symbol then scalar(":#{obj}")
38
+ when Date, Time then scalar(obj) # timestamp text, never ivars
39
+ when Hash then visit_hash(obj)
40
+ when Array then visit_array(obj)
41
+ when Struct then visit_struct(obj)
42
+ when Set then visit_set(obj)
43
+ else
44
+ if obj.respond_to?(:encode_with)
45
+ visit_encode_with(obj)
46
+ else
47
+ visit_object(obj)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Psych anchors only objects that appear more than once;
55
+ # single-occurrence containers dump clean (no &o1 noise)
56
+ def anchor_for(obj)
57
+ oid = obj.object_id
58
+ name = @names[oid]
59
+ return [:alias, name] if name
60
+
61
+ if @refs[oid] < 2
62
+ return [:none, nil]
63
+ end
64
+ name = "o#{@counter += 1}"
65
+ @names[oid] = name
66
+ [:new, name]
67
+ end
68
+
69
+ def count_refs(obj, seen)
70
+ return if seen.key?(obj.object_id)
71
+
72
+ seen[obj.object_id] = true
73
+ case obj
74
+ when Hash
75
+ obj.each_value { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
76
+ when Array
77
+ obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
78
+ when Struct
79
+ obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
80
+ when Set
81
+ obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
82
+ when String, Integer, Float, Symbol, Date, Time, true, false, nil, Numeric
83
+ # immutables: identity anchors are meaningless
84
+ else
85
+ if obj.respond_to?(:encode_with)
86
+ # coder contents are opaque here; the object itself may
87
+ # repeat — count it from the caller side only
88
+ else
89
+ obj.instance_variables.each do |iv|
90
+ v = obj.instance_variable_get(iv)
91
+ @refs[v.object_id] += 1
92
+ count_refs(v, seen)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def scalar(obj, tag: nil)
99
+ # strings route through the builder's plain-safety helper
100
+ # (one quoting rule, DRY); other scalars are their own text
101
+ text =
102
+ if obj.is_a?(Date) || obj.is_a?(Time)
103
+ obj.iso8601 # canonical timestamp form, not to_s
104
+ else
105
+ obj.nil? ? "null" : obj.to_s
106
+ end
107
+ n =
108
+ if obj.is_a?(String)
109
+ ::Yeptris::YAML::Builder.build_string(@tree, obj)
110
+ else
111
+ @tree.new_scalar(text, :plain)
112
+ end
113
+ n.set_tag(tag) if tag
114
+ n
115
+ end
116
+
117
+ def visit_hash(hash)
118
+ state, name = anchor_for(hash)
119
+ return alias_of(hash, name) if state == :alias
120
+
121
+ m = @tree.new_mapping
122
+ remember(hash, m)
123
+ m.set_anchor(name) if name
124
+ hash.each { |k, v| m.map_add(key_text(k), visit(v)) }
125
+ m
126
+ end
127
+
128
+ def visit_array(ary)
129
+ state, name = anchor_for(ary)
130
+ return alias_of(ary, name) if state == :alias
131
+
132
+ s = @tree.new_sequence
133
+ remember(ary, s)
134
+ s.set_anchor(name) if name
135
+ ary.each { |e| s.seq_add(visit(e)) }
136
+ s
137
+ end
138
+
139
+ def visit_struct(strct)
140
+ state, name = anchor_for(strct)
141
+ return alias_of(strct, name) if state == :alias
142
+
143
+ m = @tree.new_mapping
144
+ remember(strct, m)
145
+ m.set_anchor(name) if name
146
+ m.set_tag(strct.class.name ? "!ruby/struct:#{strct.class.name}" : "!ruby/struct")
147
+ strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
148
+ m
149
+ end
150
+
151
+ def visit_set(set)
152
+ state, name = anchor_for(set)
153
+ return alias_of(set, name) if state == :alias
154
+
155
+ s = @tree.new_sequence
156
+ remember(set, s)
157
+ s.set_anchor(name) if name
158
+ s.set_tag("!ruby/set")
159
+ set.each { |e| s.seq_add(visit(e)) }
160
+ s
161
+ end
162
+
163
+ def visit_encode_with(obj)
164
+ state, name = anchor_for(obj)
165
+ return alias_of(obj, name) if state == :alias
166
+
167
+ coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
168
+ obj.encode_with(coder)
169
+ node =
170
+ case coder.type
171
+ when :scalar then scalar(coder.scalar, tag: coder.tag)
172
+ when :seq
173
+ s = @tree.new_sequence
174
+ s.set_tag(coder.tag) if coder.tag
175
+ coder.seq.each { |e| s.seq_add(visit(e)) }
176
+ s
177
+ else
178
+ m = @tree.new_mapping
179
+ m.set_tag(coder.tag) if coder.tag
180
+ coder.each { |k, v| m.map_add(key_text(k), visit(v)) }
181
+ m
182
+ end
183
+ remember(obj, node)
184
+ node.set_anchor(name) if name
185
+ node
186
+ end
187
+
188
+ def visit_object(obj)
189
+ state, name = anchor_for(obj)
190
+ return alias_of(obj, name) if state == :alias
191
+
192
+ m = @tree.new_mapping
193
+ remember(obj, m)
194
+ m.set_anchor(name) if name
195
+ m.set_tag("!ruby/object:#{obj.class.name}")
196
+ obj.instance_variables.each do |ivar|
197
+ m.map_add(ivar.to_s, visit(obj.instance_variable_get(ivar)))
198
+ end
199
+ m
200
+ end
201
+
202
+ def alias_of(obj, name)
203
+ @tree.new_alias(@nodes.fetch(obj.object_id), name)
204
+ end
205
+
206
+ def remember(obj, node)
207
+ (@nodes ||= {})[obj.object_id] = node
208
+ end
209
+
210
+ def key_text(k)
211
+ k.is_a?(Symbol) ? ":#{k}" : k.to_s
212
+ end
213
+
214
+ end
215
+
216
+ # Load side: revival of !ruby/... tags over the parsed Nodes
217
+ # tree. The Materializer stays the plain-data fast path.
218
+ class ToRuby
219
+ def self.visit(node)
220
+ new.visit(node)
221
+ end
222
+
223
+ def visit(node)
224
+ @root ||= node
225
+ case node
226
+ when ::Yeptris::Psych::Nodes::Alias then visit_alias(node)
227
+ when ::Yeptris::Psych::Nodes::Scalar then visit_scalar(node)
228
+ when ::Yeptris::Psych::Nodes::Sequence then visit_sequence(node)
229
+ when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
230
+ else node&.to_ruby
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def anchors
237
+ @anchors ||= {}
238
+ end
239
+
240
+ def visit_alias(node)
241
+ name = node.anchor
242
+ unless anchors.key?(name)
243
+ target = find_anchored(root_container(node), name)
244
+ anchors[name] = target ? visit(target) : nil
245
+ end
246
+ anchors[name]
247
+ end
248
+
249
+ # the walk for alias targets starts at the first node this
250
+ # visitor was handed (the document's root)
251
+ def root_container(_node)
252
+ @root
253
+ end
254
+
255
+ def find_anchored(node, name)
256
+ return node if node.respond_to?(:anchor) && node.anchor == name
257
+
258
+ node.children.each do |c|
259
+ found = find_anchored(c, name)
260
+ return found if found
261
+ end
262
+ nil
263
+ end
264
+
265
+ def visit_scalar(node)
266
+ node.to_ruby
267
+ end
268
+
269
+ def visit_sequence(node)
270
+ if node.tag == "!ruby/set"
271
+ set = ::Set.new
272
+ anchors[node.anchor] = set if node.anchor
273
+ node.children.each { |c| set << visit(c) }
274
+ return set
275
+ end
276
+ ary = []
277
+ anchors[node.anchor] = ary if node.anchor
278
+ node.children.each { |c| ary << visit(c) }
279
+ ary
280
+ end
281
+
282
+ def visit_mapping(node)
283
+ tag = node.tag
284
+ return hash_into({}, node) if tag.nil? || tag.empty?
285
+
286
+ case tag
287
+ when "!ruby/set"
288
+ set = Set.new
289
+ anchors[node.anchor] = set if node.anchor
290
+ node.children.each_slice(2) { |_k, v| set << visit(v) }
291
+ set
292
+ when /\A!ruby\/struct(?::(.+))?\z/
293
+ klass = resolve_class(Regexp.last_match(1).to_s)
294
+ pairs = {}
295
+ anchors[node.anchor] = nil # placeholder: filled below
296
+ node.children.each_slice(2) do |k, v|
297
+ pairs[visit(k).to_s.sub(/\A:/, "")] = visit(v)
298
+ end
299
+ keys = pairs.keys.map(&:to_sym)
300
+ built =
301
+ if klass && klass <= Struct
302
+ klass.new(*pairs.values)
303
+ else
304
+ Struct.new(*keys).new(*pairs.values)
305
+ end
306
+ anchors[node.anchor] = built if node.anchor
307
+ built
308
+ when /\A!ruby\/object(?::(.+))?\z/
309
+ klass = resolve_class(Regexp.last_match(1).to_s)
310
+ revive_object(klass, node)
311
+ else
312
+ hash_into({}, node)
313
+ end
314
+ end
315
+
316
+ def hash_into(h, node)
317
+ anchors[node.anchor] = h if node.anchor
318
+ node.children.each_slice(2) do |k, v|
319
+ h[visit(k)] = visit(v)
320
+ end
321
+ h
322
+ end
323
+
324
+ def revive_object(klass, node)
325
+ obj = klass ? klass.allocate : ::Object.new
326
+ anchors[node.anchor] = obj if node.anchor
327
+ node.children.each_slice(2) do |k, v|
328
+ key = k.to_ruby.to_s
329
+ if key == "__init__"
330
+ init_with(obj, v)
331
+ else
332
+ ivar = key.start_with?("@") ? key.to_sym : "@#{key}".to_sym
333
+ obj.instance_variable_set(ivar, visit(v))
334
+ end
335
+ end
336
+ obj
337
+ end
338
+
339
+ def init_with(obj, value_node)
340
+ return unless obj.respond_to?(:init_with)
341
+
342
+ coder = ::Yeptris::Psych::CoderShim.new
343
+ if value_node.is_a?(::Yeptris::Psych::Nodes::Mapping)
344
+ value_node.children.each_slice(2) do |k, v|
345
+ coder[k.to_ruby.to_s] = visit(v)
346
+ end
347
+ end
348
+ obj.init_with(coder)
349
+ end
350
+
351
+ def resolve_class(name)
352
+ return nil if name.nil? || name.empty?
353
+
354
+ klass = ::Object.const_get(name)
355
+ raise DisallowedClass, name unless klass.is_a?(Class) || klass.is_a?(Module)
356
+
357
+ klass
358
+ rescue ::NameError
359
+ nil
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end