Skip to content

Commit 992297d

Browse files
authored
Merge pull request #692 from nevans/data-object-encoding
Data object encoding
2 parents 7d03b91 + 3573fb3 commit 992297d

File tree

10 files changed

+273
-0
lines changed

10 files changed

+273
-0
lines changed

ext/psych/psych_to_ruby.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ static VALUE path2class(VALUE self, VALUE path)
2424
return rb_path_to_class(path);
2525
}
2626

27+
static VALUE init_struct(VALUE self, VALUE data, VALUE attrs)
28+
{
29+
VALUE args = rb_ary_new2(1);
30+
rb_ary_push(args, attrs);
31+
rb_struct_initialize(data, args);
32+
33+
return data;
34+
}
35+
2736
void Init_psych_to_ruby(void)
2837
{
2938
VALUE psych = rb_define_module("Psych");
@@ -33,6 +42,7 @@ void Init_psych_to_ruby(void)
3342
VALUE visitor = rb_define_class_under(visitors, "Visitor", rb_cObject);
3443
cPsychVisitorsToRuby = rb_define_class_under(visitors, "ToRuby", visitor);
3544

45+
rb_define_private_method(cPsychVisitorsToRuby, "init_struct", init_struct, 2);
3646
rb_define_private_method(cPsychVisitorsToRuby, "build_exception", build_exception, 2);
3747
rb_define_private_method(class_loader, "path2class", path2class, 1);
3848
}

lib/psych/class_loader.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Psych
66
class ClassLoader # :nodoc:
77
BIG_DECIMAL = 'BigDecimal'
88
COMPLEX = 'Complex'
9+
DATA = 'Data' unless RUBY_VERSION < "3.2"
910
DATE = 'Date'
1011
DATE_TIME = 'DateTime'
1112
EXCEPTION = 'Exception'

lib/psych/visitors/to_ruby.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,32 @@ def visit_Psych_Nodes_Mapping o
197197
s
198198
end
199199

200+
when /^!ruby\/data(-with-ivars)?(?::(.*))?$/
201+
data = register(o, resolve_class($2).allocate) if $2
202+
members = {}
203+
204+
if $1 # data-with-ivars
205+
ivars = {}
206+
o.children.each_slice(2) do |type, vars|
207+
case accept(type)
208+
when 'members'
209+
revive_data_members(members, vars)
210+
data ||= allocate_anon_data(o, members)
211+
when 'ivars'
212+
revive_hash(ivars, vars)
213+
end
214+
end
215+
ivars.each do |ivar, v|
216+
data.instance_variable_set ivar, v
217+
end
218+
else
219+
revive_data_members(members, o)
220+
end
221+
data ||= allocate_anon_data(o, members)
222+
init_struct(data, **members)
223+
data.freeze
224+
data
225+
200226
when /^!ruby\/object:?(.*)?$/
201227
name = $1 || 'Object'
202228

@@ -340,6 +366,20 @@ def register_empty object
340366
list
341367
end
342368

