Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Data in Marshal #2385

Merged
merged 5 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions spec/core/marshal/dump_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,23 +316,17 @@ def _dump(level)
ruby_version_is "3.2" do
describe "with a Data" do
it "dumps a Data" do
NATFIXME 'dump Data', exception: SpecFailedException do
Marshal.dump(MarshalSpec::DataSpec::Measure.new(100, 'km')).should == "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
end
Marshal.dump(MarshalSpec::DataSpec::Measure.new(100, 'km')).should == "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
end

it "dumps an extended Data" do
NATFIXME 'dump Data', exception: SpecFailedException do
obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km")
Marshal.dump(obj).should == "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm"
end
obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km")
Marshal.dump(obj).should == "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm"
end

it "ignores overridden name method" do
NATFIXME 'dump Data', exception: SpecFailedException do
obj = MarshalSpec::DataSpec::MeasureWithOverriddenName.new(100, "km")
Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm"
end
obj = MarshalSpec::DataSpec::MeasureWithOverriddenName.new(100, "km")
Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm"
end

it "raises TypeError with an anonymous Struct" do
Expand Down
38 changes: 14 additions & 24 deletions spec/core/marshal/shared/load.rb
Original file line number Diff line number Diff line change
Expand Up @@ -830,20 +830,18 @@ def io.binmode; raise "binmode"; end
it "loads a struct having ivar" do
obj = Struct.new("Thick").new
obj.instance_variable_set(:@foo, 5)
NATFIXME 'loads a struct having ivar', exception: ArgumentError, message: 'dump format error' do
NATFIXME 'ivar names', exception: NameError, message: "`@@foo' is not allowed as an instance variable name" do
reloaded = Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n")
reloaded.should == obj
reloaded.instance_variable_get(:@foo).should == 5
Struct.send(:remove_const, :Thick)
end
Struct.send(:remove_const, :Thick)
end

it "loads a struct having fields" do
obj = Struct.new("Ure1", :a, :b).new
NATFIXME 'loads a struct having fields', exception: ArgumentError, message: 'dump format error' do
Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj
Struct.send(:remove_const, :Ure1)
end
Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj
Struct.send(:remove_const, :Ure1)
end

it "does not call initialize on the unmarshaled struct" do
Expand All @@ -855,13 +853,11 @@ def io.binmode; raise "binmode"; end

Thread.current[threadlocal_key] = nil

NATFIXME "It can't find the class for some reason", exception: ArgumentError, message: 'undefined class/module MarshalSpec::StructWithUserInitialize' do
dumped = Marshal.dump(s)
loaded = Marshal.send(@method, dumped)
dumped = Marshal.dump(s)
loaded = Marshal.send(@method, dumped)

Thread.current[threadlocal_key].should == nil
loaded.a.should == 'foo'
end
Thread.current[threadlocal_key].should == nil
loaded.a.should == 'foo'
end
end

Expand All @@ -870,31 +866,25 @@ def io.binmode; raise "binmode"; end
it "loads a Data" do
obj = MarshalSpec::DataSpec::Measure.new(100, 'km')
dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
NATFIXME 'dump and load Data', exception: SpecFailedException do
Marshal.dump(obj).should == dumped
Marshal.dump(obj).should == dumped

Marshal.send(@method, dumped).should == obj
end
Marshal.send(@method, dumped).should == obj
end

it "loads an extended Data" do
obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km")
dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm"
NATFIXME 'dump and load Data', exception: SpecFailedException do
Marshal.dump(obj).should == dumped
Marshal.dump(obj).should == dumped

Marshal.send(@method, dumped).should == obj
end
Marshal.send(@method, dumped).should == obj
end

it "returns a frozen object" do
obj = MarshalSpec::DataSpec::Measure.new(100, 'km')
dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
NATFIXME 'dump and load Data', exception: SpecFailedException do
Marshal.dump(obj).should == dumped
Marshal.dump(obj).should == dumped

