Skip to content

Commit c11b0e9

Browse files
Fix marshalling for virtual attributes on ActiveType::Object and ActiveType::Record
1 parent 976e94f commit c11b0e9

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

lib/active_type/marshalling.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module ActiveType
2+
module Marshalling
3+
# With 7.1 rails defines its own marshal_dump and marshal_load methods,
4+
# which selectively only dump and load the record´s attributes and some more stuff, but not our @virtual_attributes.
5+
# Whether these new methods are actually used, depends on ActiveRecord::Marshalling.format_version
6+
# For format_version = 6.1 active record uses the default ruby implementation for dumping and loading.
7+
# For format_version = 7.1 active record uses a custom implementation, which we need to override.
8+
#
9+
# format_version can also be dynamically changed during runtime, on change we need to define or undefine our marshal_dump dynamically, because:
10+
# * We cannot check the format_version at runtime within marshal_dump or marshal_load,
11+
# as we can´t just super to the default for the wrong version, because there is no method to super to.
12+
# (The default implementation is a ruby internal, not a real method.)
13+
# * We cannot override the methods at load time only when format version is 7.1,
14+
# because format version usually gets set after our initialisation and could change at any time.
15+
#
16+
# Two facts about ruby also help us with that (also see https://ruby-doc.org/core-2.6.8/Marshal.html):
17+
# * A custom marshal_load is only used, when marshal_dump is also defined. So we can keep marshal_dump always defined.
18+
# (If either is missing, ruby will use _dump and _load)
19+
# * If a serialized object is dumped using _dump it will be loaded using _load, never marshal_load, so a record
20+
# serialized with format_version = 6.1 using _dump, will always load using _load, ignoring whether marshal_load is defined or not.
21+
# This ensures objects will always be deserialized with the method they were serialized with. We don´t need to worry about that.
22+
23+
class << self
24+
attr_reader :format_version
25+
26+
def format_version=(version)
27+
case version
28+
when 6.1
29+
Methods.remove_method(:marshal_dump) if Methods.method_defined?(:marshal_dump)
30+
when 7.1
31+
Methods.alias_method(:marshal_dump, :_marshal_dump_7_1)
32+
else
33+
raise ArgumentError, "Unknown marshalling format: #{version.inspect}"
34+
end
35+
@format_version = version
36+
end
37+
end
38+
39+
module ActiveRecordMarshallingExtension
40+
def format_version=(version)
41+
ActiveType::Marshalling.format_version = version
42+
super(version)
43+
end
44+
end
45+
46+
module Methods
47+
def _marshal_dump_7_1
48+
[super, @virtual_attributes]
49+
end
50+
51+
def marshal_load(state)
52+
super_attributes, @virtual_attributes = state
53+
super(super_attributes)
54+
end
55+
end
56+
57+
end
58+
end
59+
60+
ActiveRecord::Marshalling.singleton_class.prepend(ActiveType::Marshalling::ActiveRecordMarshallingExtension)
61+
# Set ActiveType´s format_version to ActiveRecord´s, in case ActiveRecord uses the default value, which is set before we are loaded.
62+
ActiveType::Marshalling.format_version = ActiveRecord::Marshalling.format_version

lib/active_type/object.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'active_type/no_table'
22
require 'active_type/virtual_attributes'
33
require 'active_type/nested_attributes'
4+
require 'active_type/marshalling' if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
45

56
module ActiveType
67

@@ -9,6 +10,7 @@ class Object < ActiveRecord::Base
910
include NoTable
1011
include VirtualAttributes
1112
include NestedAttributes
13+
include Marshalling::Methods if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
1214

1315
end
1416

lib/active_type/record.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'active_type/record_extension'
33
require 'active_type/nested_attributes'
44
require 'active_type/change_association'
5+
require 'active_type/marshalling' if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
56

67
module ActiveType
78

@@ -13,6 +14,7 @@ class Record < ActiveRecord::Base
1314
include NestedAttributes
1415
include RecordExtension
1516
include ChangeAssociation
17+
include Marshalling::Methods if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
1618

1719
end
1820

spec/active_type/object_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,81 @@ class ObjectWithUnsupportedTypes < Object
531531
end
532532
end
533533

