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,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require "set"
6
+
7
+ # The Psych drop-in namespace (TODO.impl/15 phase C).
8
+ #
9
+ # `require "yeptris/psych"` rebinds the top-level Psych constant to
10
+ # this module (the original, if any, stays reachable as
11
+ # ::Psych::ORIGINAL). Semantics follow the Psych suite: load is
12
+ # SAFE by default (Psych 5 behavior — plain data only; anything
13
+ # tagged raises), unsafe_load materializes everything the yeptris
14
+ # loader understands, and parse returns the Nodes tree over the
15
+ # document without materializing.
16
+ module Yeptris
17
+ module Psych
18
+ # Children load via autoload declared HERE — the immediate parent
19
+ # namespace's file (never internal requires).
20
+ autoload :Handler, "yeptris/psych/handler"
21
+ # Handlers (the Recorder submodule) lives in handler.rb too — its
22
+ # own entry so referencing Psych::Handlers triggers the load
23
+ autoload :Handlers, "yeptris/psych/handler"
24
+ autoload :Parser, "yeptris/psych/parser"
25
+ autoload :CoderShim, "yeptris/psych/coder_shim"
26
+ autoload :Visitors, "yeptris/psych/visitors"
27
+ # The typed opt-in marker for arbitrary-object dump/load
28
+ # (TODO.restructure/23). Eager by intent: classes include it at
29
+ # declaration time, so the autoload must resolve before any
30
+ # object instance exists.
31
+ autoload :Encodable, "yeptris/psych/encodable"
32
+ class Error < StandardError; end
33
+ class SyntaxError < Error
34
+ attr_reader :line, :column
35
+
36
+ def initialize(message, line = 0, column = 0)
37
+ super(message)
38
+ @line = line
39
+ @column = column
40
+ end
41
+ end
42
+ class BadAlias < Error; end
43
+ class DisallowedClass < Error
44
+ attr_reader :name
45
+
46
+ def initialize(name)
47
+ super("Tried to load unspecified class: #{name}")
48
+ @name = name
49
+ end
50
+ end
51
+ class AliasNotEnabled < Error; end
52
+
53
+ class << self
54
+ # Psych 5: load is safe — plain data structures only. Tagged
55
+ # nodes raise DisallowedClass unless their class is permitted
56
+ # (Date/Time/Symbol are built in; they are plain data here).
57
+ def load(yaml, permitted_classes: [], aliases: false, **)
58
+ safe_load(yaml, permitted_classes: permitted_classes, aliases: aliases)
59
+ end
60
+
61
+ # Full revival over the Nodes tree: !ruby/object, !ruby/struct,
62
+ # !ruby/set, encode_with/init_with, alias identity. Plain-data
63
+ # loads should use load/safe_load (the Materializer fast path).
64
+ def unsafe_load(yaml, **)
65
+ tree = parse(yaml)
66
+ return nil if tree.nil?
67
+
68
+ Visitors::ToRuby.visit(tree.children.first)
69
+ end
70
+
71
+ def safe_load(yaml, permitted_classes: [], aliases: false, **)
72
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
73
+ begin
74
+ walk_safe(doc.root(0), permitted_classes, aliases)
75
+ ensure
76
+ doc.free
77
+ end
78
+ end
79
+
80
+ # The first document's node tree (no Ruby materialization).
81
+ def parse(yaml)
82
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
83
+ return nil if doc.document_count.zero?
84
+
85
+ Nodes::Builder.document(doc)
86
+ rescue Yeptris::ParseError => e
87
+ raise SyntaxError, e.message
88
+ end
89
+
90
+ def parse_stream(yaml)
91
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
92
+ return nil if doc.document_count.zero?
93
+
94
+ stream = Nodes::Stream.new
95
+ (0...doc.document_count).each do |i|
96
+ stream.children << Nodes::Builder.document_stream_child(doc, i)
97
+ end
98
+ stream.owner = doc
99
+ ObjectSpace.define_finalizer(
100
+ stream, proc { doc.free unless doc.freed? }
101
+ )
102
+ stream
103
+ rescue Yeptris::ParseError => e
104
+ raise SyntaxError, e.message
105
+ ensure
106
+ doc&.free if doc && !stream
107
+ end
108
+
109
+ # Arbitrary object graphs through the YAMLTree visitor
110
+ # (anchors, !ruby/ tags); plain data rides the fast builder.
111
+ def dump(obj, io = nil)
112
+ # scalars take the fast builder; EVERYTHING else (including
113
+ # plain containers — they may nest custom objects) goes
114
+ # through the visitor, which builds the same DOM for plain
115
+ # data anyway
116
+ out =
117
+ case obj
118
+ when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
119
+ Yeptris::YAML.dump(obj)
120
+ else
121
+ Visitors::YAMLTree.new.push(obj).finish
122
+ end
123
+ return out unless io
124
+
125
+ io.write(out)
126
+ io
127
+ end
128
+
129
+ private
130
+
131
+ # yeptris materializes plain data only — there is nothing
132
+ # unsafe it COULD load. The safety walk enforces what Psych
133
+ # enforces on such documents: aliases need opt-in, and explicit
134
+ # non-core tags raise DisallowedClass (the only "classes" the
135
+ # loader can produce are core-schema types, all permitted).
136
+ def walk_safe(root, permitted, aliases_enabled)
137
+ check(root, aliases_enabled) if root
138
+ root.to_ruby
139
+ end
140
+
141
+ def check(node, aliases_enabled)
142
+ case node.kind
143
+ when :alias
144
+ raise AliasNotEnabled, "Unknown alias" unless aliases_enabled
145
+ when :scalar, :mapping, :sequence
146
+ tag = node.tag
147
+ unless tag.nil?
148
+ name = tag.split(":").last
149
+ unless %w[str int float bool null timestamp seq map merge value
150
+ binary].include?(name)
151
+ raise DisallowedClass, name
152
+ end
153
+ end
154
+ case node.kind
155
+ when :mapping
156
+ node.each_pair do |k, v|
157
+ check(k, aliases_enabled)
158
+ check(v, aliases_enabled)
159
+ end
160
+ when :sequence
161
+ node.each { |e| check(e, aliases_enabled) }
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Psych::Nodes over the yeptris document: the tree IS the parsed
168
+ # document (children are node handles, not copies) — parse cost
169
+ # is the parse, and to_ruby reuses the Materializer.
170
+ module Nodes
171
+ class Node
172
+ include Enumerable
173
+
174
+ attr_reader :children
175
+ attr_reader :handle # @api private — the Yeptris::Node
176
+ # @api private — the tree builder attaches handles; a writer,
177
+ # never instance_variable_set from outside
178
+ attr_writer :handle
179
+ # The Document owning this tree's C memory; a plain writer —
180
+ # never instance_variable_set from outside (encapsulation law).
181
+ attr_accessor :owner
182
+
183
+ def initialize(handle = nil, children = [])
184
+ @handle = handle
185
+ @children = children
186
+ end
187
+
188
+ # Every node HAS an anchor concept (none by default) — the
189
+ # anchored search needs no type probe, just the model.
190
+ def anchor
191
+ nil
192
+ end
193
+
194
+ def each(&block)
195
+ @children.each(&block)
196
+ end
197
+
198
+ # The Ruby object for this subtree (Materializer semantics).
199
+ def to_ruby
200
+ @handle.to_ruby
201
+ end
202
+ end
203
+
204
+ class Stream < Node
205
+ def free
206
+ @owner&.free
207
+ end
208
+ end
209
+
210
+ # Owns the underlying Yeptris::Document: node handles in the
211
+ # tree stay valid while the tree is reachable; a GC finalizer
212
+ # releases the C memory when it is not.
213
+ class Document < Node
214
+ attr_reader :version, :tags
215
+
216
+ def initialize(version = [], tags = {})
217
+ super(nil)
218
+ @version = version
219
+ @tags = tags
220
+ end
221
+
222
+ # @api private — transfer of C ownership to this tree
223
+ def own(yeptris_doc)
224
+ @owner = yeptris_doc
225
+ ObjectSpace.define_finalizer(
226
+ self, proc { yeptris_doc.free unless yeptris_doc.freed? }
227
+ )
228
+ self
229
+ end
230
+
231
+ def free
232
+ @owner&.free
233
+ end
234
+ end
235
+
236
+ class Scalar < Node
237
+ attr_reader :value, :tag, :anchor, :plain, :quoted, :style
238
+
239
+ def initialize(value = nil, anchor: nil, tag: nil, plain: true,
240
+ quoted: false, style: :plain)
241
+ super(nil)
242
+ @value = value
243
+ @anchor = anchor
244
+ @tag = tag
245
+ @plain = plain
246
+ @quoted = quoted
247
+ @style = style
248
+ end
249
+ end
250
+
251
+ class Sequence < Node
252
+ attr_reader :anchor, :tag, :style
253
+
254
+ def initialize(anchor: nil, tag: nil, style: :block)
255
+ super(nil, [])
256
+ @anchor = anchor
257
+ @tag = tag
258
+ @style = style
259
+ end
260
+ end
261
+
262
+ class Mapping < Node
263
+ attr_reader :anchor, :tag, :style
264
+
265
+ def initialize(anchor: nil, tag: nil, style: :block)
266
+ super(nil, [])
267
+ @anchor = anchor
268
+ @tag = tag
269
+ @style = style
270
+ end
271
+ end
272
+
273
+ class Alias < Node
274
+ attr_reader :anchor
275
+
276
+ def initialize(anchor)
277
+ super(nil)
278
+ @anchor = anchor
279
+ end
280
+ end
281
+
282
+ # Builds the Nodes tree from a parsed document.
283
+ module Builder
284
+ module_function
285
+
286
+ # parse(): one document, owning the yeptris document
287
+ def document(doc, index = 0)
288
+ document_stream_child(doc, index).own(doc)
289
+ end
290
+
291
+ # parse_stream(): a child document sharing one owner (the
292
+ # stream owns the yeptris document)
293
+ def document_stream_child(doc, index)
294
+ root = doc.root(index)
295
+ d = Document.new
296
+ d.handle = root&.document
297
+ d.children << node(root) if root
298
+ d
299
+ end
300
+
301
+ def node(n)
302
+ case n.kind
303
+ when :mapping
304
+ m = Mapping.new(anchor: n.anchor, tag: n.tag)
305
+ n.each_pair do |k, v|
306
+ m.children << node(k)
307
+ m.children << node(v)
308
+ end
309
+ m.handle = n
310
+ m
311
+ when :sequence
312
+ s = Sequence.new(anchor: n.anchor, tag: n.tag)
313
+ n.each { |e| s.children << node(e) }
314
+ s.handle = n
315
+ s
316
+ when :alias
317
+ a = Alias.new(n.value)
318
+ a.handle = n
319
+ a
320
+ else
321
+ sc = Scalar.new(n.value, anchor: n.anchor, tag: n.tag,
322
+ plain: n.style == :plain, style: n.style)
323
+ sc.handle = n
324
+ sc
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ # The drop-in: rebind the top-level constant ( Psych-stdlib, if
333
+ # already loaded, stays reachable as Yeptris::Psych::ORIGINAL).
334
+ if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych) &&
335
+ !Yeptris::Psych.const_defined?(:ORIGINAL, false)
336
+ Yeptris::Psych.const_set(:ORIGINAL, ::Psych)
337
+ end
338
+ # class_eval reaches Module-private methods WITHOUT send (the law:
339
+ # no send to private methods); remove_const has no public form, and
340
+ # the rebind is this namespace's whole purpose
341
+ Object.class_eval { remove_const(:Psych) } if defined?(::Psych)
342
+ ::Psych = Yeptris::Psych