yeptris 0.1.0
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 +59 -0
- data/lib/yeptris/document.rb +209 -0
- data/lib/yeptris/error.rb +19 -0
- data/lib/yeptris/ffi.rb +216 -0
- data/lib/yeptris/materializer.rb +338 -0
- data/lib/yeptris/node.rb +320 -0
- data/lib/yeptris/psych/coder_shim.rb +55 -0
- data/lib/yeptris/psych/handler.rb +90 -0
- data/lib/yeptris/psych/parser.rb +105 -0
- data/lib/yeptris/psych/visitors.rb +364 -0
- data/lib/yeptris/psych.rb +315 -0
- data/lib/yeptris/valueml.rb +218 -0
- data/lib/yeptris/version.rb +5 -0
- data/lib/yeptris/yaml.rb +247 -0
- data/lib/yeptris.rb +29 -0
- metadata +74 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yeptris"
|
|
4
|
+
require "yeptris/psych/handler"
|
|
5
|
+
require "yeptris/psych/parser"
|
|
6
|
+
require "yeptris/psych/coder_shim"
|
|
7
|
+
require "yeptris/psych/visitors"
|
|
8
|
+
|
|
9
|
+
# The Psych drop-in namespace (TODO.impl/15 phase C).
|
|
10
|
+
#
|
|
11
|
+
# `require "yeptris/psych"` rebinds the top-level Psych constant to
|
|
12
|
+
# this module (the original, if any, 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
|
+
class Error < StandardError; end
|
|
21
|
+
class SyntaxError < Error
|
|
22
|
+
attr_reader :line, :column
|
|
23
|
+
|
|
24
|
+
def initialize(message, line = 0, column = 0)
|
|
25
|
+
super(message)
|
|
26
|
+
@line = line
|
|
27
|
+
@column = column
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
class BadAlias < Error; end
|
|
31
|
+
class DisallowedClass < Error
|
|
32
|
+
attr_reader :name
|
|
33
|
+
|
|
34
|
+
def initialize(name)
|
|
35
|
+
super("Tried to load unspecified class: #{name}")
|
|
36
|
+
@name = name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
class AliasNotEnabled < Error; end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Psych 5: load is safe — plain data structures only. Tagged
|
|
43
|
+
# nodes raise DisallowedClass unless their class is permitted
|
|
44
|
+
# (Date/Time/Symbol are built in; they are plain data here).
|
|
45
|
+
def load(yaml, permitted_classes: [], aliases: false, **)
|
|
46
|
+
safe_load(yaml, permitted_classes: permitted_classes, aliases: aliases)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Full revival over the Nodes tree: !ruby/object, !ruby/struct,
|
|
50
|
+
# !ruby/set, encode_with/init_with, alias identity. Plain-data
|
|
51
|
+
# loads should use load/safe_load (the Materializer fast path).
|
|
52
|
+
def unsafe_load(yaml, **)
|
|
53
|
+
tree = parse(yaml)
|
|
54
|
+
return nil if tree.nil?
|
|
55
|
+
|
|
56
|
+
Visitors::ToRuby.visit(tree.children.first)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def safe_load(yaml, permitted_classes: [], aliases: false, **)
|
|
60
|
+
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
61
|
+
begin
|
|
62
|
+
walk_safe(doc.root(0), permitted_classes, aliases)
|
|
63
|
+
ensure
|
|
64
|
+
doc.free
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The first document's node tree (no Ruby materialization).
|
|
69
|
+
def parse(yaml)
|
|
70
|
+
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
71
|
+
return nil if doc.document_count.zero?
|
|
72
|
+
|
|
73
|
+
Nodes::Builder.document(doc)
|
|
74
|
+
rescue Yeptris::ParseError => e
|
|
75
|
+
raise SyntaxError, e.message
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_stream(yaml)
|
|
79
|
+
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
80
|
+
return nil if doc.document_count.zero?
|
|
81
|
+
|
|
82
|
+
stream = Nodes::Stream.new
|
|
83
|
+
(0...doc.document_count).each do |i|
|
|
84
|
+
stream.children << Nodes::Builder.document_stream_child(doc, i)
|
|
85
|
+
end
|
|
86
|
+
stream.instance_variable_set(:@owner, doc)
|
|
87
|
+
ObjectSpace.define_finalizer(
|
|
88
|
+
stream, proc { doc.free unless doc.freed? }
|
|
89
|
+
)
|
|
90
|
+
stream
|
|
91
|
+
rescue Yeptris::ParseError => e
|
|
92
|
+
raise SyntaxError, e.message
|
|
93
|
+
ensure
|
|
94
|
+
doc&.free if doc && !stream
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Arbitrary object graphs through the YAMLTree visitor
|
|
98
|
+
# (anchors, !ruby/ tags); plain data rides the fast builder.
|
|
99
|
+
def dump(obj, io = nil)
|
|
100
|
+
# scalars take the fast builder; EVERYTHING else (including
|
|
101
|
+
# plain containers — they may nest custom objects) goes
|
|
102
|
+
# through the visitor, which builds the same DOM for plain
|
|
103
|
+
# data anyway
|
|
104
|
+
out =
|
|
105
|
+
case obj
|
|
106
|
+
when nil, true, false, String, Integer, Float, Symbol, Date, Time
|
|
107
|
+
Yeptris::YAML.dump(obj)
|
|
108
|
+
else
|
|
109
|
+
Visitors::YAMLTree.new.push(obj).finish
|
|
110
|
+
end
|
|
111
|
+
return out unless io
|
|
112
|
+
|
|
113
|
+
io.write(out)
|
|
114
|
+
io
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# yeptris materializes plain data only — there is nothing
|
|
120
|
+
# unsafe it COULD load. The safety walk enforces what Psych
|
|
121
|
+
# enforces on such documents: aliases need opt-in, and explicit
|
|
122
|
+
# non-core tags raise DisallowedClass (the only "classes" the
|
|
123
|
+
# loader can produce are core-schema types, all permitted).
|
|
124
|
+
def walk_safe(root, permitted, aliases_enabled)
|
|
125
|
+
check(root, aliases_enabled) if root
|
|
126
|
+
root.to_ruby
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check(node, aliases_enabled)
|
|
130
|
+
case node.kind
|
|
131
|
+
when :alias
|
|
132
|
+
raise AliasNotEnabled, "Unknown alias" unless aliases_enabled
|
|
133
|
+
when :scalar, :mapping, :sequence
|
|
134
|
+
tag = node.tag
|
|
135
|
+
unless tag.nil?
|
|
136
|
+
name = tag.split(":").last
|
|
137
|
+
unless %w[str int float bool null timestamp seq map merge value
|
|
138
|
+
binary].include?(name)
|
|
139
|
+
raise DisallowedClass, name
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
case node.kind
|
|
143
|
+
when :mapping
|
|
144
|
+
node.each_pair do |k, v|
|
|
145
|
+
check(k, aliases_enabled)
|
|
146
|
+
check(v, aliases_enabled)
|
|
147
|
+
end
|
|
148
|
+
when :sequence
|
|
149
|
+
node.each { |e| check(e, aliases_enabled) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Psych::Nodes over the yeptris document: the tree IS the parsed
|
|
156
|
+
# document (children are node handles, not copies) — parse cost
|
|
157
|
+
# is the parse, and to_ruby reuses the Materializer.
|
|
158
|
+
module Nodes
|
|
159
|
+
class Node
|
|
160
|
+
include Enumerable
|
|
161
|
+
|
|
162
|
+
attr_reader :children
|
|
163
|
+
attr_reader :handle # @api private — the Yeptris::Node
|
|
164
|
+
|
|
165
|
+
def initialize(handle = nil, children = [])
|
|
166
|
+
@handle = handle
|
|
167
|
+
@children = children
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def each(&block)
|
|
171
|
+
@children.each(&block)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The Ruby object for this subtree (Materializer semantics).
|
|
175
|
+
def to_ruby
|
|
176
|
+
@handle.to_ruby
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class Stream < Node
|
|
181
|
+
def free
|
|
182
|
+
@owner&.free
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Owns the underlying Yeptris::Document: node handles in the
|
|
187
|
+
# tree stay valid while the tree is reachable; a GC finalizer
|
|
188
|
+
# releases the C memory when it is not.
|
|
189
|
+
class Document < Node
|
|
190
|
+
attr_reader :version, :tags
|
|
191
|
+
|
|
192
|
+
def initialize(version = [], tags = {})
|
|
193
|
+
super(nil)
|
|
194
|
+
@version = version
|
|
195
|
+
@tags = tags
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @api private — transfer of C ownership to this tree
|
|
199
|
+
def own(yeptris_doc)
|
|
200
|
+
@owner = yeptris_doc
|
|
201
|
+
ObjectSpace.define_finalizer(
|
|
202
|
+
self, proc { yeptris_doc.free unless yeptris_doc.freed? }
|
|
203
|
+
)
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def free
|
|
208
|
+
@owner&.free
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class Scalar < Node
|
|
213
|
+
attr_reader :value, :tag, :anchor, :plain, :quoted, :style
|
|
214
|
+
|
|
215
|
+
def initialize(value = nil, anchor: nil, tag: nil, plain: true,
|
|
216
|
+
quoted: false, style: :plain)
|
|
217
|
+
super(nil)
|
|
218
|
+
@value = value
|
|
219
|
+
@anchor = anchor
|
|
220
|
+
@tag = tag
|
|
221
|
+
@plain = plain
|
|
222
|
+
@quoted = quoted
|
|
223
|
+
@style = style
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class Sequence < Node
|
|
228
|
+
attr_reader :anchor, :tag, :style
|
|
229
|
+
|
|
230
|
+
def initialize(anchor: nil, tag: nil, style: :block)
|
|
231
|
+
super(nil, [])
|
|
232
|
+
@anchor = anchor
|
|
233
|
+
@tag = tag
|
|
234
|
+
@style = style
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
class Mapping < Node
|
|
239
|
+
attr_reader :anchor, :tag, :style
|
|
240
|
+
|
|
241
|
+
def initialize(anchor: nil, tag: nil, style: :block)
|
|
242
|
+
super(nil, [])
|
|
243
|
+
@anchor = anchor
|
|
244
|
+
@tag = tag
|
|
245
|
+
@style = style
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
class Alias < Node
|
|
250
|
+
attr_reader :anchor
|
|
251
|
+
|
|
252
|
+
def initialize(anchor)
|
|
253
|
+
super(nil)
|
|
254
|
+
@anchor = anchor
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Builds the Nodes tree from a parsed document.
|
|
259
|
+
module Builder
|
|
260
|
+
module_function
|
|
261
|
+
|
|
262
|
+
# parse(): one document, owning the yeptris document
|
|
263
|
+
def document(doc, index = 0)
|
|
264
|
+
document_stream_child(doc, index).own(doc)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# parse_stream(): a child document sharing one owner (the
|
|
268
|
+
# stream owns the yeptris document)
|
|
269
|
+
def document_stream_child(doc, index)
|
|
270
|
+
root = doc.root(index)
|
|
271
|
+
d = Document.new
|
|
272
|
+
d.instance_variable_set(:@handle, root&.document)
|
|
273
|
+
d.children << node(root) if root
|
|
274
|
+
d
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def node(n)
|
|
278
|
+
case n.kind
|
|
279
|
+
when :mapping
|
|
280
|
+
m = Mapping.new(anchor: n.anchor, tag: n.tag)
|
|
281
|
+
n.each_pair do |k, v|
|
|
282
|
+
m.children << node(k)
|
|
283
|
+
m.children << node(v)
|
|
284
|
+
end
|
|
285
|
+
m.instance_variable_set(:@handle, n)
|
|
286
|
+
m
|
|
287
|
+
when :sequence
|
|
288
|
+
s = Sequence.new(anchor: n.anchor, tag: n.tag)
|
|
289
|
+
n.each { |e| s.children << node(e) }
|
|
290
|
+
s.instance_variable_set(:@handle, n)
|
|
291
|
+
s
|
|
292
|
+
when :alias
|
|
293
|
+
a = Alias.new(n.value)
|
|
294
|
+
a.instance_variable_set(:@handle, n)
|
|
295
|
+
a
|
|
296
|
+
else
|
|
297
|
+
sc = Scalar.new(n.value, anchor: n.anchor, tag: n.tag,
|
|
298
|
+
plain: n.style == :plain, style: n.style)
|
|
299
|
+
sc.instance_variable_set(:@handle, n)
|
|
300
|
+
sc
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# The drop-in: rebind the top-level constant ( Psych-stdlib, if
|
|
309
|
+
# already loaded, stays reachable as Yeptris::Psych::ORIGINAL).
|
|
310
|
+
if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych) &&
|
|
311
|
+
!Yeptris::Psych.const_defined?(:ORIGINAL, false)
|
|
312
|
+
Yeptris::Psych.const_set(:ORIGINAL, ::Psych)
|
|
313
|
+
end
|
|
314
|
+
Object.send(:remove_const, :Psych) if defined?(::Psych)
|
|
315
|
+
::Psych = Yeptris::Psych
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yeptris
|
|
4
|
+
# Value-stream materialization (TODO.impl/15 phase F): one drain of
|
|
5
|
+
# PRE-CONVERTED typed values from the C side, then a minimal Ruby
|
|
6
|
+
# walk — no per-scalar parsing (the number kernels already ran),
|
|
7
|
+
# key/value pairing rides the entries' is_key bit, anchors arrive
|
|
8
|
+
# as uniform entries decorating the value they bind. The Psych
|
|
9
|
+
# quirks re-decide from tag_id + the raw bytes every entry carries.
|
|
10
|
+
module ValueML
|
|
11
|
+
DOC = 0
|
|
12
|
+
V_NULL = 1
|
|
13
|
+
V_BOOL = 2
|
|
14
|
+
V_INT = 3
|
|
15
|
+
V_FLOAT = 4
|
|
16
|
+
V_STR = 5
|
|
17
|
+
V_TS = 6
|
|
18
|
+
SEQ_OPEN = 7
|
|
19
|
+
MAP_OPEN = 8
|
|
20
|
+
CLOSE = 9
|
|
21
|
+
ALIAS = 10
|
|
22
|
+
ANCHOR = 11
|
|
23
|
+
|
|
24
|
+
FIELDS = 7
|
|
25
|
+
VALUE_SIZE = 24
|
|
26
|
+
# kind, tag, is_key, b | off, len | pad4 | payload (INT/FLOAT bits)
|
|
27
|
+
UNPACK = "C4V2x4q<"
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def load_all(yaml, schema: :compat_11)
|
|
32
|
+
yaml = yaml.read if yaml.respond_to?(:read)
|
|
33
|
+
yaml = yaml.to_s
|
|
34
|
+
vals_p = ::FFI::MemoryPointer.new(:pointer)
|
|
35
|
+
count_p = ::FFI::MemoryPointer.new(:uint64)
|
|
36
|
+
arena_p = ::FFI::MemoryPointer.new(:pointer)
|
|
37
|
+
alen_p = ::FFI::MemoryPointer.new(:uint64)
|
|
38
|
+
st = FFI.yeptris_value_drain(
|
|
39
|
+
yaml, yaml.bytesize,
|
|
40
|
+
schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE,
|
|
41
|
+
vals_p, count_p, arena_p, alen_p
|
|
42
|
+
)
|
|
43
|
+
raise ParseError, FFI.last_error_message if st != FFI::OK
|
|
44
|
+
|
|
45
|
+
vals = vals_p.read_pointer
|
|
46
|
+
arena = arena_p.read_pointer
|
|
47
|
+
begin
|
|
48
|
+
count = count_p.read_uint64
|
|
49
|
+
flat = vals.read_bytes(count * VALUE_SIZE).unpack(UNPACK * count)
|
|
50
|
+
arena_bytes = arena.read_bytes(alen_p.read_uint64)
|
|
51
|
+
arena_bytes.force_encoding(Encoding::UTF_8)
|
|
52
|
+
walk(flat, arena_bytes)
|
|
53
|
+
ensure
|
|
54
|
+
FFI.yeptris_value_free(vals, arena)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load(yaml, schema: :compat_11)
|
|
59
|
+
docs = load_all(yaml, schema: schema)
|
|
60
|
+
docs.empty? ? nil : docs.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# field offsets in the unpacked 7-tuple
|
|
64
|
+
KIND = 0
|
|
65
|
+
TAG = 1
|
|
66
|
+
IS_KEY = 2
|
|
67
|
+
B = 3
|
|
68
|
+
OFF = 4
|
|
69
|
+
LEN = 5
|
|
70
|
+
P64 = 6
|
|
71
|
+
|
|
72
|
+
def walk(flat, arena)
|
|
73
|
+
docs = []
|
|
74
|
+
stack = []
|
|
75
|
+
anchors = {}
|
|
76
|
+
pending_key = []
|
|
77
|
+
pending_key_tag = []
|
|
78
|
+
pending_anchor = nil
|
|
79
|
+
merge_target = []
|
|
80
|
+
|
|
81
|
+
i = 0
|
|
82
|
+
n = flat.length
|
|
83
|
+
while i < n
|
|
84
|
+
kind = flat[i + KIND]
|
|
85
|
+
case kind
|
|
86
|
+
when V_STR
|
|
87
|
+
text = arena.byteslice(flat[i + OFF], flat[i + LEN])
|
|
88
|
+
# implicit-plain ':name' scans to a Symbol (Psych's
|
|
89
|
+
# ScalarScanner); quoted ':x' stays a String
|
|
90
|
+
v = if flat[i + B] == 1 && text.length > 1 &&
|
|
91
|
+
text.start_with?(":") && !text.start_with?("::")
|
|
92
|
+
text[1..].to_sym
|
|
93
|
+
else
|
|
94
|
+
text
|
|
95
|
+
end
|
|
96
|
+
if pending_anchor
|
|
97
|
+
anchors[pending_anchor] = v
|
|
98
|
+
pending_anchor = nil
|
|
99
|
+
end
|
|
100
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
101
|
+
when V_INT
|
|
102
|
+
v = flat[i + P64]
|
|
103
|
+
if pending_anchor
|
|
104
|
+
anchors[pending_anchor] = v
|
|
105
|
+
pending_anchor = nil
|
|
106
|
+
end
|
|
107
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
108
|
+
when V_FLOAT
|
|
109
|
+
text = arena.byteslice(flat[i + OFF], flat[i + LEN])
|
|
110
|
+
# Psych's float grammar requires the dot (or an inf/nan
|
|
111
|
+
# word, or sexagesimal ':') — exponent-only forms are
|
|
112
|
+
# Strings even when the compat tag says FLOAT
|
|
113
|
+
v = if text.include?(".") || text.include?(":") || text.start_with?(".")
|
|
114
|
+
[flat[i + P64]].pack("q<").unpack1("E")
|
|
115
|
+
else
|
|
116
|
+
text
|
|
117
|
+
end
|
|
118
|
+
if pending_anchor
|
|
119
|
+
anchors[pending_anchor] = v
|
|
120
|
+
pending_anchor = nil
|
|
121
|
+
end
|
|
122
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
123
|
+
when V_BOOL
|
|
124
|
+
text = arena.byteslice(flat[i + OFF], flat[i + LEN])
|
|
125
|
+
# Psych quirk: single-char y/n stay Strings
|
|
126
|
+
v = text.length == 1 ? text : flat[i + B] == 1
|
|
127
|
+
if pending_anchor
|
|
128
|
+
anchors[pending_anchor] = v
|
|
129
|
+
pending_anchor = nil
|
|
130
|
+
end
|
|
131
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
132
|
+
when V_NULL
|
|
133
|
+
slot(docs, stack, pending_key, pending_key_tag, nil, merge_target, flat, i)
|
|
134
|
+
when V_TS
|
|
135
|
+
v = Materializer.parse_timestamp(arena.byteslice(flat[i + OFF], flat[i + LEN]))
|
|
136
|
+
if pending_anchor
|
|
137
|
+
anchors[pending_anchor] = v
|
|
138
|
+
pending_anchor = nil
|
|
139
|
+
end
|
|
140
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
141
|
+
when MAP_OPEN
|
|
142
|
+
h = {}
|
|
143
|
+
if pending_anchor
|
|
144
|
+
anchors[pending_anchor] = h
|
|
145
|
+
pending_anchor = nil
|
|
146
|
+
end
|
|
147
|
+
merge_target.push(slot(docs, stack, pending_key, pending_key_tag, h, merge_target,
|
|
148
|
+
flat, i))
|
|
149
|
+
stack.push(h)
|
|
150
|
+
pending_key.push(nil)
|
|
151
|
+
pending_key_tag.push(nil)
|
|
152
|
+
when SEQ_OPEN
|
|
153
|
+
a = []
|
|
154
|
+
if pending_anchor
|
|
155
|
+
anchors[pending_anchor] = a
|
|
156
|
+
pending_anchor = nil
|
|
157
|
+
end
|
|
158
|
+
merge_target.push(slot(docs, stack, pending_key, pending_key_tag, a, merge_target,
|
|
159
|
+
flat, i))
|
|
160
|
+
stack.push(a)
|
|
161
|
+
pending_key.push(nil)
|
|
162
|
+
pending_key_tag.push(nil)
|
|
163
|
+
when CLOSE
|
|
164
|
+
closed = stack.pop
|
|
165
|
+
pending_key.pop
|
|
166
|
+
pending_key_tag.pop
|
|
167
|
+
target = merge_target.pop
|
|
168
|
+
Materializer.merge_into(target, closed) if target
|
|
169
|
+
when DOC
|
|
170
|
+
docs.push(nil)
|
|
171
|
+
when ALIAS
|
|
172
|
+
v = anchors[arena.byteslice(flat[i + OFF], flat[i + LEN])]
|
|
173
|
+
slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat, i)
|
|
174
|
+
when ANCHOR
|
|
175
|
+
pending_anchor = arena.byteslice(flat[i + OFF], flat[i + LEN])
|
|
176
|
+
end
|
|
177
|
+
i += FIELDS
|
|
178
|
+
end
|
|
179
|
+
docs
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Places a completed value: document root, sequence entry, or a
|
|
183
|
+
# map's key (is_key) / value (completing the pending pair).
|
|
184
|
+
# Returns the merge TARGET when the value is a still-empty
|
|
185
|
+
# container placed under a '<<' key — the open-site caller pushes
|
|
186
|
+
# it so the matching CLOSE merges into it (an inline map's
|
|
187
|
+
# contents arrive after its open); scalar merges apply now.
|
|
188
|
+
def slot(docs, stack, pending_key, pending_key_tag, v, _merge_target, flat, i)
|
|
189
|
+
if stack.empty?
|
|
190
|
+
docs[-1] = v
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
parent = stack.last
|
|
194
|
+
if parent.is_a?(Array)
|
|
195
|
+
parent.push(v)
|
|
196
|
+
return nil
|
|
197
|
+
end
|
|
198
|
+
if flat[i + IS_KEY] == 1
|
|
199
|
+
pending_key[-1] = v
|
|
200
|
+
pending_key_tag[-1] = flat[i + TAG]
|
|
201
|
+
return nil
|
|
202
|
+
end
|
|
203
|
+
key = pending_key[-1]
|
|
204
|
+
pending_key[-1] = nil
|
|
205
|
+
if key == "<<" && pending_key_tag[-1] == 9 # TAG_MERGE
|
|
206
|
+
if (v.is_a?(Hash) || v.is_a?(Array)) && v.empty?
|
|
207
|
+
parent # deferred: contents arrive after the open
|
|
208
|
+
else
|
|
209
|
+
Materializer.merge_into(parent, v)
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
else
|
|
213
|
+
parent[key] = v
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|