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
data/lib/yeptris/node.rb
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
# A node inside a document. Borrowed C memory: every call routes
|
|
7
|
+
# through the owning document's liveness check — use after free
|
|
8
|
+
# raises Yeptris::FreedError, never a segfault.
|
|
9
|
+
class Yeptris::Node
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
attr_reader :c_ptr
|
|
13
|
+
|
|
14
|
+
# @api private — always constructed via Document#wrap_node (identity).
|
|
15
|
+
def initialize(c_ptr, document)
|
|
16
|
+
@c_ptr = c_ptr
|
|
17
|
+
@document = document
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def document
|
|
21
|
+
@document
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
KINDS = {
|
|
25
|
+
Yeptris::FFI::NODE_SCALAR => :scalar,
|
|
26
|
+
Yeptris::FFI::NODE_SEQUENCE => :sequence,
|
|
27
|
+
Yeptris::FFI::NODE_MAPPING => :mapping,
|
|
28
|
+
Yeptris::FFI::NODE_ALIAS => :alias,
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def kind
|
|
32
|
+
KINDS[alive { Yeptris::FFI.yeptris_node_kind(@c_ptr) }]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def scalar?
|
|
36
|
+
kind == :scalar
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sequence?
|
|
40
|
+
kind == :sequence
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mapping?
|
|
44
|
+
kind == :mapping
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def alias?
|
|
48
|
+
kind == :alias
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scalar content / alias name (UTF-8 String), nil for collections.
|
|
52
|
+
def value
|
|
53
|
+
len = ::FFI::MemoryPointer.new(:uint64)
|
|
54
|
+
ptr = alive { Yeptris::FFI.yeptris_node_value(@c_ptr, len) }
|
|
55
|
+
return nil if ptr.null?
|
|
56
|
+
|
|
57
|
+
ptr.read_bytes(len.read_uint64).force_encoding(Encoding::UTF_8)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
STYLES = {
|
|
61
|
+
Yeptris::FFI::STYLE_PLAIN => :plain,
|
|
62
|
+
Yeptris::FFI::STYLE_SINGLE_QUOTED => :single_quoted,
|
|
63
|
+
Yeptris::FFI::STYLE_DOUBLE_QUOTED => :double_quoted,
|
|
64
|
+
Yeptris::FFI::STYLE_LITERAL => :literal,
|
|
65
|
+
Yeptris::FFI::STYLE_FOLDED => :folded,
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
def style
|
|
69
|
+
STYLES[alive { Yeptris::FFI.yeptris_node_style(@c_ptr) }]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Explicit tag URI when present, else nil.
|
|
73
|
+
def tag
|
|
74
|
+
len = ::FFI::MemoryPointer.new(:uint64)
|
|
75
|
+
ptr = alive { Yeptris::FFI.yeptris_node_tag(@c_ptr, len) }
|
|
76
|
+
return nil if ptr.null?
|
|
77
|
+
|
|
78
|
+
ptr.read_bytes(len.read_uint64).force_encoding(Encoding::UTF_8)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def anchor
|
|
82
|
+
len = ::FFI::MemoryPointer.new(:uint64)
|
|
83
|
+
ptr = alive { Yeptris::FFI.yeptris_node_anchor(@c_ptr, len) }
|
|
84
|
+
return nil if ptr.null?
|
|
85
|
+
|
|
86
|
+
ptr.read_bytes(len.read_uint64).force_encoding(Encoding::UTF_8)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def alias_target
|
|
90
|
+
t = alive { Yeptris::FFI.yeptris_node_alias_target(@c_ptr) }
|
|
91
|
+
@document.wrap_node(t)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ---- typed scalar reads (tag id decides eligibility) ----
|
|
95
|
+
|
|
96
|
+
def to_i
|
|
97
|
+
out = ::FFI::MemoryPointer.new(:int64)
|
|
98
|
+
status = alive { Yeptris::FFI.yeptris_node_int(@c_ptr, out) }
|
|
99
|
+
Yeptris::FFI.check_status(status, "yeptris_node_int")
|
|
100
|
+
out.read_int64
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_f
|
|
104
|
+
out = ::FFI::MemoryPointer.new(:double)
|
|
105
|
+
status = alive { Yeptris::FFI.yeptris_node_float(@c_ptr, out) }
|
|
106
|
+
Yeptris::FFI.check_status(status, "yeptris_node_float")
|
|
107
|
+
out.read_double
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_bool
|
|
111
|
+
out = ::FFI::MemoryPointer.new(:int)
|
|
112
|
+
status = alive { Yeptris::FFI.yeptris_node_bool(@c_ptr, out) }
|
|
113
|
+
Yeptris::FFI.check_status(status, "yeptris_node_bool")
|
|
114
|
+
out.read_int != 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
TAGS = {
|
|
118
|
+
Yeptris::FFI::TAG_STR => :str,
|
|
119
|
+
Yeptris::FFI::TAG_INT => :int,
|
|
120
|
+
Yeptris::FFI::TAG_FLOAT => :float,
|
|
121
|
+
Yeptris::FFI::TAG_BOOL => :bool,
|
|
122
|
+
Yeptris::FFI::TAG_NULL => :null,
|
|
123
|
+
Yeptris::FFI::TAG_TIMESTAMP => :timestamp,
|
|
124
|
+
}.freeze
|
|
125
|
+
|
|
126
|
+
def tag_id
|
|
127
|
+
TAGS[alive { Yeptris::FFI.yeptris_node_tag_id(@c_ptr) }]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# ---- sequence access ----
|
|
131
|
+
|
|
132
|
+
def size
|
|
133
|
+
case kind
|
|
134
|
+
when :sequence then seq_count
|
|
135
|
+
when :mapping then map_count
|
|
136
|
+
else 1
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def seq_count
|
|
141
|
+
alive { Yeptris::FFI.yeptris_node_seq_count(@c_ptr) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def seq_at(index)
|
|
145
|
+
@document.wrap_node(alive { Yeptris::FFI.yeptris_node_seq_at(@c_ptr, index) })
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def each
|
|
149
|
+
return enum_for(:each) unless block_given?
|
|
150
|
+
raise Yeptris::Error, "#each is for sequences" unless sequence?
|
|
151
|
+
|
|
152
|
+
(0...seq_count).each { |i| yield seq_at(i) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# ---- mapping access ----
|
|
156
|
+
|
|
157
|
+
def map_count
|
|
158
|
+
alive { Yeptris::FFI.yeptris_node_map_count(@c_ptr) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def [](key)
|
|
162
|
+
raise Yeptris::Error, "#[] is for mappings" unless mapping?
|
|
163
|
+
|
|
164
|
+
key = key.to_s
|
|
165
|
+
@document.wrap_node(
|
|
166
|
+
alive { Yeptris::FFI.yeptris_node_map_get(@c_ptr, key, key.bytesize) }
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def key?(key)
|
|
171
|
+
!self[key].nil?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Ordered [key, value] pairs.
|
|
175
|
+
def each_pair
|
|
176
|
+
return enum_for(:each_pair) unless block_given?
|
|
177
|
+
raise Yeptris::Error, "#each_pair is for mappings" unless mapping?
|
|
178
|
+
|
|
179
|
+
k = ::FFI::MemoryPointer.new(:pointer)
|
|
180
|
+
v = ::FFI::MemoryPointer.new(:pointer)
|
|
181
|
+
(0...map_count).each do |i|
|
|
182
|
+
rc = alive { Yeptris::FFI.yeptris_node_map_at(@c_ptr, i, k, v) }
|
|
183
|
+
next unless rc.zero?
|
|
184
|
+
|
|
185
|
+
yield @document.wrap_node(k.read_pointer), @document.wrap_node(v.read_pointer)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def keys
|
|
190
|
+
# explicit block: two-arg yield + Symbol#to_proc would call
|
|
191
|
+
# key.first(value) instead of taking the pair
|
|
192
|
+
each_pair.map { |k, _v| k }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ---- construction (TODO.impl/11 phase 3; errors raise) ----
|
|
196
|
+
|
|
197
|
+
def map_add(key, node)
|
|
198
|
+
key = key.to_s
|
|
199
|
+
rc = alive { Yeptris::FFI.yeptris_node_map_add(@c_ptr, key, key.bytesize, node.c_ptr) }
|
|
200
|
+
Yeptris::FFI.check_status(rc, "yeptris_node_map_add")
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def map_set(key, node)
|
|
205
|
+
key = key.to_s
|
|
206
|
+
rc = alive { Yeptris::FFI.yeptris_node_map_set(@c_ptr, key, key.bytesize, node.c_ptr) }
|
|
207
|
+
Yeptris::FFI.check_status(rc, "yeptris_node_map_set")
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def map_del(key)
|
|
212
|
+
key = key.to_s
|
|
213
|
+
alive { Yeptris::FFI.yeptris_node_map_del(@c_ptr, key, key.bytesize) }.zero?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def seq_add(node)
|
|
217
|
+
rc = alive { Yeptris::FFI.yeptris_node_seq_add(@c_ptr, node.c_ptr) }
|
|
218
|
+
Yeptris::FFI.check_status(rc, "yeptris_node_seq_add")
|
|
219
|
+
self
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Props on synthesized nodes (15's YAMLTree); values copied in.
|
|
223
|
+
def set_anchor(name)
|
|
224
|
+
alive { Yeptris::FFI.yeptris_node_set_anchor(@c_ptr, name, name.bytesize) }.zero?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def set_tag(tag)
|
|
228
|
+
alive { Yeptris::FFI.yeptris_node_set_tag(@c_ptr, tag, tag.bytesize) }.zero?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def seq_del(index)
|
|
232
|
+
alive { Yeptris::FFI.yeptris_node_seq_del(@c_ptr, index) }.zero?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ---- Ruby materialization (Psych-compatible) ----
|
|
236
|
+
|
|
237
|
+
# Materializes the subtree as native Ruby objects. Aliases preserve
|
|
238
|
+
# object identity (the same Ruby object for every reference), keys
|
|
239
|
+
# materialize with Psych's implicit typing (":sym" -> Symbol under
|
|
240
|
+
# compat), and the anchor memo makes cycles defined by anchors safe.
|
|
241
|
+
def node_id
|
|
242
|
+
alive { Yeptris::FFI.yeptris_node_id(@c_ptr) }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def to_ruby(memo = nil)
|
|
246
|
+
# readonly documents memoize per node: a second materialization
|
|
247
|
+
# returns the SAME object (the readonly cache is the ground truth
|
|
248
|
+
# for alias identity across calls).
|
|
249
|
+
if @document.readonly?
|
|
250
|
+
rm = @document.readonly_memo
|
|
251
|
+
cached = rm[node_id]
|
|
252
|
+
return cached if cached
|
|
253
|
+
end
|
|
254
|
+
# Marshal fast path (TODO.restructure/21): one C call turns the
|
|
255
|
+
# whole subtree into Ruby objects, preserving alias identity via
|
|
256
|
+
# the object's own object-link table. Only at the TOP-LEVEL entry
|
|
257
|
+
# (memo nil): inside a recursive walk a partial marshal would
|
|
258
|
+
# materialize outside the shared memo and break alias identity.
|
|
259
|
+
# Falls back to the per-node FFI walk on constructs the format
|
|
260
|
+
# cannot express (merge keys, timestamps) and on older builds.
|
|
261
|
+
if Yeptris::FFI::MARSHAL && memo.nil?
|
|
262
|
+
result =
|
|
263
|
+
begin
|
|
264
|
+
out_p = ::FFI::MemoryPointer.new(:pointer)
|
|
265
|
+
olen_p = ::FFI::MemoryPointer.new(:size_t)
|
|
266
|
+
st = Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p)
|
|
267
|
+
if st == Yeptris::FFI::ERROR_UNSUPPORTED
|
|
268
|
+
nil
|
|
269
|
+
elsif st != Yeptris::FFI::OK
|
|
270
|
+
raise Yeptris::ParseError, Yeptris::FFI.last_error_message
|
|
271
|
+
else
|
|
272
|
+
buf = out_p.read_pointer
|
|
273
|
+
len = olen_p.read_uint64
|
|
274
|
+
bytes = buf.read_bytes(len)
|
|
275
|
+
bytes.force_encoding(Encoding::ASCII_8BIT)
|
|
276
|
+
::Marshal.load(bytes)
|
|
277
|
+
end
|
|
278
|
+
ensure
|
|
279
|
+
Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
|
|
280
|
+
end
|
|
281
|
+
unless result.nil?
|
|
282
|
+
@document.readonly_memo[node_id] = result if @document.readonly?
|
|
283
|
+
return result
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
if @document.readonly?
|
|
287
|
+
return @document.readonly_memo[node_id] = to_ruby_walk({})
|
|
288
|
+
end
|
|
289
|
+
to_ruby_walk(memo || {})
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def to_ruby_walk(memo)
|
|
293
|
+
cached = memo[node_id]
|
|
294
|
+
return cached if cached
|
|
295
|
+
|
|
296
|
+
case kind
|
|
297
|
+
when :mapping
|
|
298
|
+
h = {}
|
|
299
|
+
memo[node_id] = h
|
|
300
|
+
each_pair do |k, v|
|
|
301
|
+
h[k.to_ruby_key] = v.to_ruby(memo)
|
|
302
|
+
end
|
|
303
|
+
h
|
|
304
|
+
when :sequence
|
|
305
|
+
a = []
|
|
306
|
+
memo[node_id] = a
|
|
307
|
+
each { |e| a << e.to_ruby(memo) }
|
|
308
|
+
a
|
|
309
|
+
when :alias
|
|
310
|
+
t = alias_target
|
|
311
|
+
t.nil? ? nil : t.to_ruby(memo)
|
|
312
|
+
else
|
|
313
|
+
scalar_to_ruby
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Mapping keys: Psych semantics — a plain scalar ":name" under the
|
|
318
|
+
# compat schema scans to a Symbol; everything else materializes as
|
|
319
|
+
# the scalar itself.
|
|
320
|
+
def to_ruby_key
|
|
321
|
+
symbol? ? symbolize : to_ruby
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def scalar_to_ruby
|
|
325
|
+
return symbolize if symbol? && tag_id == :str
|
|
326
|
+
|
|
327
|
+
case tag_id
|
|
328
|
+
when :null then nil
|
|
329
|
+
when :bool then to_bool
|
|
330
|
+
when :int then to_i
|
|
331
|
+
when :float
|
|
332
|
+
# Psych's dot-required float applies ONLY under compat_11; the
|
|
333
|
+
# core schema's regexp has an optional dot (spec 10.3.2), so
|
|
334
|
+
# core_12 exponent-only floats materialize as Floats
|
|
335
|
+
text = value.to_s
|
|
336
|
+
if @document.parse_schema == :compat_11 &&
|
|
337
|
+
!text.include?(".") && !text.include?(":") && !text.start_with?(".")
|
|
338
|
+
text
|
|
339
|
+
else
|
|
340
|
+
to_f
|
|
341
|
+
end
|
|
342
|
+
when :timestamp then ::Yeptris::Materializer.parse_timestamp(value)
|
|
343
|
+
else
|
|
344
|
+
# beyond int64 the C resolver leaves plain scalars :str; Psych
|
|
345
|
+
# materializes Integer (issue #31) — rebuild from the text
|
|
346
|
+
text = value.to_s
|
|
347
|
+
if style == :plain && text.length > 18 &&
|
|
348
|
+
::Yeptris::ValueML::PSYCH_INT_SHAPE.match?(text)
|
|
349
|
+
text.delete(",_").to_i
|
|
350
|
+
else
|
|
351
|
+
value
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Psych's ScalarScanner: plain ":name" (and ":", the null Symbol)
|
|
357
|
+
# scans to a Symbol; quoting defeats it.
|
|
358
|
+
def symbol?
|
|
359
|
+
style == :plain && value&.start_with?(":") && !value.start_with?("::")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def symbolize
|
|
363
|
+
v = value
|
|
364
|
+
v.length <= 1 ? :"" : v[1..].to_sym
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def alive
|
|
370
|
+
@document.ensure_alive!
|
|
371
|
+
yield
|
|
372
|
+
end
|
|
373
|
+
end
|
|
@@ -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,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The process-exclusive drop-in (issue #69): rebinds the top-level
|
|
4
|
+
# Psych constant to Yeptris::Psych. Once this runs, stdlib psych must
|
|
5
|
+
# NOT be loaded afterwards — its require would re-open THIS module and
|
|
6
|
+
# clobber constants ("already initialized" warnings, then
|
|
7
|
+
# NameError/NoMethodError at call time). Bundles that cannot
|
|
8
|
+
# guarantee the load order (activesupport et al.) should require
|
|
9
|
+
# "yeptris/psych" only and reference Yeptris::Psych directly.
|
|
10
|
+
require "yeptris" # the autoload declarations (Document, Node, ...) live here
|
|
11
|
+
require "yeptris/psych"
|
|
12
|
+
|
|
13
|
+
# ( Psych-stdlib, if already loaded, stays reachable as
|
|
14
|
+
# ::Psych::ORIGINAL.)
|
|
15
|
+
if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych) &&
|
|
16
|
+
!Yeptris::Psych.const_defined?(:ORIGINAL, false)
|
|
17
|
+
Yeptris::Psych.const_set(:ORIGINAL, ::Psych)
|
|
18
|
+
end
|
|
19
|
+
# class_eval reaches Module-private methods WITHOUT send (the law: no
|
|
20
|
+
# send to private methods); remove_const has no public form, and the
|
|
21
|
+
# rebind is this file's whole purpose
|
|
22
|
+
Object.class_eval { remove_const(:Psych) } if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych)
|
|
23
|
+
::Psych = Yeptris::Psych
|
|
@@ -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.from_parse_error(e)
|
|
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
|