369+
def allocate_anon_data node, members
370+
klass = class_loader.data.define(*members.keys)
371+
register(node, klass.allocate)
372+
end
373+
374+
def revive_data_members hash, o
375+
o.children.each_slice(2) do |k,v|
376+
name = accept(k)
377+
value = accept(v)
378+
hash[class_loader.symbolize(name)] = value
379+
end
380+
hash
381+
end
382+
343383
def revive_hash hash, o, tagged= false
344384
o.children.each_slice(2) { |k,v|
345385
key = accept(k)

lib/psych/visitors/yaml_tree.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,44 @@ def visit_Object o
162162

163163
alias :visit_Delegator :visit_Object
164164

165+
def visit_Data o
166+
ivars = o.instance_variables
167+
if ivars.empty?
168+
tag = ['!ruby/data', o.class.name].compact.join(':')
169+
register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
170+
o.members.each do |member|
171+
@emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
172+
accept o.send member
173+
end
174+
@emitter.end_mapping
175+
176+
else
177+
tag = ['!ruby/data-with-ivars', o.class.name].compact.join(':')
178+
node = @emitter.start_mapping(nil, tag, false, Psych::Nodes::Mapping::BLOCK)
179+
register(o, node)
180+
181+
# Dump the members
182+
accept 'members'
183+
@emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
184+
o.members.each do |member|
185+
@emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
186+
accept o.send member
187+
end
188+
@emitter.end_mapping
189+
190+
# Dump the ivars
191+
accept 'ivars'
192+
@emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
193+
ivars.each do |ivar|
194+
accept ivar.to_s
195+
accept o.instance_variable_get ivar
196+
end
197+
@emitter.end_mapping
198+
199+
@emitter.end_mapping
200+
end
201+
end
202+
165203
def visit_Struct o
166204
tag = ['!ruby/struct', o.class.name].compact.join(':')
167205

test/psych/test_data.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# frozen_string_literal: true
2+
require_relative 'helper'
3+
4+
class PsychDataWithIvar < Data.define(:foo)
5+
attr_reader :bar
6+
def initialize(**)
7+
@bar = 'hello'
8+
super
9+
end
10+
end unless RUBY_VERSION < "3.2"
11+
12+
module Psych
13+
class TestData < TestCase
14+
class SelfReferentialData < Data.define(:foo)
15+
attr_accessor :ref
16+
def initialize(foo:)
17+
@ref = self
18+
super
19+
end
20+
end unless RUBY_VERSION < "3.2"
21+
22+
def setup
23+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
24+
end
25+
26+
# TODO: move to another test?
27+
def test_dump_data
28+
assert_equal <<~eoyml, Psych.dump(PsychDataWithIvar["bar"])
29+
--- !ruby/data-with-ivars:PsychDataWithIvar
30+
members:
31+
foo: bar
32+
ivars:
33+
"@bar": hello
34+
eoyml
35+
end
36+
37+
def test_self_referential_data
38+
circular = SelfReferentialData.new("foo")
39+
40+
loaded = Psych.unsafe_load(Psych.dump(circular))
41+
assert_instance_of(SelfReferentialData, loaded.ref)
42+
43+
assert_equal(circular, loaded)
44+
assert_same(loaded, loaded.ref)
45+
end
46+
47+
def test_roundtrip
48+
thing = PsychDataWithIvar.new("bar")
49+
data = Psych.unsafe_load(Psych.dump(thing))
50+
51+
assert_equal "hello", data.bar
52+
assert_equal "bar", data.foo
53+
end
54+
55+
def test_load
56+
obj = Psych.unsafe_load(<<~eoyml)
57+
--- !ruby/data-with-ivars:PsychDataWithIvar
58+
members:
59+
foo: bar
60+
ivars:
61+
"@bar": hello
62+
eoyml
63+
64+
assert_equal "hello", obj.bar
65+
assert_equal "bar", obj.foo
66+
end
67+
end
68+
end
69+

test/psych/test_object_references.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def test_struct_has_references
3131
assert_reference_trip Struct.new(:foo).new(1)
3232
end
3333

34+
def test_data_has_references
35+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
36+
assert_reference_trip Data.define(:foo).new(1)
37+
end
38+
3439
def assert_reference_trip obj
3540
yml = Psych.dump([obj, obj])
3641
assert_match(/\*-?\d+/, yml)

test/psych/test_safe_load.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,38 @@ def test_anon_struct
114114
end
115115
end
116116

117+
D = Data.define(:d) unless RUBY_VERSION < "3.2"
118+
119+
def test_data_depends_on_sym
120+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
121+
assert_safe_cycle(D.new(nil), permitted_classes: [D, Symbol])
122+
assert_raise(Psych::DisallowedClass) do
123+
cycle D.new(nil), permitted_classes: [D]
124+
end
125+
end
126+
127+
def test_anon_data
128+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
129+
assert Psych.safe_load(<<-eoyml, permitted_classes: [Data, Symbol])
130+
--- !ruby/data
131+
foo: bar
132+
eoyml
133+
134+
assert_raise(Psych::DisallowedClass) do
135+
Psych.safe_load(<<-eoyml, permitted_classes: [Data])
136+
--- !ruby/data
137+
foo: bar
138+
eoyml
139+
end
140+
141+
assert_raise(Psych::DisallowedClass) do
142+
Psych.safe_load(<<-eoyml, permitted_classes: [Symbol])
143+
--- !ruby/data
144+
foo: bar
145+
eoyml
146+
end
147+
end
148+
117149
def test_safe_load_default_fallback
118150
assert_nil Psych.safe_load("")
119151
end

test/psych/test_serialize_subclasses.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,23 @@ def test_struct_subclass
3535
so = StructSubclass.new('foo', [1,2,3])
3636
assert_equal so, Psych.unsafe_load(Psych.dump(so))
3737
end
38+
39+
class DataSubclass < Data.define(:foo)
40+
def initialize(foo:)
41+
@bar = "hello #{foo}"
42+
super(foo: foo)
43+
end
44+
45+
def == other
46+
super(other) && @bar == other.instance_eval{ @bar }
47+
end
48+
end unless RUBY_VERSION < "3.2"
49+
50+
def test_data_subclass
51+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
52+
so = DataSubclass.new('foo')
53+
assert_equal so, Psych.unsafe_load(Psych.dump(so))
54+
end
55+
3856
end
3957
end

test/psych/test_yaml.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# [ruby-core:01946]
77
module Psych_Tests
88
StructTest = Struct::new( :c )
9+
DataTest = Data.define( :c ) unless RUBY_VERSION < "3.2"
910
end
1011

1112
class Psych_Unit_Tests < Psych::TestCase
@@ -1075,6 +1076,44 @@ def test_ruby_struct
10751076

10761077
end
10771078

1079+
def test_ruby_data
1080+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
1081+
Object.remove_const :MyBookData if Object.const_defined?(:MyBookData)
1082+
# Ruby Data value objects
1083+
book_class = Data.define(:author, :title, :year, :isbn)
1084+
Object.const_set(:MyBookData, book_class)
1085+
assert_to_yaml(
1086+
[ book_class.new( "Yukihiro Matsumoto", "Ruby in a Nutshell", 2002, "0-596-00214-9" ),
1087+
book_class.new( [ 'Dave Thomas', 'Andy Hunt' ], "The Pickaxe", 2002,
1088+
book_class.new( "This should be the ISBN", "but I have more data here", 2002, "None" )
1089+
)
1090+
], <<EOY
1091+
- !ruby/data:MyBookData
1092+
author: Yukihiro Matsumoto
1093+
title: Ruby in a Nutshell
1094+
year: 2002
1095+
isbn: 0-596-00214-9
1096+
- !ruby/data:MyBookData
1097+
author:
1098+
- Dave Thomas
1099+
- Andy Hunt
1100+
title: The Pickaxe
1101+
year: 2002
1102+
isbn: !ruby/data:MyBookData
1103+
author: This should be the ISBN
1104+
title: but I have more data here
1105+
year: 2002
1106+
isbn: None
1107+
EOY
1108+
)
1109+
1110+
assert_to_yaml( Psych_Tests::DataTest.new( 123 ), <<EOY )
1111+
--- !ruby/data:Psych_Tests::DataTest
1112+
c: 123
1113+
EOY
1114+
1115+
end
1116+
10781117
def test_ruby_rational
10791118
assert_to_yaml( Rational(1, 2), <<EOY )
10801119
--- !ruby/object:Rational

test/psych/visitors/test_yaml_tree.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,27 @@ def test_override_method
7373
assert_equal s.method, obj.method
7474
end
7575

76+
D = Data.define(:foo) unless RUBY_VERSION < "3.2"
77+
78+
def test_data
79+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
80+
assert_cycle D.new('bar')
81+
end
82+
83+
def test_data_anon
84+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
85+
d = Data.define(:foo).new('bar')
86+
obj = Psych.unsafe_load(Psych.dump(d))
87+
assert_equal d.foo, obj.foo
88+
end
89+
90+
def test_data_override_method
91+
omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2"
92+
d = Data.define(:method).new('override')
93+
obj = Psych.unsafe_load(Psych.dump(d))
94+
assert_equal d.method, obj.method
95+
end
96+
7697
def test_exception
7798
ex = Exception.new 'foo'
7899
loaded = Psych.unsafe_load(Psych.dump(ex))

0 commit comments

Comments
 (0)