yeptris 0.1.13.3-x86_64-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 +126 -0
- data/ext/yeptris_native/extconf.rb +47 -0
- data/ext/yeptris_native/json_ruby.c +330 -0
- data/ext/yeptris_native/yeptris_native.c +438 -0
- data/lib/yeptris/document.rb +209 -0
- data/lib/yeptris/ffi.rb +256 -0
- data/lib/yeptris/json.rb +145 -0
- data/lib/yeptris/materializer.rb +347 -0
- data/lib/yeptris/native.so +0 -0
- data/lib/yeptris/node.rb +354 -0
- data/lib/yeptris/psych/coder_shim.rb +55 -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 +342 -0
- data/lib/yeptris/valueml.rb +442 -0
- data/lib/yeptris/yaml.rb +278 -0
- data/lib/yeptris.rb +70 -0
- data/libyeptris.so +0 -0
- metadata +82 -0
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TODO.restructure/23 — the typed opt-in marker for arbitrary-object
|
|
4
|
+
# dump/load. Classes include this module and implement
|
|
5
|
+
# encode_with(coder) / init_with(coder) (Psych's coder protocol).
|
|
6
|
+
# The visitors call those methods directly; the library never uses
|
|
7
|
+
# respond_to?, never reads or writes instance variables from outside
|
|
8
|
+
# the object's public surface (the encapsulation law).
|
|
9
|
+
module Yeptris
|
|
10
|
+
module Psych
|
|
11
|
+
module Encodable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -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 = Yeptris.read_input(yaml)
|
|
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,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
|