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,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 = yaml.read if yaml.respond_to?(:read)
|
|
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, e.message
|
|
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
|
|
@@ -0,0 +1,364 @@
|
|
|
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, encode_with support, 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
|
+
else
|
|
44
|
+
if obj.respond_to?(:encode_with)
|
|
45
|
+
visit_encode_with(obj)
|
|
46
|
+
else
|
|
47
|
+
visit_object(obj)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Psych anchors only objects that appear more than once;
|
|
55
|
+
# single-occurrence containers dump clean (no &o1 noise)
|
|
56
|
+
def anchor_for(obj)
|
|
57
|
+
oid = obj.object_id
|
|
58
|
+
name = @names[oid]
|
|
59
|
+
return [:alias, name] if name
|
|
60
|
+
|
|
61
|
+
if @refs[oid] < 2
|
|
62
|
+
return [:none, nil]
|
|
63
|
+
end
|
|
64
|
+
name = "o#{@counter += 1}"
|
|
65
|
+
@names[oid] = name
|
|
66
|
+
[:new, name]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def count_refs(obj, seen)
|
|
70
|
+
return if seen.key?(obj.object_id)
|
|
71
|
+
|
|
72
|
+
seen[obj.object_id] = true
|
|
73
|
+
case obj
|
|
74
|
+
when Hash
|
|
75
|
+
obj.each_value { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
76
|
+
when Array
|
|
77
|
+
obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
78
|
+
when Struct
|
|
79
|
+
obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
80
|
+
when Set
|
|
81
|
+
obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
82
|
+
when String, Integer, Float, Symbol, Date, Time, true, false, nil, Numeric
|
|
83
|
+
# immutables: identity anchors are meaningless
|
|
84
|
+
else
|
|
85
|
+
if obj.respond_to?(:encode_with)
|
|
86
|
+
# coder contents are opaque here; the object itself may
|
|
87
|
+
# repeat — count it from the caller side only
|
|
88
|
+
else
|
|
89
|
+
obj.instance_variables.each do |iv|
|
|
90
|
+
v = obj.instance_variable_get(iv)
|
|
91
|
+
@refs[v.object_id] += 1
|
|
92
|
+
count_refs(v, seen)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def scalar(obj, tag: nil)
|
|
99
|
+
# strings route through the builder's plain-safety helper
|
|
100
|
+
# (one quoting rule, DRY); other scalars are their own text
|
|
101
|
+
text =
|
|
102
|
+
if obj.is_a?(Date) || obj.is_a?(Time)
|
|
103
|
+
obj.iso8601 # canonical timestamp form, not to_s
|
|
104
|
+
else
|
|
105
|
+
obj.nil? ? "null" : obj.to_s
|
|
106
|
+
end
|
|
107
|
+
n =
|
|
108
|
+
if obj.is_a?(String)
|
|
109
|
+
::Yeptris::YAML::Builder.build_string(@tree, obj)
|
|
110
|
+
else
|
|
111
|
+
@tree.new_scalar(text, :plain)
|
|
112
|
+
end
|
|
113
|
+
n.set_tag(tag) if tag
|
|
114
|
+
n
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def visit_hash(hash)
|
|
118
|
+
state, name = anchor_for(hash)
|
|
119
|
+
return alias_of(hash, name) if state == :alias
|
|
120
|
+
|
|
121
|
+
m = @tree.new_mapping
|
|
122
|
+
remember(hash, m)
|
|
123
|
+
m.set_anchor(name) if name
|
|
124
|
+
hash.each { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
125
|
+
m
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def visit_array(ary)
|
|
129
|
+
state, name = anchor_for(ary)
|
|
130
|
+
return alias_of(ary, name) if state == :alias
|
|
131
|
+
|
|
132
|
+
s = @tree.new_sequence
|
|
133
|
+
remember(ary, s)
|
|
134
|
+
s.set_anchor(name) if name
|
|
135
|
+
ary.each { |e| s.seq_add(visit(e)) }
|
|
136
|
+
s
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def visit_struct(strct)
|
|
140
|
+
state, name = anchor_for(strct)
|
|
141
|
+
return alias_of(strct, name) if state == :alias
|
|
142
|
+
|
|
143
|
+
m = @tree.new_mapping
|
|
144
|
+
remember(strct, m)
|
|
145
|
+
m.set_anchor(name) if name
|
|
146
|
+
m.set_tag(strct.class.name ? "!ruby/struct:#{strct.class.name}" : "!ruby/struct")
|
|
147
|
+
strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
148
|
+
m
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def visit_set(set)
|
|
152
|
+
state, name = anchor_for(set)
|
|
153
|
+
return alias_of(set, name) if state == :alias
|
|
154
|
+
|
|
155
|
+
s = @tree.new_sequence
|
|
156
|
+
remember(set, s)
|
|
157
|
+
s.set_anchor(name) if name
|
|
158
|
+
s.set_tag("!ruby/set")
|
|
159
|
+
set.each { |e| s.seq_add(visit(e)) }
|
|
160
|
+
s
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def visit_encode_with(obj)
|
|
164
|
+
state, name = anchor_for(obj)
|
|
165
|
+
return alias_of(obj, name) if state == :alias
|
|
166
|
+
|
|
167
|
+
coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
|
|
168
|
+
obj.encode_with(coder)
|
|
169
|
+
node =
|
|
170
|
+
case coder.type
|
|
171
|
+
when :scalar then scalar(coder.scalar, tag: coder.tag)
|
|
172
|
+
when :seq
|
|
173
|
+
s = @tree.new_sequence
|
|
174
|
+
s.set_tag(coder.tag) if coder.tag
|
|
175
|
+
coder.seq.each { |e| s.seq_add(visit(e)) }
|
|
176
|
+
s
|
|
177
|
+
else
|
|
178
|
+
m = @tree.new_mapping
|
|
179
|
+
m.set_tag(coder.tag) if coder.tag
|
|
180
|
+
coder.each { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
181
|
+
m
|
|
182
|
+
end
|
|
183
|
+
remember(obj, node)
|
|
184
|
+
node.set_anchor(name) if name
|
|
185
|
+
node
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def visit_object(obj)
|
|
189
|
+
state, name = anchor_for(obj)
|
|
190
|
+
return alias_of(obj, name) if state == :alias
|
|
191
|
+
|
|
192
|
+
m = @tree.new_mapping
|
|
193
|
+
remember(obj, m)
|
|
194
|
+
m.set_anchor(name) if name
|
|
195
|
+
m.set_tag("!ruby/object:#{obj.class.name}")
|
|
196
|
+
obj.instance_variables.each do |ivar|
|
|
197
|
+
m.map_add(ivar.to_s, visit(obj.instance_variable_get(ivar)))
|
|
198
|
+
end
|
|
199
|
+
m
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def alias_of(obj, name)
|
|
203
|
+
@tree.new_alias(@nodes.fetch(obj.object_id), name)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def remember(obj, node)
|
|
207
|
+
(@nodes ||= {})[obj.object_id] = node
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def key_text(k)
|
|
211
|
+
k.is_a?(Symbol) ? ":#{k}" : k.to_s
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Load side: revival of !ruby/... tags over the parsed Nodes
|
|
217
|
+
# tree. The Materializer stays the plain-data fast path.
|
|
218
|
+
class ToRuby
|
|
219
|
+
def self.visit(node)
|
|
220
|
+
new.visit(node)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def visit(node)
|
|
224
|
+
@root ||= node
|
|
225
|
+
case node
|
|
226
|
+
when ::Yeptris::Psych::Nodes::Alias then visit_alias(node)
|
|
227
|
+
when ::Yeptris::Psych::Nodes::Scalar then visit_scalar(node)
|
|
228
|
+
when ::Yeptris::Psych::Nodes::Sequence then visit_sequence(node)
|
|
229
|
+
when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
|
|
230
|
+
else node&.to_ruby
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def anchors
|
|
237
|
+
@anchors ||= {}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def visit_alias(node)
|
|
241
|
+
name = node.anchor
|
|
242
|
+
unless anchors.key?(name)
|
|
243
|
+
target = find_anchored(root_container(node), name)
|
|
244
|
+
anchors[name] = target ? visit(target) : nil
|
|
245
|
+
end
|
|
246
|
+
anchors[name]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# the walk for alias targets starts at the first node this
|
|
250
|
+
# visitor was handed (the document's root)
|
|
251
|
+
def root_container(_node)
|
|
252
|
+
@root
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def find_anchored(node, name)
|
|
256
|
+
return node if node.respond_to?(:anchor) && node.anchor == name
|
|
257
|
+
|
|
258
|
+
node.children.each do |c|
|
|
259
|
+
found = find_anchored(c, name)
|
|
260
|
+
return found if found
|
|
261
|
+
end
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def visit_scalar(node)
|
|
266
|
+
node.to_ruby
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def visit_sequence(node)
|
|
270
|
+
if node.tag == "!ruby/set"
|
|
271
|
+
set = ::Set.new
|
|
272
|
+
anchors[node.anchor] = set if node.anchor
|
|
273
|
+
node.children.each { |c| set << visit(c) }
|
|
274
|
+
return set
|
|
275
|
+
end
|
|
276
|
+
ary = []
|
|
277
|
+
anchors[node.anchor] = ary if node.anchor
|
|
278
|
+
node.children.each { |c| ary << visit(c) }
|
|
279
|
+
ary
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def visit_mapping(node)
|
|
283
|
+
tag = node.tag
|
|
284
|
+
return hash_into({}, node) if tag.nil? || tag.empty?
|
|
285
|
+
|
|
286
|
+
case tag
|
|
287
|
+
when "!ruby/set"
|
|
288
|
+
set = Set.new
|
|
289
|
+
anchors[node.anchor] = set if node.anchor
|
|
290
|
+
node.children.each_slice(2) { |_k, v| set << visit(v) }
|
|
291
|
+
set
|
|
292
|
+
when /\A!ruby\/struct(?::(.+))?\z/
|
|
293
|
+
klass = resolve_class(Regexp.last_match(1).to_s)
|
|
294
|
+
pairs = {}
|
|
295
|
+
anchors[node.anchor] = nil # placeholder: filled below
|
|
296
|
+
node.children.each_slice(2) do |k, v|
|
|
297
|
+
pairs[visit(k).to_s.sub(/\A:/, "")] = visit(v)
|
|
298
|
+
end
|
|
299
|
+
keys = pairs.keys.map(&:to_sym)
|
|
300
|
+
built =
|
|
301
|
+
if klass && klass <= Struct
|
|
302
|
+
klass.new(*pairs.values)
|
|
303
|
+
else
|
|
304
|
+
Struct.new(*keys).new(*pairs.values)
|
|
305
|
+
end
|
|
306
|
+
anchors[node.anchor] = built if node.anchor
|
|
307
|
+
built
|
|
308
|
+
when /\A!ruby\/object(?::(.+))?\z/
|
|
309
|
+
klass = resolve_class(Regexp.last_match(1).to_s)
|
|
310
|
+
revive_object(klass, node)
|
|
311
|
+
else
|
|
312
|
+
hash_into({}, node)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def hash_into(h, node)
|
|
317
|
+
anchors[node.anchor] = h if node.anchor
|
|
318
|
+
node.children.each_slice(2) do |k, v|
|
|
319
|
+
h[visit(k)] = visit(v)
|
|
320
|
+
end
|
|
321
|
+
h
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def revive_object(klass, node)
|
|
325
|
+
obj = klass ? klass.allocate : ::Object.new
|
|
326
|
+
anchors[node.anchor] = obj if node.anchor
|
|
327
|
+
node.children.each_slice(2) do |k, v|
|
|
328
|
+
key = k.to_ruby.to_s
|
|
329
|
+
if key == "__init__"
|
|
330
|
+
init_with(obj, v)
|
|
331
|
+
else
|
|
332
|
+
ivar = key.start_with?("@") ? key.to_sym : "@#{key}".to_sym
|
|
333
|
+
obj.instance_variable_set(ivar, visit(v))
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
obj
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def init_with(obj, value_node)
|
|
340
|
+
return unless obj.respond_to?(:init_with)
|
|
341
|
+
|
|
342
|
+
coder = ::Yeptris::Psych::CoderShim.new
|
|
343
|
+
if value_node.is_a?(::Yeptris::Psych::Nodes::Mapping)
|
|
344
|
+
value_node.children.each_slice(2) do |k, v|
|
|
345
|
+
coder[k.to_ruby.to_s] = visit(v)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
obj.init_with(coder)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def resolve_class(name)
|
|
352
|
+
return nil if name.nil? || name.empty?
|
|
353
|
+
|
|
354
|
+
klass = ::Object.const_get(name)
|
|
355
|
+
raise DisallowedClass, name unless klass.is_a?(Class) || klass.is_a?(Module)
|
|
356
|
+
|
|
357
|
+
klass
|
|
358
|
+
rescue ::NameError
|
|
359
|
+
nil
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|