Marshal.send(@method, dumped).should.frozen?
end
Marshal.send(@method, dumped).should.frozen?
end
end
end
Expand Down
6 changes: 5 additions & 1 deletion src/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class Data
def self.define(*members, &block)
members = members.map(&:to_sym)

Class.new do
Class.new(Data) do
members.each do |name|
define_method(name) { instance_variable_get(:"@#{name}") }
end
Expand Down Expand Up @@ -55,6 +55,10 @@ def self.define(*members, &block)
end
end

define_method :== do |other|
self.class == other.class && to_h == other.to_h
end

define_singleton_method(:[]) { |*args, **kwargs| new(*args, **kwargs) }

define_singleton_method(:members) { members }
Expand Down
37 changes: 36 additions & 1 deletion src/marshal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ def write_regexp(value)
write_encoding_bytes(value)
end

def write_data(value)
raise TypeError, "can't dump anonymous class #{value.class}" if value.class.name.nil?
write_char('S')
write(value.class.to_s.to_sym)
values = value.to_h
write_integer_bytes(values.size)
values.each do |name, value|
write(name)
write(value)
end
end

def write_user_marshaled_object_with_allocate(value)
write_char('U')
write(value.class.to_s.to_sym)
Expand Down Expand Up @@ -314,6 +326,8 @@ def write(value)
write_module(value)
elsif value.is_a?(Regexp)
write_regexp(value)
elsif value.is_a?(Data)
write_data(value)
elsif value.respond_to?(:marshal_dump, true)
write_user_marshaled_object_with_allocate(value)
elsif value.respond_to?(:_dump, true)
Expand Down Expand Up @@ -515,6 +529,24 @@ def read_user_marshaled_object_without_allocate
object_class._load(data)
end

def read_struct
name = read_value
object_class = find_constant(name)
size = read_integer
values = size.times.each_with_object({}) do |_, tmp|
name = read_value
value = read_value
tmp[name] = value
end
if object_class.ancestors.include?(Data)
object_class.new(**values)
else
object = object_class.allocate
values.each { |k, v| object.send(:"#{k}=", v) }
object
end
end

def read_object
name = read_value
object_class = find_constant(name)
Expand Down Expand Up @@ -581,6 +613,8 @@ def read_value
read_user_marshaled_object_with_allocate
when 'u'
read_user_marshaled_object_without_allocate
when 'S'
read_struct
when 'o'
read_object
when 'I'
Expand All @@ -594,7 +628,8 @@ def read_value

def find_constant(name)
begin
Object.const_get(name)
# NATFIXME: Should be supported directly in const_get
name.to_s.split('::').reduce(Object) { |memo, part| memo.const_get(part) }
rescue NameError
raise ArgumentError, "undefined class/module #{name}"
end
Expand Down
53 changes: 53 additions & 0 deletions test/natalie/data_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@
it 'results in a readable representation' do
1.should == 1
end

it 'should be a subclass of Data' do
1.should == 1
end

it "returns true if the other is the same object" do
1.should == 1
end

it "returns true if the other has all the same fields" do
1.should == 1
end

it "returns false if the other is a different object or has different fields" do
1.should == 1
end

it "returns false if other is of a different class" do
1.should == 1
end
end
end

Expand All @@ -27,4 +47,37 @@
data.to_s.should == '#<data DataSpecs::Measure amount=42, unit="km">'
end
end

describe 'Data#is_a?' do
it 'should be a subclass of Data' do
data = DataSpecs::Measure.new(amount: 42, unit: 'km')
data.should be_kind_of(Data)
end
end

describe 'Data#==' do
it "returns true if the other is the same object" do
measure = same_measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
measure.should == same_measure
end

it "returns true if the other has all the same fields" do
measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
similar_measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
measure.should == similar_measure
end

it "returns false if the other is a different object or has different fields" do
measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
different_measure = DataSpecs::Measure.new(amount: 26, unit: 'miles')
measure.should != different_measure
end

it "returns false if other is of a different class" do
measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
klass = Data.define(:amount, :unit)
clone = klass.new(amount: 42, unit: 'km')
measure.should != clone
end
end
end
Loading