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,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Yeptris
|
|
7
|
+
# Recorder-driven Ruby materialization (TODO.impl/15 phase B).
|
|
8
|
+
#
|
|
9
|
+
# One bulk drain: the record array and string arena are read in two
|
|
10
|
+
# calls, then a pure-Ruby stack machine walks fixed-layout records —
|
|
11
|
+
# the FFI tax is O(chunks), never O(events). The DOM-walk
|
|
12
|
+
# materializer (Node#to_ruby) stays for node-based use; this is the
|
|
13
|
+
# Yeptris::YAML fast path.
|
|
14
|
+
class Materializer
|
|
15
|
+
RECORD_SIZE = 36 # YeptrisEventRecord layout (events.h, ABI-pinned)
|
|
16
|
+
FIELDS = 12 # unpacked values per record
|
|
17
|
+
RECORD_UNPACK = "C4V8" # type/style/flags/tag_id, line..tag_len
|
|
18
|
+
|
|
19
|
+
STREAM_START = 1
|
|
20
|
+
STREAM_END = 2
|
|
21
|
+
DOCUMENT_START = 3
|
|
22
|
+
DOCUMENT_END = 4
|
|
23
|
+
SEQUENCE_START = 5
|
|
24
|
+
SEQUENCE_END = 6
|
|
25
|
+
MAPPING_START = 7
|
|
26
|
+
MAPPING_END = 8
|
|
27
|
+
SCALAR = 9
|
|
28
|
+
ALIAS = 10
|
|
29
|
+
|
|
30
|
+
EF_IMPLICIT = 1 << 2
|
|
31
|
+
STYLE_PLAIN = 1
|
|
32
|
+
|
|
33
|
+
# YeptrisTagId values (resolve.h; FFI mirrors them)
|
|
34
|
+
TAG_STR = Yeptris::FFI::TAG_STR
|
|
35
|
+
TAG_INT = Yeptris::FFI::TAG_INT
|
|
36
|
+
TAG_FLOAT = Yeptris::FFI::TAG_FLOAT
|
|
37
|
+
TAG_BOOL = Yeptris::FFI::TAG_BOOL
|
|
38
|
+
TAG_NULL = Yeptris::FFI::TAG_NULL
|
|
39
|
+
TAG_TIMESTAMP = Yeptris::FFI::TAG_TIMESTAMP
|
|
40
|
+
TAG_MERGE = 9 # YEPTRIS_TAG_MERGE (resolve.h): a plain '<<' key
|
|
41
|
+
|
|
42
|
+
INF_WORDS = {
|
|
43
|
+
".inf" => Float::INFINITY, ".Inf" => Float::INFINITY, ".INF" => Float::INFINITY,
|
|
44
|
+
"+.inf" => Float::INFINITY, "+.Inf" => Float::INFINITY, "+.INF" => Float::INFINITY,
|
|
45
|
+
"-.inf" => -Float::INFINITY, "-.Inf" => -Float::INFINITY, "-.INF" => -Float::INFINITY,
|
|
46
|
+
".nan" => Float::NAN, ".NaN" => Float::NAN, ".NAN" => Float::NAN,
|
|
47
|
+
}.freeze
|
|
48
|
+
SEXAGESIMAL_INT = /\A[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+\z/
|
|
49
|
+
SEXAGESIMAL_FLOAT = /\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+:[0-5]?[0-9]\.[0-9_]*\z/
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# First document of the stream, or nil when the stream is empty.
|
|
53
|
+
def load(yaml, schema: :compat_11)
|
|
54
|
+
docs = load_stream(yaml, schema: schema)
|
|
55
|
+
docs.empty? ? nil : docs.first
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Every document in the stream, in order.
|
|
59
|
+
def load_stream(yaml, schema: :compat_11)
|
|
60
|
+
new(schema: schema).materialize(yaml)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The shared drain seam: one parse, records + arena read once,
|
|
64
|
+
# ONE unpack into a flat Integer array (12 fields per record:
|
|
65
|
+
# type style flags tag_id line col v_off v_len a_off a_len
|
|
66
|
+
# t_off t_len). Both consumers ride it — the stack machine
|
|
67
|
+
# (materialize) and the Psych::Parser dispatch loop.
|
|
68
|
+
def drain(yaml, schema: :compat_11)
|
|
69
|
+
rec = Yeptris::FFI.yeptris_recorder_new_ex(
|
|
70
|
+
schema == :compat_11 ? Yeptris::FFI::SCHEMA_11_COMPAT : Yeptris::FFI::SCHEMA_12_CORE
|
|
71
|
+
)
|
|
72
|
+
begin
|
|
73
|
+
status = Yeptris::FFI.yeptris_recorder_feed(rec, yaml, yaml.bytesize, 1)
|
|
74
|
+
if status != Yeptris::FFI::OK
|
|
75
|
+
raise Yeptris::ParseError,
|
|
76
|
+
"parse failed: #{Yeptris::FFI.last_error_message}"
|
|
77
|
+
end
|
|
78
|
+
count_p = ::FFI::MemoryPointer.new(:size_t)
|
|
79
|
+
records = Yeptris::FFI.yeptris_recorder_records(rec, count_p)
|
|
80
|
+
count = count_p.read_uint64
|
|
81
|
+
arena_len = ::FFI::MemoryPointer.new(:size_t)
|
|
82
|
+
arena_ptr = Yeptris::FFI.yeptris_recorder_arena(rec, arena_len)
|
|
83
|
+
arena_len_v = arena_len.read_uint64
|
|
84
|
+
arena = arena_ptr.null? || arena_len_v.zero? ? +"" : arena_ptr.read_bytes(arena_len_v)
|
|
85
|
+
flat = records.read_bytes(count * RECORD_SIZE)
|
|
86
|
+
.unpack(RECORD_UNPACK * count)
|
|
87
|
+
[flat, arena]
|
|
88
|
+
ensure
|
|
89
|
+
Yeptris::FFI.yeptris_recorder_free(rec)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The Ruby value of a scalar per schema — the ScalarScanner rule
|
|
94
|
+
# set, spec-verified against Psych case by case. Only implicit
|
|
95
|
+
# PLAIN scalars scan; quoting is the escape hatch.
|
|
96
|
+
# The record's tag_id IS the resolver's verdict (the typing
|
|
97
|
+
# SSOT): conversion by tag, Kernel#Integer/Float for the bytes —
|
|
98
|
+
# no host-side grammar. Two Psych-quirk overrides where Psych's
|
|
99
|
+
# scanner disagrees with the 1.1 resolver: single-char y/Y/n/N
|
|
100
|
+
# are STRINGS in Psych (libyaml says bool), and values the
|
|
101
|
+
# resolver tagged INT but Kernel rejects (mixed forms) fall back
|
|
102
|
+
# through sexagesimal to String.
|
|
103
|
+
PSYCH_TRUE = %w[y yes true on].freeze
|
|
104
|
+
|
|
105
|
+
# '<<' merge: existing keys win; sequences merge in order.
|
|
106
|
+
# Shared by the record walk and the value-stream walk.
|
|
107
|
+
def merge_into(map, obj)
|
|
108
|
+
case obj
|
|
109
|
+
when Hash
|
|
110
|
+
obj.each { |k, v| map[k] = v unless map.key?(k) }
|
|
111
|
+
when Array
|
|
112
|
+
obj.each { |e| merge_into(map, e) if e.is_a?(Hash) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_timestamp(v)
|
|
117
|
+
return Date.parse(v) unless v.match?(/[Tt ]\d/)
|
|
118
|
+
|
|
119
|
+
# normalize the YAML 1.1 space forms onto iso8601 for
|
|
120
|
+
# xmlschema: "2001-12-14 21:59:43.10 -05:00" ->
|
|
121
|
+
# "2001-12-14T21:59:43.10-05:00" (Psych's scanner does the
|
|
122
|
+
# same dance)
|
|
123
|
+
Time.xmlschema(v.sub(/ (\d)/, 'T\1').sub(/ ([+-]\d)/, '\1'))
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
v
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def scan_by_tag(value, tag_id, implicit)
|
|
129
|
+
case tag_id
|
|
130
|
+
when TAG_STR
|
|
131
|
+
return value unless implicit
|
|
132
|
+
|
|
133
|
+
value.start_with?(":") && !value.start_with?("::") &&
|
|
134
|
+
value.length > 1 ? value[1..].to_sym : value
|
|
135
|
+
when TAG_NULL then nil
|
|
136
|
+
when TAG_BOOL
|
|
137
|
+
return value if value.length == 1 # Psych: "y"/"n" stay Strings
|
|
138
|
+
|
|
139
|
+
PSYCH_TRUE.include?(value.downcase)
|
|
140
|
+
when TAG_INT
|
|
141
|
+
int_or_string(value)
|
|
142
|
+
when TAG_FLOAT
|
|
143
|
+
float_or_string(value)
|
|
144
|
+
when TAG_TIMESTAMP
|
|
145
|
+
parse_timestamp(value)
|
|
146
|
+
else
|
|
147
|
+
value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def int_or_string(value)
|
|
152
|
+
Integer(value.tr("_", ""))
|
|
153
|
+
rescue ArgumentError
|
|
154
|
+
# the resolver said INT but Kernel disagrees (mixed form):
|
|
155
|
+
# sexagesimal or back to String
|
|
156
|
+
return sexagesimal(value) if SEXAGESIMAL_INT.match?(value) ||
|
|
157
|
+
SEXAGESIMAL_FLOAT.match?(value)
|
|
158
|
+
|
|
159
|
+
value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def float_or_string(value)
|
|
163
|
+
return INF_WORDS[value] if INF_WORDS.key?(value)
|
|
164
|
+
return value unless value.include?(".")
|
|
165
|
+
|
|
166
|
+
# Psych's FLOAT: the exponent carries a mandatory sign —
|
|
167
|
+
# "1e3" and "1.5e3" are Strings (resolver quirk override)
|
|
168
|
+
return value if /[eE][^+-]/.match?(value)
|
|
169
|
+
|
|
170
|
+
Float(value.tr("_", ""))
|
|
171
|
+
rescue ArgumentError
|
|
172
|
+
return sexagesimal(value) if SEXAGESIMAL_FLOAT.match?(value)
|
|
173
|
+
|
|
174
|
+
value
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
# Psych's scanner IS Kernel#Integer on the plain digits: Ruby
|
|
180
|
+
# reads leading-zero strings as octal, rejects "018", takes
|
|
181
|
+
# 0x/0b/bases — verified case by case against Psych.
|
|
182
|
+
def sexagesimal(v)
|
|
183
|
+
is_float = v.include?(".")
|
|
184
|
+
total = 0
|
|
185
|
+
v.split(":").each_with_index do |n, e|
|
|
186
|
+
total += (is_float ? n.to_f : n.to_i) * (60**(e - 2).abs)
|
|
187
|
+
end
|
|
188
|
+
total
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def initialize(schema: :compat_11)
|
|
195
|
+
@schema = schema
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def materialize(yaml)
|
|
199
|
+
yaml = yaml.read if yaml.respond_to?(:read)
|
|
200
|
+
yaml = yaml.to_s
|
|
201
|
+
flat, arena = Materializer.drain(yaml, schema: @schema)
|
|
202
|
+
walk(flat, arena)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
# The stack machine: containers on a stack, a pending-key slot per
|
|
208
|
+
# open mapping, anchors by name (first definition wins; an alias
|
|
209
|
+
# yields the SAME Ruby object — identity preserved). Merge keys
|
|
210
|
+
# (<<) resolve inline: existing keys win, sequences merge in
|
|
211
|
+
# order — Psych load-time semantics.
|
|
212
|
+
#
|
|
213
|
+
# flat: one Integer per unpack field, 12 per record:
|
|
214
|
+
# 0 type, 1 style, 2 flags, 3 tag_id, 4 line, 5 col, 6 value_off,
|
|
215
|
+
# 7 value_len, 8 anchor_off, 9 anchor_len, 10 tag_off, 11 tag_len.
|
|
216
|
+
def walk(flat, arena)
|
|
217
|
+
docs = []
|
|
218
|
+
stack = []
|
|
219
|
+
anchors = {}
|
|
220
|
+
pending_key = []
|
|
221
|
+
pending_key_merge = []
|
|
222
|
+
# per open container: the map to merge into when this container
|
|
223
|
+
# closes (inline `<<:` values), nil otherwise
|
|
224
|
+
merge_target = []
|
|
225
|
+
|
|
226
|
+
# the arena is UTF-8 by construction (validated at parse), so
|
|
227
|
+
# forcing its encoding ONCE makes every slice UTF-8 for free —
|
|
228
|
+
# no per-scalar force_encoding
|
|
229
|
+
arena.force_encoding(Encoding::UTF_8)
|
|
230
|
+
# (each_slice DESTRUCTURING measured SLOWER than plain indexing
|
|
231
|
+
# here: it allocates a 12-slot Array per record. Index away.)
|
|
232
|
+
i = 0
|
|
233
|
+
n = flat.length
|
|
234
|
+
while i < n
|
|
235
|
+
type = flat[i]
|
|
236
|
+
case type
|
|
237
|
+
when SCALAR
|
|
238
|
+
# a plain, resolver-tagged '<<' (TAG_MERGE) merges; a
|
|
239
|
+
# QUOTED '<<' is a literal key — Psych merges on the tag,
|
|
240
|
+
# not the text
|
|
241
|
+
tag_id = flat[i + 3]
|
|
242
|
+
v = Materializer.scan_by_tag(
|
|
243
|
+
arena[flat[i + 6], flat[i + 7]], tag_id, (flat[i + 2] & EF_IMPLICIT) != 0
|
|
244
|
+
)
|
|
245
|
+
l = flat[i + 9]
|
|
246
|
+
anchors[arena[flat[i + 8], l]] = v if l != 0
|
|
247
|
+
# place(): the scalar fast path inlined — the overwhelming
|
|
248
|
+
# majority of events land here
|
|
249
|
+
if stack.empty?
|
|
250
|
+
docs[-1] = v
|
|
251
|
+
else
|
|
252
|
+
parent = stack.last
|
|
253
|
+
if parent.is_a?(Hash)
|
|
254
|
+
if (key = pending_key[-1]).nil?
|
|
255
|
+
pending_key[-1] = v
|
|
256
|
+
pending_key_merge[-1] = flat[i + 3] == TAG_MERGE
|
|
257
|
+
else
|
|
258
|
+
pending_key[-1] = nil
|
|
259
|
+
if pending_key_merge[-1]
|
|
260
|
+
merge_into(parent, v)
|
|
261
|
+
else
|
|
262
|
+
parent[key] = v
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
parent.push(v)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
when MAPPING_START
|
|
270
|
+
h = {}
|
|
271
|
+
l = flat[i + 9]
|
|
272
|
+
anchors[arena[flat[i + 8], l]] = h if l != 0
|
|
273
|
+
merge_target.push(place(docs, stack, pending_key, pending_key_merge, h))
|
|
274
|
+
stack.push(h)
|
|
275
|
+
pending_key.push(nil)
|
|
276
|
+
pending_key_merge.push(nil)
|
|
277
|
+
when SEQUENCE_START
|
|
278
|
+
a = []
|
|
279
|
+
l = flat[i + 9]
|
|
280
|
+
anchors[arena[flat[i + 8], l]] = a if l != 0
|
|
281
|
+
merge_target.push(place(docs, stack, pending_key, pending_key_merge, a))
|
|
282
|
+
stack.push(a)
|
|
283
|
+
pending_key.push(nil)
|
|
284
|
+
pending_key_merge.push(nil)
|
|
285
|
+
when ALIAS
|
|
286
|
+
name = arena[flat[i + 6], flat[i + 7]]
|
|
287
|
+
raise Yeptris::ParseError, "unknown anchor: #{name.inspect}" unless anchors.key?(name)
|
|
288
|
+
|
|
289
|
+
place(docs, stack, pending_key, pending_key_merge, anchors[name])
|
|
290
|
+
when MAPPING_END, SEQUENCE_END
|
|
291
|
+
done = stack.pop
|
|
292
|
+
pending_key.pop
|
|
293
|
+
pending_key_merge.pop
|
|
294
|
+
target = merge_target.pop
|
|
295
|
+
merge_into(target, done) if target
|
|
296
|
+
when DOCUMENT_START
|
|
297
|
+
docs.push(nil)
|
|
298
|
+
end
|
|
299
|
+
i += FIELDS
|
|
300
|
+
end
|
|
301
|
+
docs
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Attaches obj: as the current document's root when nothing is
|
|
305
|
+
# open, as a sequence entry, or as the pending mapping key/value.
|
|
306
|
+
# Returns the merge TARGET when obj is a container placed under a
|
|
307
|
+
# "<<" key — the caller defers the merge to the container's END
|
|
308
|
+
# (an inline map's contents arrive after its start event); scalar
|
|
309
|
+
# and alias merges apply immediately.
|
|
310
|
+
def place(docs, stack, pending_key, pending_key_merge, obj)
|
|
311
|
+
if stack.empty?
|
|
312
|
+
docs[-1] = obj
|
|
313
|
+
return nil
|
|
314
|
+
end
|
|
315
|
+
parent = stack.last
|
|
316
|
+
if parent.is_a?(Hash)
|
|
317
|
+
if pending_key.last.nil?
|
|
318
|
+
pending_key[-1] = obj
|
|
319
|
+
pending_key_merge[-1] = false
|
|
320
|
+
return nil
|
|
321
|
+
end
|
|
322
|
+
key = pending_key.last
|
|
323
|
+
pending_key[-1] = nil
|
|
324
|
+
if pending_key_merge[-1]
|
|
325
|
+
merge_into(parent, obj)
|
|
326
|
+
parent
|
|
327
|
+
else
|
|
328
|
+
parent[key] = obj
|
|
329
|
+
nil
|
|
330
|
+
end
|
|
331
|
+
else # Array
|
|
332
|
+
parent.push(obj)
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
end
|
|
338
|
+
end
|
data/lib/yeptris/node.rb
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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: repeated materialization
|
|
247
|
+
# of a shared subtree returns the SAME object at zero walk cost
|
|
248
|
+
if @document.readonly?
|
|
249
|
+
rm = @document.readonly_memo
|
|
250
|
+
cached = rm[node_id]
|
|
251
|
+
return cached if cached
|
|
252
|
+
|
|
253
|
+
return rm[node_id] = to_ruby_walk({})
|
|
254
|
+
end
|
|
255
|
+
to_ruby_walk(memo || {})
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def to_ruby_walk(memo)
|
|
259
|
+
cached = memo[node_id]
|
|
260
|
+
return cached if cached
|
|
261
|
+
|
|
262
|
+
case kind
|
|
263
|
+
when :mapping
|
|
264
|
+
h = {}
|
|
265
|
+
memo[node_id] = h
|
|
266
|
+
each_pair do |k, v|
|
|
267
|
+
h[k.to_ruby_key] = v.to_ruby(memo)
|
|
268
|
+
end
|
|
269
|
+
h
|
|
270
|
+
when :sequence
|
|
271
|
+
a = []
|
|
272
|
+
memo[node_id] = a
|
|
273
|
+
each { |e| a << e.to_ruby(memo) }
|
|
274
|
+
a
|
|
275
|
+
when :alias
|
|
276
|
+
t = alias_target
|
|
277
|
+
t.nil? ? nil : t.to_ruby(memo)
|
|
278
|
+
else
|
|
279
|
+
scalar_to_ruby
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Mapping keys: Psych semantics — a plain scalar ":name" under the
|
|
284
|
+
# compat schema scans to a Symbol; everything else materializes as
|
|
285
|
+
# the scalar itself.
|
|
286
|
+
def to_ruby_key
|
|
287
|
+
symbol? ? symbolize : to_ruby
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def scalar_to_ruby
|
|
291
|
+
return symbolize if symbol? && tag_id == :str
|
|
292
|
+
|
|
293
|
+
case tag_id
|
|
294
|
+
when :null then nil
|
|
295
|
+
when :bool then to_bool
|
|
296
|
+
when :int then to_i
|
|
297
|
+
when :float then to_f
|
|
298
|
+
when :timestamp then ::Yeptris::Materializer.parse_timestamp(value)
|
|
299
|
+
else value
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Psych's ScalarScanner: plain ":name" (and ":", the null Symbol)
|
|
304
|
+
# scans to a Symbol; quoting defeats it.
|
|
305
|
+
def symbol?
|
|
306
|
+
style == :plain && value&.start_with?(":") && !value.start_with?("::")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def symbolize
|
|
310
|
+
v = value
|
|
311
|
+
v.length <= 1 ? :"" : v[1..].to_sym
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def alive
|
|
317
|
+
@document.ensure_alive!
|
|
318
|
+
yield
|
|
319
|
+
end
|
|
320
|
+
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
|