yeptris 0.1.13.3-x86_64-linux

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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ module Psych
5
+ # Minimal Psych::Coder stand-in (encode_with / init_with carry
6
+ # these): tag + one of scalar / seq / map. map is ordered.
7
+ class CoderShim
8
+ attr_reader :type, :tag, :scalar, :seq
9
+
10
+ def initialize(class_name = nil)
11
+ @type = :map
12
+ # Psych's default for encode_with objects: their own class tag
13
+ @tag = class_name ? "!ruby/object:#{class_name}" : nil
14
+ @scalar = nil
15
+ @seq = nil
16
+ @map = nil
17
+ @class_name = class_name
18
+ end
19
+
20
+ def tag=(t)
21
+ @tag = t
22
+ end
23
+
24
+ def scalar=(value)
25
+ @type = :scalar
26
+ @scalar = value
27
+ end
28
+
29
+ def seq=(list)
30
+ @type = :seq
31
+ @seq = list
32
+ end
33
+
34
+ def map=(hash)
35
+ @type = :map
36
+ @map = nil
37
+ hash&.each { |k, v| self[k] = v }
38
+ end
39
+
40
+ def []=(key, value)
41
+ @type = :map
42
+ @map ||= {}
43
+ @map[key.to_s] = value
44
+ end
45
+
46
+ def [](key)
47
+ @map&.[](key.to_s)
48
+ end
49
+
50
+ def each(&block)
51
+ @map&.each(&block)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO.restructure/23 — the typed opt-in marker for arbitrary-object
4
+ # dump/load. Classes include this module and implement
5
+ # encode_with(coder) / init_with(coder) (Psych's coder protocol).
6
+ # The visitors call those methods directly; the library never uses
7
+ # respond_to?, never reads or writes instance variables from outside
8
+ # the object's public surface (the encapsulation law).
9
+ module Yeptris
10
+ module Psych
11
+ module Encodable
12
+ end
13
+ end
14
+ end
@@ -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 = Yeptris.read_input(yaml)
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,383 @@
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, the Encodable protocol, 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
+ when Encodable then visit_encode_with(obj)
44
+ else
45
+ if obj.instance_of?(::Object)
46
+ # A BARE Object has no declared state to lose — the
47
+ # empty !ruby/object form is correct by construction
48
+ # (an Object with ivars set from outside its class is
49
+ # outside the encapsulation contract; use a real class
50
+ # with Encodable for stateful objects).
51
+ return visit_bare_object(obj)
52
+ end
53
+ refuse_dump(obj)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Psych anchors only objects that appear more than once;
60
+ # single-occurrence containers dump clean (no &o1 noise)
61
+ def anchor_for(obj)
62
+ oid = obj.object_id
63
+ name = @names[oid]
64
+ return [:alias, name] if name
65
+
66
+ if @refs[oid] < 2
67
+ return [:none, nil]
68
+ end
69
+ name = "o#{@counter += 1}"
70
+ @names[oid] = name
71
+ [:new, name]
72
+ end
73
+
74
+ def count_refs(obj, seen)
75
+ return if seen.key?(obj.object_id)
76
+
77
+ seen[obj.object_id] = true
78
+ case obj
79
+ when Hash
80
+ obj.each_value { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
81
+ when Array
82
+ obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
83
+ when Struct
84
+ obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
85
+ when Set
86
+ obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
87
+ when ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time, true, false, nil, ::Numeric
88
+ # immutables: identity anchors are meaningless
89
+ else
90
+ unless obj.instance_of?(::Object)
91
+ refuse_dump(obj) unless obj.is_a?(Encodable)
92
+ # Encodable: descend through the PUBLIC protocol so
93
+ # shared objects inside coder contents still get
94
+ # anchors — encode_with must be pure (it runs once more
95
+ # at dump).
96
+ coder = ::Yeptris::Psych::CoderShim.new(nil)
97
+ obj.encode_with(coder)
98
+ case coder.type
99
+ when :seq
100
+ coder.seq.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
101
+ when :map
102
+ coder.each { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def refuse_dump(obj)
109
+ raise ::Yeptris::DumpError,
110
+ "cannot dump #{obj.class}: include Yeptris::Psych::Encodable and implement encode_with(coder)"
111
+ end
112
+
113
+ def scalar(obj, tag: nil)
114
+ # strings route through the builder's plain-safety helper
115
+ # (one quoting rule, DRY); other scalars are their own text
116
+ text =
117
+ if obj.is_a?(::Date) || obj.is_a?(::Time)
118
+ obj.iso8601 # canonical timestamp form, not to_s
119
+ else
120
+ obj.nil? ? "null" : obj.to_s
121
+ end
122
+ n =
123
+ if obj.is_a?(::String)
124
+ ::Yeptris::YAML::Builder.build_string(@tree, obj)
125
+ else
126
+ @tree.new_scalar(text, :plain)
127
+ end
128
+ n.set_tag(tag) if tag
129
+ n
130
+ end
131
+
132
+ def visit_hash(hash)
133
+ state, name = anchor_for(hash)
134
+ return alias_of(hash, name) if state == :alias
135
+
136
+ m = @tree.new_mapping
137
+ remember(hash, m)
138
+ m.set_anchor(name) if name
139
+ hash.each { |k, v| m.map_add(key_text(k), visit(v)) }
140
+ m
141
+ end
142
+
143
+ def visit_array(ary)
144
+ state, name = anchor_for(ary)
145
+ return alias_of(ary, name) if state == :alias
146
+
147
+ s = @tree.new_sequence
148
+ remember(ary, s)
149
+ s.set_anchor(name) if name
150
+ ary.each { |e| s.seq_add(visit(e)) }
151
+ s
152
+ end
153
+
154
+ def visit_struct(strct)
155
+ state, name = anchor_for(strct)
156
+ return alias_of(strct, name) if state == :alias
157
+
158
+ m = @tree.new_mapping
159
+ remember(strct, m)
160
+ m.set_anchor(name) if name
161
+ m.set_tag(strct.class.name ? "!ruby/struct:#{strct.class.name}" : "!ruby/struct")
162
+ strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
163
+ m
164
+ end
165
+
166
+ def visit_set(set)
167
+ state, name = anchor_for(set)
168
+ return alias_of(set, name) if state == :alias
169
+
170
+ s = @tree.new_sequence
171
+ remember(set, s)
172
+ s.set_anchor(name) if name
173
+ s.set_tag("!ruby/set")
174
+ set.each { |e| s.seq_add(visit(e)) }
175
+ s
176
+ end
177
+
178
+ def visit_encode_with(obj)
179
+ state, name = anchor_for(obj)
180
+ return alias_of(obj, name) if state == :alias
181
+
182
+ coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
183
+ obj.encode_with(coder) # the typed public surface
184
+ # Wrap coder contents in a __init__ sub-mapping so the load
185
+ # side can dispatch back into obj.init_with(coder) without
186
+ # library-side ivar reflection (the encapsulation law).
187
+ init = @tree.new_mapping
188
+ case coder.type
189
+ when :scalar
190
+ init.map_add("__scalar__", scalar(coder.scalar))
191
+ init.map_add("__tag__", scalar(coder.tag)) if coder.tag
192
+ when :seq
193
+ inner = @tree.new_sequence
194
+ inner.set_tag(coder.tag) if coder.tag
195
+ coder.seq.each { |e| inner.seq_add(visit(e)) }
196
+ init.seq_add(inner)
197
+ else
198
+ coder.each { |k, v| init.map_add(key_text(k), visit(v)) }
199
+ init.set_tag(coder.tag) if coder.tag
200
+ end
201
+ m = @tree.new_mapping
202
+ m.set_anchor(name) if name
203
+ m.set_tag("!ruby/object:#{obj.class.name}")
204
+ m.map_add("__init__", init)
205
+ remember(obj, m)
206
+ m
207
+ end
208
+
209
+ def visit_bare_object(obj)
210
+ state, name = anchor_for(obj)
211
+ return alias_of(obj, name) if state == :alias
212
+
213
+ m = @tree.new_mapping
214
+ remember(obj, m)
215
+ m.set_anchor(name) if name
216
+ m.set_tag("!ruby/object")
217
+ m
218
+ end
219
+
220
+ def alias_of(obj, name)
221
+ @tree.new_alias(@nodes.fetch(obj.object_id), name)
222
+ end
223
+
224
+ def remember(obj, node)
225
+ (@nodes ||= {})[obj.object_id] = node
226
+ end
227
+
228
+ def key_text(k)
229
+ k.is_a?(Symbol) ? ":#{k}" : k.to_s
230
+ end
231
+
232
+ end
233
+
234
+ # Load side: revival of !ruby/... tags over the parsed Nodes
235
+ # tree. The Materializer stays the plain-data fast path.
236
+ class ToRuby
237
+ def self.visit(node)
238
+ new.visit(node)
239
+ end
240
+
241
+ def visit(node)
242
+ @root ||= node
243
+ case node
244
+ when ::Yeptris::Psych::Nodes::Alias then visit_alias(node)
245
+ when ::Yeptris::Psych::Nodes::Scalar then visit_scalar(node)
246
+ when ::Yeptris::Psych::Nodes::Sequence then visit_sequence(node)
247
+ when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
248
+ else node&.to_ruby
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ def anchors
255
+ @anchors ||= {}
256
+ end
257
+
258
+ def visit_alias(node)
259
+ name = node.anchor
260
+ unless anchors.key?(name)
261
+ target = find_anchored(root_container(node), name)
262
+ anchors[name] = target ? visit(target) : nil
263
+ end
264
+ anchors[name]
265
+ end
266
+
267
+ # the walk for alias targets starts at the first node this
268
+ # visitor was handed (the document's root)
269
+ def root_container(_node)
270
+ @root
271
+ end
272
+
273
+ def find_anchored(node, name)
274
+ return node if node.anchor == name
275
+
276
+ node.children.each do |c|
277
+ found = find_anchored(c, name)
278
+ return found if found
279
+ end
280
+ nil
281
+ end
282
+
283
+ def visit_scalar(node)
284
+ node.to_ruby
285
+ end
286
+
287
+ def visit_sequence(node)
288
+ if node.tag == "!ruby/set"
289
+ set = ::Set.new
290
+ anchors[node.anchor] = set if node.anchor
291
+ node.children.each { |c| set << visit(c) }
292
+ return set
293
+ end
294
+ ary = []
295
+ anchors[node.anchor] = ary if node.anchor
296
+ node.children.each { |c| ary << visit(c) }
297
+ ary
298
+ end
299
+
300
+ def visit_mapping(node)
301
+ tag = node.tag
302
+ return hash_into({}, node) if tag.nil? || tag.empty?
303
+
304
+ case tag
305
+ when "!ruby/set"
306
+ set = Set.new
307
+ anchors[node.anchor] = set if node.anchor
308
+ node.children.each_slice(2) { |_k, v| set << visit(v) }
309
+ set
310
+ when /\A!ruby\/struct(?::(.+))?\z/
311
+ klass = resolve_class(Regexp.last_match(1).to_s)
312
+ pairs = {}
313
+ anchors[node.anchor] = nil # placeholder: filled below
314
+ node.children.each_slice(2) do |k, v|
315
+ pairs[visit(k).to_s.sub(/\A:/, "")] = visit(v)
316
+ end
317
+ keys = pairs.keys.map(&:to_sym)
318
+ built =
319
+ if klass && klass <= Struct
320
+ klass.new(*pairs.values)
321
+ else
322
+ Struct.new(*keys).new(*pairs.values)
323
+ end
324
+ anchors[node.anchor] = built if node.anchor
325
+ built
326
+ when /\A!ruby\/object(?::(.+))?\z/
327
+ klass = resolve_class(Regexp.last_match(1).to_s)
328
+ revive_object(klass, node)
329
+ else
330
+ hash_into({}, node)
331
+ end
332
+ end
333
+
334
+ def hash_into(h, node)
335
+ anchors[node.anchor] = h if node.anchor
336
+ node.children.each_slice(2) do |k, v|
337
+ h[visit(k)] = visit(v)
338
+ end
339
+ h
340
+ end
341
+
342
+ def revive_object(klass, node)
343
+ # bare !ruby/object (no class): Psych.dump(Object.new)'s form.
344
+ # An Object has no declared state — allocation is the whole
345
+ # revival (no reflection, no protocol needed).
346
+ return ::Object.allocate if klass.nil?
347
+
348
+ raise ::Yeptris::DumpError, "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)" unless klass <= ::Yeptris::Psych::Encodable
349
+
350
+ obj = klass.allocate
351
+ anchors[node.anchor] = obj if node.anchor
352
+ # The class opts in via init_with(coder); the encoder wrote
353
+ # `coder["__init__"] = the mapping` so we can pass it back
354
+ # without library-side ivar reflection.
355
+ node.children.each_slice(2) do |k, v|
356
+ key = k.to_ruby.to_s
357
+ next unless key == "__init__"
358
+
359
+ coder = ::Yeptris::Psych::CoderShim.new
360
+ if v.is_a?(::Yeptris::Psych::Nodes::Mapping)
361
+ v.children.each_slice(2) do |ck, cv|
362
+ coder[ck.to_ruby.to_s] = visit(cv)
363
+ end
364
+ end
365
+ obj.init_with(coder)
366
+ end
367
+ obj
368
+ end
369
+
370
+ def resolve_class(name)
371
+ return nil if name.nil? || name.empty?
372
+
373
+ klass = ::Object.const_get(name)
374
+ raise DisallowedClass, name unless klass.is_a?(Class) || klass.is_a?(Module)
375
+
376
+ klass
377
+ rescue ::NameError
378
+ nil
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end