534+
describe "marshalling" do
535+
shared_examples "marshalling attributes" do
536+
it "marshals attributes properly" do
537+
object = ObjectSpec::Object.create(
538+
virtual_string: "foobar",
539+
virtual_integer: 123,
540+
virtual_time: Time.parse("12:00 15.10.2025"),
541+
virtual_date: Date.parse("15.10.2025"),
542+
virtual_boolean: true,
543+
virtual_attribute: { some: "random object" },
544+
virtual_type_attribute: "ObjectSpec::Object::PlainObject",
545+
)
546+
547+
serialized_object = Marshal.dump(object)
548+
deserialized_object = Marshal.load(serialized_object)
549+
550+
expect(deserialized_object.virtual_string).to eq "foobar"
551+
expect(deserialized_object.virtual_integer).to eq 123
552+
expect(deserialized_object.virtual_time).to eq Time.parse("12:00 15.10.2025")
553+
expect(deserialized_object.virtual_date).to eq Date.parse("15.10.2025")
554+
expect(deserialized_object.virtual_boolean).to eq true
555+
expect(deserialized_object.virtual_attribute).to eq({ some: "random object" })
556+
expect(deserialized_object.virtual_type_attribute).to eq "ObjectSpec::Object::PlainObject"
557+
end
558+
end
559+
560+
if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
561+
context 'for 6.1 marshalling' do
562+
before do
563+
ActiveRecord::Marshalling.format_version = 6.1
564+
end
565+
566+
include_examples "marshalling attributes"
567+
end
568+
569+
context 'for 7.1 marshalling' do
570+
before do
571+
ActiveRecord::Marshalling.format_version = 7.1
572+
end
573+
574+
include_examples "marshalling attributes"
575+
end
576+
577+
describe "loading a object marshalled with format version 6.1, but the current version is 7.1" do
578+
it "marshals attributes properly" do
579+
object = ObjectSpec::Object.create(virtual_attribute: "foobar")
580+
581+
ActiveRecord::Marshalling.format_version = 6.1
582+
serialized_object = Marshal.dump(object)
583+
584+
ActiveRecord::Marshalling.format_version = 7.1
585+
deserialized_object = Marshal.load(serialized_object)
586+
587+
expect(deserialized_object.virtual_attribute).to eq "foobar"
588+
end
589+
end
590+
591+
describe "loading a object marshalled with format version 7.1, but the current version is 6.1" do
592+
it "marshals attributes properly" do
593+
object = ObjectSpec::Object.create(virtual_attribute: "foobar")
594+
595+
ActiveRecord::Marshalling.format_version = 7.1
596+
serialized_object = Marshal.dump(object)
597+
598+
ActiveRecord::Marshalling.format_version = 6.1
599+
deserialized_object = Marshal.load(serialized_object)
600+
601+
expect(deserialized_object.virtual_attribute).to eq "foobar"
602+
end
603+
end
604+
605+
else
606+
include_examples "marshalling attributes"
607+
end
608+
609+
end
610+
534611
end

spec/active_type/record_spec.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,88 @@ class RecordWithOptionalBelongsToFlippedValidatesForeignKey < Record
376376
end
377377
end
378378

379+
describe "marshalling" do
380+
shared_examples "marshalling attributes" do
381+
it "marshals attributes properly" do
382+
object = RecordSpec::Record.create!(
383+
virtual_string: "foobar",
384+
virtual_integer: 123,
385+
virtual_time: Time.parse("12:00 15.10.2025"),
386+
virtual_date: Date.parse("15.10.2025"),
387+
virtual_boolean: true,
388+
virtual_attribute: { some: "random object" },
389+
virtual_type_attribute: "RecordSpec::Record",
390+
persisted_string: "a real active record attribute"
391+
)
392+
393+
serialized_object = Marshal.dump(object)
394+
deserialized_object = Marshal.load(serialized_object)
395+
396+
expect(deserialized_object.virtual_string).to eq "foobar"
397+
expect(deserialized_object.virtual_integer).to eq 123
398+
expect(deserialized_object.virtual_time).to eq Time.parse("12:00 15.10.2025")
399+
expect(deserialized_object.virtual_date).to eq Date.parse("15.10.2025")
400+
expect(deserialized_object.virtual_boolean).to eq true
401+
expect(deserialized_object.virtual_attribute).to eq({ some: "random object" })
402+
expect(deserialized_object.virtual_type_attribute).to eq "RecordSpec::Record"
403+
expect(deserialized_object.persisted_string).to eq "a real active record attribute"
404+
end
405+
end
406+
407+
if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
408+
context 'for 6.1 marshalling' do
409+
before do
410+
ActiveRecord::Marshalling.format_version = 6.1
411+
end
412+
413+
include_examples "marshalling attributes"
414+
end
415+
416+
context 'for 7.1 marshalling' do
417+
before do
418+
ActiveRecord::Marshalling.format_version = 7.1
419+
end
420+
421+
include_examples "marshalling attributes"
422+
end
423+
424+
describe "loading a object marshalled with format version 6.1, but the current version is 7.1" do
425+
it "marshals attributes properly" do
426+
object = RecordSpec::Record.create!(
427+
virtual_string: "foo",
428+
persisted_string: "bar"
429+
)
430+
431+
ActiveRecord::Marshalling.format_version = 6.1
432+
serialized_object = Marshal.dump(object)
433+
434+
ActiveRecord::Marshalling.format_version = 7.1
435+
deserialized_object = Marshal.load(serialized_object)
436+
437+
expect(deserialized_object.virtual_string).to eq "foo"
438+
expect(deserialized_object.persisted_string).to eq "bar"
439+
end
440+
end
441+
442+
describe "loading a object marshalled with format version 7.1, but the current version is 6.1" do
443+
it "marshals attributes properly" do
444+
object = RecordSpec::Record.create!(
445+
virtual_string: "foo",
446+
persisted_string: "bar"
447+
)
448+
449+
ActiveRecord::Marshalling.format_version = 7.1
450+
serialized_object = Marshal.dump(object)
451+
452+
ActiveRecord::Marshalling.format_version = 6.1
453+
deserialized_object = Marshal.load(serialized_object)
454+
455+
expect(deserialized_object.virtual_string).to eq "foo"
456+
expect(deserialized_object.persisted_string).to eq "bar"
457+
end
458+
end
459+
else
460+
include_examples "marshalling attributes"
461+
end
462+
end
379463
end

0 commit comments

Comments
 (0)