From a397e4d4b0a0e7e8499a33ec760dba97ce494c63 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Oct 2024 15:41:01 -0400 Subject: [PATCH] [ruby/psych] Add support for ruby 3.2 Data objects https://github.com/ruby/psych/commit/788b844c83 --- ext/psych/lib/psych/class_loader.rb | 1 + ext/psych/lib/psych/visitors/to_ruby.rb | 22 +++++++++++++ ext/psych/lib/psych/visitors/yaml_tree.rb | 10 ++++++ test/psych/test_object_references.rb | 5 +++ test/psych/test_safe_load.rb | 32 +++++++++++++++++++ test/psych/test_yaml.rb | 39 +++++++++++++++++++++++ test/psych/visitors/test_yaml_tree.rb | 21 ++++++++++++ 7 files changed, 130 insertions(+) diff --git a/ext/psych/lib/psych/class_loader.rb b/ext/psych/lib/psych/class_loader.rb index 50efc35ee2..c8f509720a 100644 --- a/ext/psych/lib/psych/class_loader.rb +++ b/ext/psych/lib/psych/class_loader.rb @@ -6,6 +6,7 @@ module Psych class ClassLoader # :nodoc: BIG_DECIMAL = 'BigDecimal' COMPLEX = 'Complex' + DATA = 'Data' unless RUBY_VERSION < "3.2" DATE = 'Date' DATE_TIME = 'DateTime' EXCEPTION = 'Exception' diff --git a/ext/psych/lib/psych/visitors/to_ruby.rb b/ext/psych/lib/psych/visitors/to_ruby.rb index d8a203993b..e46026a528 100644 --- a/ext/psych/lib/psych/visitors/to_ruby.rb +++ b/ext/psych/lib/psych/visitors/to_ruby.rb @@ -197,6 +197,14 @@ module Psych s end + when /^!ruby\/data(?::(.*))?$/ + data = register(o, resolve_class($1).allocate) if $1 + members = {} + revive_data_members(members, o) + data ||= allocate_anon_data(o, members) + data.send(:initialize, **members) + data + when /^!ruby\/object:?(.*)?$/ name = $1 || 'Object' @@ -340,6 +348,20 @@ module Psych list end + def allocate_anon_data node, members + klass = class_loader.data.define(*members.keys) + register(node, klass.allocate) + end + + def revive_data_members hash, o + o.children.each_slice(2) do |k,v| + name = accept(k) + value = accept(v) + hash[class_loader.symbolize(name)] = value + end + hash + end + def revive_hash hash, o, tagged= false o.children.each_slice(2) { |k,v| key = accept(k) diff --git a/ext/psych/lib/psych/visitors/yaml_tree.rb b/ext/psych/lib/psych/visitors/yaml_tree.rb index a9476df96e..41b8069917 100644 --- a/ext/psych/lib/psych/visitors/yaml_tree.rb +++ b/ext/psych/lib/psych/visitors/yaml_tree.rb @@ -162,6 +162,16 @@ module Psych alias :visit_Delegator :visit_Object + def visit_Data o + tag = ['!ruby/data', o.class.name].compact.join(':') + register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK) + o.members.each do |member| + @emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY + accept o.send member + end + @emitter.end_mapping + end + def visit_Struct o tag = ['!ruby/struct', o.class.name].compact.join(':') diff --git a/test/psych/test_object_references.rb b/test/psych/test_object_references.rb index 86bb9034b9..0498d54eec 100644 --- a/test/psych/test_object_references.rb +++ b/test/psych/test_object_references.rb @@ -31,6 +31,11 @@ module Psych assert_reference_trip Struct.new(:foo).new(1) end + def test_data_has_references + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + assert_reference_trip Data.define(:foo).new(1) + end + def assert_reference_trip obj yml = Psych.dump([obj, obj]) assert_match(/\*-?\d+/, yml) diff --git a/test/psych/test_safe_load.rb b/test/psych/test_safe_load.rb index a9ed737528..e6ca1e142b 100644 --- a/test/psych/test_safe_load.rb +++ b/test/psych/test_safe_load.rb @@ -114,6 +114,38 @@ module Psych end end + D = Data.define(:d) unless RUBY_VERSION < "3.2" + + def test_data_depends_on_sym + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + assert_safe_cycle(D.new(nil), permitted_classes: [D, Symbol]) + assert_raise(Psych::DisallowedClass) do + cycle D.new(nil), permitted_classes: [D] + end + end + + def test_anon_data + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + assert Psych.safe_load(<<-eoyml, permitted_classes: [Data, Symbol]) +--- !ruby/data + foo: bar + eoyml + + assert_raise(Psych::DisallowedClass) do + Psych.safe_load(<<-eoyml, permitted_classes: [Data]) +--- !ruby/data + foo: bar + eoyml + end + + assert_raise(Psych::DisallowedClass) do + Psych.safe_load(<<-eoyml, permitted_classes: [Symbol]) +--- !ruby/data + foo: bar + eoyml + end + end + def test_safe_load_default_fallback assert_nil Psych.safe_load("") end diff --git a/test/psych/test_yaml.rb b/test/psych/test_yaml.rb index bb9b25f250..89bd3130e3 100644 --- a/test/psych/test_yaml.rb +++ b/test/psych/test_yaml.rb @@ -6,6 +6,7 @@ require_relative 'helper' # [ruby-core:01946] module Psych_Tests StructTest = Struct::new( :c ) + DataTest = Data.define( :c ) unless RUBY_VERSION < "3.2" end class Psych_Unit_Tests < Psych::TestCase @@ -1075,6 +1076,44 @@ EOY end + def test_ruby_data + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + Object.remove_const :MyBookData if Object.const_defined?(:MyBookData) + # Ruby Data value objects + book_class = Data.define(:author, :title, :year, :isbn) + Object.const_set(:MyBookData, book_class) + assert_to_yaml( + [ book_class.new( "Yukihiro Matsumoto", "Ruby in a Nutshell", 2002, "0-596-00214-9" ), + book_class.new( [ 'Dave Thomas', 'Andy Hunt' ], "The Pickaxe", 2002, + book_class.new( "This should be the ISBN", "but I have more data here", 2002, "None" ) + ) + ], <= 3.2" if RUBY_VERSION < "3.2" + assert_cycle D.new('bar') + end + + def test_data_anon + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + d = Data.define(:foo).new('bar') + obj = Psych.unsafe_load(Psych.dump(d)) + assert_equal d.foo, obj.foo + end + + def test_data_override_method + omit "Data requires ruby >= 3.2" if RUBY_VERSION < "3.2" + d = Data.define(:method).new('override') + obj = Psych.unsafe_load(Psych.dump(d)) + assert_equal d.method, obj.method + end + def test_exception ex = Exception.new 'foo' loaded = Psych.unsafe_load(Psych.dump(ex))