Skip to content

Commit 60e979d

Browse files
authored
Merge pull request rails#41139 from kamipo/fix_decorated_type_with_serialized_attribute
Fix decorated type with `type_for_attribute` on the serialized attribute
2 parents db79da6 + 70259f5 commit 60e979d

File tree

4 files changed

+88
-20
lines changed

4 files changed

+88
-20
lines changed

activerecord/lib/active_record/attribute_methods/serialization.rb

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def serialize(attr_name, class_name_or_coder = Object, **options)
133133
raise ColumnNotSerializableError.new(attr_name, cast_type)
134134
end
135135

136+
cast_type = cast_type.subtype if Type::Serialized === cast_type
136137
Type::Serialized.new(cast_type, coder)
137138
end
138139
end

activerecord/lib/active_record/attributes.rb

+21-19
Original file line numberDiff line numberDiff line change
@@ -208,21 +208,30 @@ module ClassMethods
208208
# tracking is performed. The methods +changed?+ and +changed_in_place?+
209209
# will be called from ActiveModel::Dirty. See the documentation for those
210210
# methods in ActiveModel::Type::Value for more details.
211-
def attribute(name, cast_type = nil, **options, &decorator)
211+
def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options, &block)
212212
name = name.to_s
213213
reload_schema_from_cache
214214

215-
prev_cast_type, prev_options, prev_decorator = attributes_to_define_after_schema_loads[name]
215+
case cast_type
216+
when Symbol
217+
type = cast_type
218+
cast_type = -> _ { Type.lookup(type, **options, adapter: Type.adapter_name_from(self)) }
219+
when nil
220+
if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name])
221+
default = prev_default if default == NO_DEFAULT_PROVIDED
216222

217-
unless cast_type && prev_cast_type
218-
cast_type ||= prev_cast_type
219-
options = prev_options || options if options.empty?
220-
decorator ||= prev_decorator
223+
cast_type = if block_given?
224+
-> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type }
225+
else
226+
prev_cast_type
227+
end
228+
else
229+
cast_type = block || -> subtype { subtype }
230+
end
221231
end
222232

223-
self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge(
224-
name => [cast_type, options, decorator]
225-
)
233+
self.attributes_to_define_after_schema_loads =
234+
attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
226235
end
227236

228237
# This is the low level API which sits beneath +attribute+. It only
@@ -255,16 +264,9 @@ def define_attribute(
255264

256265
def load_schema! # :nodoc:
257266
super
258-
attributes_to_define_after_schema_loads.each do |name, (type, options, decorator)|
259-
if type.is_a?(Symbol)
260-
type = ActiveRecord::Type.lookup(type, **options.except(:default), adapter: ActiveRecord::Type.adapter_name_from(self))
261-
elsif type.nil?
262-
type = type_for_attribute(name)
263-
end
264-
265-
type = decorator[type] if decorator
266-
267-
define_attribute(name, type, **options.slice(:default))
267+
attributes_to_define_after_schema_loads.each do |name, (cast_type, default)|
268+
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
269+
define_attribute(name, cast_type, default: default)
268270
end
269271
end
270272

activerecord/lib/active_record/enum.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,10 @@ def assert_valid_value(value)
153153
end
154154
end
155155

156+
attr_reader :subtype
157+
156158
private
157-
attr_reader :name, :mapping, :subtype
159+
attr_reader :name, :mapping
158160
end
159161

160162
def enum(definitions)
@@ -182,6 +184,7 @@ def enum(definitions)
182184
attr = attribute_alias?(name) ? attribute_alias(name) : name
183185

184186
attribute(attr, **default) do |subtype|
187+
subtype = subtype.subtype if EnumType === subtype
185188
EnumType.new(attr, enum_values, subtype)
186189
end
187190

activerecord/test/cases/serialized_attribute_test.rb

+62
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,68 @@ def test_nil_is_always_persisted_as_null
392392
assert_equal [topic], Topic.where(content: nil)
393393
end
394394

395+
class EncryptedType < ActiveRecord::Type::Text
396+
include ActiveModel::Type::Helpers::Mutable
397+
398+
attr_reader :subtype, :encryptor
399+
400+
def initialize(subtype: ActiveModel::Type::String.new)
401+
super()
402+
403+
@subtype = subtype
404+
@encryptor = ActiveSupport::MessageEncryptor.new("abcd" * 8)
405+
end
406+
407+
def serialize(value)
408+
subtype.serialize(value).yield_self do |cleartext|
409+
encryptor.encrypt_and_sign(cleartext) unless cleartext.nil?
410+
end
411+
end
412+
413+
def deserialize(ciphertext)
414+
encryptor.decrypt_and_verify(ciphertext)
415+
.yield_self { |cleartext| subtype.deserialize(cleartext) } unless ciphertext.nil?
416+
end
417+
418+
def changed_in_place?(old, new)
419+
if old.nil?
420+
!new.nil?
421+
else
422+
deserialize(old) != new
423+
end
424+
end
425+
end
426+
427+
def test_decorated_type_with_type_for_attribute
428+
old_registry = ActiveRecord::Type.registry
429+
ActiveRecord::Type.registry = ActiveRecord::Type.registry.dup
430+
ActiveRecord::Type.register :encrypted, EncryptedType
431+
432+
klass = Class.new(ActiveRecord::Base) do
433+
self.table_name = Topic.table_name
434+
store :content
435+
attribute :content, :encrypted, subtype: type_for_attribute(:content)
436+
end
437+
438+
topic = klass.create!(content: { trial: true })
439+
440+
assert_equal({ "trial" => true }, topic.content)
441+
ensure
442+
ActiveRecord::Type.registry = old_registry
443+
end
444+
445+
def test_decorated_type_with_decorator_block
446+
klass = Class.new(ActiveRecord::Base) do
447+
self.table_name = Topic.table_name
448+
store :content
449+
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) }
450+
end
451+
452+
topic = klass.create!(content: { trial: true })
453+
454+
assert_equal({ "trial" => true }, topic.content)
455+
end
456+
395457
def test_mutation_detection_does_not_double_serialize
396458
coder = Object.new
397459
def coder.dump(value)

0 commit comments

Comments
 (0)