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,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
7
|
+
module Yeptris
|
|
8
|
+
module Psych
|
|
9
|
+
module Visitors
|
|
10
|
+
# The dump-side visitor (Psych's YAMLTree core): an arbitrary
|
|
11
|
+
# Ruby object graph becomes DOM nodes — anchors for shared
|
|
12
|
+
# objects, aliases for repeats, the Encodable protocol, and the
|
|
13
|
+
# !ruby/... tags Psych's own emitter produces.
|
|
14
|
+
class YAMLTree
|
|
15
|
+
def initialize
|
|
16
|
+
@tree = ::Yeptris::Document.create
|
|
17
|
+
@names = {} # object_id -> anchor name
|
|
18
|
+
@counter = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def push(obj)
|
|
22
|
+
@refs = Hash.new(0)
|
|
23
|
+
@refs[obj.object_id] += 1 # the root counts as a reference
|
|
24
|
+
count_refs(obj, {})
|
|
25
|
+
root = visit(obj)
|
|
26
|
+
@tree.set_root(root) if root
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def finish
|
|
31
|
+
@tree.serialize
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit(obj)
|
|
35
|
+
case obj
|
|
36
|
+
when nil, true, false, ::Integer, ::Float, ::String then scalar(obj)
|
|
37
|
+
when ::Symbol then scalar(":#{obj}")
|
|
38
|
+
when ::Date, ::Time then scalar(obj) # timestamp text, never ivars
|
|
39
|
+
when ::Hash then visit_hash(obj)
|
|
40
|
+
when ::Array then visit_array(obj)
|
|
41
|
+
when ::Struct then visit_struct(obj)
|
|
42
|
+
when ::Set then visit_set(obj)
|
|
43
|
+
when Encodable then visit_encode_with(obj)
|
|
44
|
+
else
|
|
45
|
+
if obj.instance_of?(::Object)
|
|
46
|
+
# A BARE Object has no declared state to lose — the
|
|
47
|
+
# empty !ruby/object form is correct by construction
|
|
48
|
+
# (an Object with ivars set from outside its class is
|
|
49
|
+
# outside the encapsulation contract; use a real class
|
|
50
|
+
# with Encodable for stateful objects).
|
|
51
|
+
return visit_bare_object(obj)
|
|
52
|
+
end
|
|
53
|
+
refuse_dump(obj)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Psych anchors only objects that appear more than once;
|
|
60
|
+
# single-occurrence containers dump clean (no &o1 noise)
|
|
61
|
+
def anchor_for(obj)
|
|
62
|
+
oid = obj.object_id
|
|
63
|
+
name = @names[oid]
|
|
64
|
+
return [:alias, name] if name
|
|
65
|
+
|
|
66
|
+
if @refs[oid] < 2
|
|
67
|
+
return [:none, nil]
|
|
68
|
+
end
|
|
69
|
+
name = "o#{@counter += 1}"
|
|
70
|
+
@names[oid] = name
|
|
71
|
+
[:new, name]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def count_refs(obj, seen)
|
|
75
|
+
return if seen.key?(obj.object_id)
|
|
76
|
+
|
|
77
|
+
seen[obj.object_id] = true
|
|
78
|
+
case obj
|
|
79
|
+
when Hash
|
|
80
|
+
obj.each_value { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
81
|
+
when Array
|
|
82
|
+
obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
83
|
+
when Struct
|
|
84
|
+
obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
85
|
+
when Set
|
|
86
|
+
obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
87
|
+
when ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time, true, false, nil, ::Numeric
|
|
88
|
+
# immutables: identity anchors are meaningless
|
|
89
|
+
else
|
|
90
|
+
unless obj.instance_of?(::Object)
|
|
91
|
+
refuse_dump(obj) unless obj.is_a?(Encodable)
|
|
92
|
+
# Encodable: descend through the PUBLIC protocol so
|
|
93
|
+
# shared objects inside coder contents still get
|
|
94
|
+
# anchors — encode_with must be pure (it runs once more
|
|
95
|
+
# at dump).
|
|
96
|
+
coder = ::Yeptris::Psych::CoderShim.new(nil)
|
|
97
|
+
obj.encode_with(coder)
|
|
98
|
+
case coder.type
|
|
99
|
+
when :seq
|
|
100
|
+
coder.seq.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
101
|
+
when :map
|
|
102
|
+
coder.each { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def refuse_dump(obj)
|
|
109
|
+
raise ::Yeptris::DumpError,
|
|
110
|
+
"cannot dump #{obj.class}: include Yeptris::Psych::Encodable and implement encode_with(coder)"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def scalar(obj, tag: nil)
|
|
114
|
+
# strings route through the builder's plain-safety helper
|
|
115
|
+
# (one quoting rule, DRY); other scalars are their own text
|
|
116
|
+
text =
|
|
117
|
+
if obj.is_a?(::Date) || obj.is_a?(::Time)
|
|
118
|
+
obj.iso8601 # canonical timestamp form, not to_s
|
|
119
|
+
else
|
|
120
|
+
obj.nil? ? "null" : obj.to_s
|
|
121
|
+
end
|
|
122
|
+
n =
|
|
123
|
+
if obj.is_a?(::String)
|
|
124
|
+
::Yeptris::YAML::Builder.build_string(@tree, obj)
|
|
125
|
+
else
|
|
126
|
+
@tree.new_scalar(text, :plain)
|
|
127
|
+
end
|
|
128
|
+
n.set_tag(tag) if tag
|
|
129
|
+
n
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def visit_hash(hash)
|
|
133
|
+
state, name = anchor_for(hash)
|
|
134
|
+
return alias_of(hash, name) if state == :alias
|
|
135
|
+
|
|
136
|
+
m = @tree.new_mapping
|
|
137
|
+
remember(hash, m)
|
|
138
|
+
m.set_anchor(name) if name
|
|
139
|
+
hash.each { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
140
|
+
m
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def visit_array(ary)
|
|
144
|
+
state, name = anchor_for(ary)
|
|
145
|
+
return alias_of(ary, name) if state == :alias
|
|
146
|
+
|
|
147
|
+
s = @tree.new_sequence
|
|
148
|
+
remember(ary, s)
|
|
149
|
+
s.set_anchor(name) if name
|
|
150
|
+
ary.each { |e| s.seq_add(visit(e)) }
|
|
151
|
+
s
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def visit_struct(strct)
|
|
155
|
+
state, name = anchor_for(strct)
|
|
156
|
+
return alias_of(strct, name) if state == :alias
|
|
157
|
+
|
|
158
|
+
m = @tree.new_mapping
|
|
159
|
+
remember(strct, m)
|
|
160
|
+
m.set_anchor(name) if name
|
|
161
|
+
m.set_tag(strct.class.name ? "!ruby/struct:#{strct.class.name}" : "!ruby/struct")
|
|
162
|
+
strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
163
|
+
m
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def visit_set(set)
|
|
167
|
+
state, name = anchor_for(set)
|
|
168
|
+
return alias_of(set, name) if state == :alias
|
|
169
|
+
|
|
170
|
+
s = @tree.new_sequence
|
|
171
|
+
remember(set, s)
|
|
172
|
+
s.set_anchor(name) if name
|
|
173
|
+
s.set_tag("!ruby/set")
|
|
174
|
+
set.each { |e| s.seq_add(visit(e)) }
|
|
175
|
+
s
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def visit_encode_with(obj)
|
|
179
|
+
state, name = anchor_for(obj)
|
|
180
|
+
return alias_of(obj, name) if state == :alias
|
|
181
|
+
|
|
182
|
+
coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
|
|
183
|
+
obj.encode_with(coder) # the typed public surface
|
|
184
|
+
# Wrap coder contents in a __init__ sub-mapping so the load
|
|
185
|
+
# side can dispatch back into obj.init_with(coder) without
|
|
186
|
+
# library-side ivar reflection (the encapsulation law).
|
|
187
|
+
init = @tree.new_mapping
|
|
188
|
+
case coder.type
|
|
189
|
+
when :scalar
|
|
190
|
+
init.map_add("__scalar__", scalar(coder.scalar))
|
|
191
|
+
init.map_add("__tag__", scalar(coder.tag)) if coder.tag
|
|
192
|
+
when :seq
|
|
193
|
+
inner = @tree.new_sequence
|
|
194
|
+
inner.set_tag(coder.tag) if coder.tag
|
|
195
|
+
coder.seq.each { |e| inner.seq_add(visit(e)) }
|
|
196
|
+
init.seq_add(inner)
|
|
197
|
+
else
|
|
198
|
+
coder.each { |k, v| init.map_add(key_text(k), visit(v)) }
|
|
199
|
+
init.set_tag(coder.tag) if coder.tag
|
|
200
|
+
end
|
|
201
|
+
m = @tree.new_mapping
|
|
202
|
+
m.set_anchor(name) if name
|
|
203
|
+
m.set_tag("!ruby/object:#{obj.class.name}")
|
|
204
|
+
m.map_add("__init__", init)
|
|
205
|
+
remember(obj, m)
|
|
206
|
+
m
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def visit_bare_object(obj)
|
|
210
|
+
state, name = anchor_for(obj)
|
|
211
|
+
return alias_of(obj, name) if state == :alias
|
|
212
|
+
|
|
213
|
+
m = @tree.new_mapping
|
|
214
|
+
remember(obj, m)
|
|
215
|
+
m.set_anchor(name) if name
|
|
216
|
+
m.set_tag("!ruby/object")
|
|
217
|
+
m
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def alias_of(obj, name)
|
|
221
|
+
@tree.new_alias(@nodes.fetch(obj.object_id), name)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def remember(obj, node)
|
|
225
|
+
(@nodes ||= {})[obj.object_id] = node
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def key_text(k)
|
|
229
|
+
k.is_a?(Symbol) ? ":#{k}" : k.to_s
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Load side: revival of !ruby/... tags over the parsed Nodes
|
|
235
|
+
# tree. The Materializer stays the plain-data fast path.
|
|
236
|
+
class ToRuby
|
|
237
|
+
def self.visit(node)
|
|
238
|
+
new.visit(node)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def visit(node)
|
|
242
|
+
@root ||= node
|
|
243
|
+
case node
|
|
244
|
+
when ::Yeptris::Psych::Nodes::Alias then visit_alias(node)
|
|
245
|
+
when ::Yeptris::Psych::Nodes::Scalar then visit_scalar(node)
|
|
246
|
+
when ::Yeptris::Psych::Nodes::Sequence then visit_sequence(node)
|
|
247
|
+
when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
|
|
248
|
+
else node&.to_ruby
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def anchors
|
|
255
|
+
@anchors ||= {}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def visit_alias(node)
|
|
259
|
+
name = node.anchor
|
|
260
|
+
unless anchors.key?(name)
|
|
261
|
+
target = find_anchored(root_container(node), name)
|
|
262
|
+
anchors[name] = target ? visit(target) : nil
|
|
263
|
+
end
|
|
264
|
+
anchors[name]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# the walk for alias targets starts at the first node this
|
|
268
|
+
# visitor was handed (the document's root)
|
|
269
|
+
def root_container(_node)
|
|
270
|
+
@root
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def find_anchored(node, name)
|
|
274
|
+
return node if node.anchor == name
|
|
275
|
+
|
|
276
|
+
node.children.each do |c|
|
|
277
|
+
found = find_anchored(c, name)
|
|
278
|
+
return found if found
|
|
279
|
+
end
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def visit_scalar(node)
|
|
284
|
+
node.to_ruby
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def visit_sequence(node)
|
|
288
|
+
if node.tag == "!ruby/set"
|
|
289
|
+
set = ::Set.new
|
|
290
|
+
anchors[node.anchor] = set if node.anchor
|
|
291
|
+
node.children.each { |c| set << visit(c) }
|
|
292
|
+
return set
|
|
293
|
+
end
|
|
294
|
+
ary = []
|
|
295
|
+
anchors[node.anchor] = ary if node.anchor
|
|
296
|
+
node.children.each { |c| ary << visit(c) }
|
|
297
|
+
ary
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def visit_mapping(node)
|
|
301
|
+
tag = node.tag
|
|
302
|
+
return hash_into({}, node) if tag.nil? || tag.empty?
|
|
303
|
+
|
|
304
|
+
case tag
|
|
305
|
+
when "!ruby/set"
|
|
306
|
+
set = Set.new
|
|
307
|
+
anchors[node.anchor] = set if node.anchor
|
|
308
|
+
node.children.each_slice(2) { |_k, v| set << visit(v) }
|
|
309
|
+
set
|
|
310
|
+
when /\A!ruby\/struct(?::(.+))?\z/
|
|
311
|
+
klass = resolve_class(Regexp.last_match(1).to_s)
|
|
312
|
+
pairs = {}
|
|
313
|
+
anchors[node.anchor] = nil # placeholder: filled below
|
|
314
|
+
node.children.each_slice(2) do |k, v|
|
|
315
|
+
pairs[visit(k).to_s.sub(/\A:/, "")] = visit(v)
|
|
316
|
+
end
|
|
317
|
+
keys = pairs.keys.map(&:to_sym)
|
|
318
|
+
built =
|
|
319
|
+
if klass && klass <= Struct
|
|
320
|
+
klass.new(*pairs.values)
|
|
321
|
+
else
|
|
322
|
+
Struct.new(*keys).new(*pairs.values)
|
|
323
|
+
end
|
|
324
|
+
anchors[node.anchor] = built if node.anchor
|
|
325
|
+
built
|
|
326
|
+
when /\A!ruby\/object(?::(.+))?\z/
|
|
327
|
+
klass = resolve_class(Regexp.last_match(1).to_s)
|
|
328
|
+
revive_object(klass, node)
|
|
329
|
+
else
|
|
330
|
+
hash_into({}, node)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def hash_into(h, node)
|
|
335
|
+
anchors[node.anchor] = h if node.anchor
|
|
336
|
+
node.children.each_slice(2) do |k, v|
|
|
337
|
+
h[visit(k)] = visit(v)
|
|
338
|
+
end
|
|
339
|
+
h
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def revive_object(klass, node)
|
|
343
|
+
# bare !ruby/object (no class): Psych.dump(Object.new)'s form.
|
|
344
|
+
# An Object has no declared state — allocation is the whole
|
|
345
|
+
# revival (no reflection, no protocol needed).
|
|
346
|
+
return ::Object.allocate if klass.nil?
|
|
347
|
+
|
|
348
|
+
raise ::Yeptris::DumpError, "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)" unless klass <= ::Yeptris::Psych::Encodable
|
|
349
|
+
|
|
350
|
+
obj = klass.allocate
|
|
351
|
+
anchors[node.anchor] = obj if node.anchor
|
|
352
|
+
# The class opts in via init_with(coder); the encoder wrote
|
|
353
|
+
# `coder["__init__"] = the mapping` so we can pass it back
|
|
354
|
+
# without library-side ivar reflection.
|
|
355
|
+
node.children.each_slice(2) do |k, v|
|
|
356
|
+
key = k.to_ruby.to_s
|
|
357
|
+
next unless key == "__init__"
|
|
358
|
+
|
|
359
|
+
coder = ::Yeptris::Psych::CoderShim.new
|
|
360
|
+
if v.is_a?(::Yeptris::Psych::Nodes::Mapping)
|
|
361
|
+
v.children.each_slice(2) do |ck, cv|
|
|
362
|
+
coder[ck.to_ruby.to_s] = visit(cv)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
obj.init_with(coder)
|
|
366
|
+
end
|
|
367
|
+
obj
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def resolve_class(name)
|
|
371
|
+
return nil if name.nil? || name.empty?
|
|
372
|
+
|
|
373
|
+
klass = ::Object.const_get(name)
|
|
374
|
+
raise DisallowedClass, name unless klass.is_a?(Class) || klass.is_a?(Module)
|
|
375
|
+
|
|
376
|
+
klass
|
|
377
|
+
rescue ::NameError
|
|
378
|
+
nil
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|