Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## 2.6.5 (2025-10-16)

* Fixed: ActiveType::Object and ActiveType::Record are now serialized/deserialized correctly using Marshal.dump/Marshal.load

## 2.6.4 (2025-09-11)

* Fixed: When using nests_many, nested updates, with preloaded records, using string ids will no longer cause additional DB queries.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.6.1.pg.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.6.1.sqlite3.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.7.1.pg.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.7.1.sqlite3.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.7.2.mysql2.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.7.2.pg.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.7.2.sqlite3.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.8.0.sqlite3.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
active_type (2.6.4)
active_type (2.6.5)
activerecord (>= 6.1)

GEM
Expand Down
62 changes: 62 additions & 0 deletions lib/active_type/marshalling.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module ActiveType
module Marshalling
# With 7.1 rails defines its own marshal_dump and marshal_load methods,
# which selectively only dump and load the record´s attributes and some more stuff, but not our @virtual_attributes.
# Whether these new methods are actually used, depends on ActiveRecord::Marshalling.format_version
# For format_version = 6.1 active record uses the default ruby implementation for dumping and loading.
# For format_version = 7.1 active record uses a custom implementation, which we need to override.
#
# format_version can also be dynamically changed during runtime, on change we need to define or undefine our marshal_dump dynamically, because:
# * We cannot check the format_version at runtime within marshal_dump or marshal_load,
# as we can´t just super to the default for the wrong version, because there is no method to super to.
# (The default implementation is a ruby internal, not a real method.)
# * We cannot override the methods at load time only when format version is 7.1,
# because format version usually gets set after our initialisation and could change at any time.
#
# Two facts about ruby also help us with that (also see https://ruby-doc.org/core-2.6.8/Marshal.html):
# * A custom marshal_load is only used, when marshal_dump is also defined. So we can keep marshal_dump always defined.
# (If either is missing, ruby will use _dump and _load)
# * If a serialized object is dumped using _dump it will be loaded using _load, never marshal_load, so a record
# serialized with format_version = 6.1 using _dump, will always load using _load, ignoring whether marshal_load is defined or not.
# This ensures objects will always be deserialized with the method they were serialized with. We don´t need to worry about that.

class << self
attr_reader :format_version

def format_version=(version)
case version
when 6.1
Methods.remove_method(:marshal_dump) if Methods.method_defined?(:marshal_dump)
when 7.1
Methods.alias_method(:marshal_dump, :_marshal_dump_7_1)
else
raise ArgumentError, "Unknown marshalling format: #{version.inspect}"
end
@format_version = version
end
end

module ActiveRecordMarshallingExtension
def format_version=(version)
ActiveType::Marshalling.format_version = version
super(version)
end
end

module Methods
def _marshal_dump_7_1
[super, @virtual_attributes]
end

def marshal_load(state)
super_attributes, @virtual_attributes = state
super(super_attributes)
end
end

end
end

ActiveRecord::Marshalling.singleton_class.prepend(ActiveType::Marshalling::ActiveRecordMarshallingExtension)
# Set ActiveType´s format_version to ActiveRecord´s, in case ActiveRecord uses the default value, which is set before we are loaded.
ActiveType::Marshalling.format_version = ActiveRecord::Marshalling.format_version
2 changes: 2 additions & 0 deletions lib/active_type/object.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'active_type/no_table'
require 'active_type/virtual_attributes'
require 'active_type/nested_attributes'
require 'active_type/marshalling' if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1

module ActiveType

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

end

Expand Down
2 changes: 2 additions & 0 deletions lib/active_type/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'active_type/record_extension'
require 'active_type/nested_attributes'
require 'active_type/change_association'
require 'active_type/marshalling' if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1

module ActiveType

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

end

Expand Down
2 changes: 1 addition & 1 deletion lib/active_type/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActiveType
VERSION = '2.6.4'
VERSION = '2.6.5'
end
77 changes: 77 additions & 0 deletions spec/active_type/object_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -531,4 +531,81 @@ class ObjectWithUnsupportedTypes < Object
end
end

describe "marshalling" do
shared_examples "marshalling attributes" do
it "marshals attributes properly" do
object = ObjectSpec::Object.create(
virtual_string: "foobar",
virtual_integer: 123,
virtual_time: Time.parse("12:00 15.10.2025"),
virtual_date: Date.parse("15.10.2025"),
virtual_boolean: true,
virtual_attribute: { some: "random object" },
virtual_type_attribute: "ObjectSpec::Object::PlainObject",
)

