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.
@@ -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