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