serialized_object = Marshal.dump(object)
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_string).to eq "foobar"
expect(deserialized_object.virtual_integer).to eq 123
expect(deserialized_object.virtual_time).to eq Time.parse("12:00 15.10.2025")
expect(deserialized_object.virtual_date).to eq Date.parse("15.10.2025")
expect(deserialized_object.virtual_boolean).to eq true
expect(deserialized_object.virtual_attribute).to eq({ some: "random object" })
expect(deserialized_object.virtual_type_attribute).to eq "ObjectSpec::Object::PlainObject"
end
end

if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
context 'for 6.1 marshalling' do
before do
ActiveRecord::Marshalling.format_version = 6.1
end

include_examples "marshalling attributes"
end

context 'for 7.1 marshalling' do
before do
ActiveRecord::Marshalling.format_version = 7.1
end

include_examples "marshalling attributes"
end

describe "loading a object marshalled with format version 6.1, but the current version is 7.1" do
it "marshals attributes properly" do
object = ObjectSpec::Object.create(virtual_attribute: "foobar")

ActiveRecord::Marshalling.format_version = 6.1
serialized_object = Marshal.dump(object)

ActiveRecord::Marshalling.format_version = 7.1
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_attribute).to eq "foobar"
end
end

describe "loading a object marshalled with format version 7.1, but the current version is 6.1" do
it "marshals attributes properly" do
object = ObjectSpec::Object.create(virtual_attribute: "foobar")

ActiveRecord::Marshalling.format_version = 7.1
serialized_object = Marshal.dump(object)

ActiveRecord::Marshalling.format_version = 6.1
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_attribute).to eq "foobar"
end
end

else
include_examples "marshalling attributes"
end

end

end
84 changes: 84 additions & 0 deletions spec/active_type/record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,88 @@ class RecordWithOptionalBelongsToFlippedValidatesForeignKey < Record
end
end

describe "marshalling" do
shared_examples "marshalling attributes" do
it "marshals attributes properly" do
object = RecordSpec::Record.create!(
virtual_string: "foobar",
virtual_integer: 123,
virtual_time: Time.parse("12:00 15.10.2025"),
virtual_date: Date.parse("15.10.2025"),
virtual_boolean: true,
virtual_attribute: { some: "random object" },
virtual_type_attribute: "RecordSpec::Record",
persisted_string: "a real active record attribute"
)

serialized_object = Marshal.dump(object)
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_string).to eq "foobar"
expect(deserialized_object.virtual_integer).to eq 123
expect(deserialized_object.virtual_time).to eq Time.parse("12:00 15.10.2025")
expect(deserialized_object.virtual_date).to eq Date.parse("15.10.2025")
expect(deserialized_object.virtual_boolean).to eq true
expect(deserialized_object.virtual_attribute).to eq({ some: "random object" })
expect(deserialized_object.virtual_type_attribute).to eq "RecordSpec::Record"
expect(deserialized_object.persisted_string).to eq "a real active record attribute"
end
end

if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
context 'for 6.1 marshalling' do
before do
ActiveRecord::Marshalling.format_version = 6.1
end

include_examples "marshalling attributes"
end

context 'for 7.1 marshalling' do
before do
ActiveRecord::Marshalling.format_version = 7.1
end

include_examples "marshalling attributes"
end

describe "loading a object marshalled with format version 6.1, but the current version is 7.1" do
it "marshals attributes properly" do
object = RecordSpec::Record.create!(
virtual_string: "foo",
persisted_string: "bar"
)

ActiveRecord::Marshalling.format_version = 6.1
serialized_object = Marshal.dump(object)

ActiveRecord::Marshalling.format_version = 7.1
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_string).to eq "foo"
expect(deserialized_object.persisted_string).to eq "bar"
end
end

describe "loading a object marshalled with format version 7.1, but the current version is 6.1" do
it "marshals attributes properly" do
object = RecordSpec::Record.create!(
virtual_string: "foo",
persisted_string: "bar"
)

ActiveRecord::Marshalling.format_version = 7.1
serialized_object = Marshal.dump(object)

ActiveRecord::Marshalling.format_version = 6.1
deserialized_object = Marshal.load(serialized_object)

expect(deserialized_object.virtual_string).to eq "foo"
expect(deserialized_object.persisted_string).to eq "bar"
end
end
else
include_examples "marshalling attributes"
end
end
end