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.
- checksums.yaml +7 -0
- data/README.adoc +141 -0
- data/ext/yeptris_native/extconf.rb +47 -0
- data/ext/yeptris_native/json_ruby.c +343 -0
- data/ext/yeptris_native/yeptris_native.c +440 -0
- data/lib/yeptris/document.rb +221 -0
- data/lib/yeptris/ffi.rb +256 -0
- data/lib/yeptris/json.rb +159 -0
- data/lib/yeptris/materializer.rb +347 -0
- data/lib/yeptris/native.so +0 -0
- data/lib/yeptris/node.rb +373 -0
- data/lib/yeptris/psych/coder_shim.rb +55 -0
- data/lib/yeptris/psych/drop_in.rb +23 -0
- data/lib/yeptris/psych/encodable.rb +14 -0
- data/lib/yeptris/psych/handler.rb +90 -0
- data/lib/yeptris/psych/parser.rb +105 -0
- data/lib/yeptris/psych/visitors.rb +383 -0
- data/lib/yeptris/psych.rb +368 -0
- data/lib/yeptris/valueml.rb +455 -0
- data/lib/yeptris/yaml.rb +289 -0
- data/lib/yeptris.rb +70 -0
- data/libyeptris.so +0 -0
- metadata +83 -0
|
@@ -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.
|