Skip to content

Commit 70259f5

Browse files
committed
Allow multiple decorations which can be used by such like EncryptedType
1 parent 5fc0c4c commit 70259f5

File tree

5 files changed

+60
-35
lines changed

5 files changed

+60
-35
lines changed

activerecord/lib/active_record/attribute_methods/serialization.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,12 @@ def serialize(attr_name, class_name_or_coder = Object, **options)
128128
Coders::YAMLColumn.new(attr_name, class_name_or_coder)
129129
end
130130

131-
decorate_attribute_type(attr_name.to_s, **options) do |cast_type|
131+
attribute(attr_name, **options) do |cast_type|
132132
if type_incompatible_with_serialize?(cast_type, class_name_or_coder)
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

+23-32
Original file line numberDiff line numberDiff line change
@@ -208,14 +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, &block)
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+
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
222+
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
231+
end
232+
215233
self.attributes_to_define_after_schema_loads =
216-
attributes_to_define_after_schema_loads.merge(
217-
name => [cast_type || block, options]
218-
)
234+
attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
219235
end
220236

221237
# This is the low level API which sits beneath +attribute+. It only
@@ -248,8 +264,9 @@ def define_attribute(
248264

249265
def load_schema! # :nodoc:
250266
super
251-
attributes_to_define_after_schema_loads.each do |name, (type, options)|
252-
define_attribute(name, _lookup_cast_type(name, type, options), **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)
253270
end
254271
end
255272

@@ -272,32 +289,6 @@ def define_default_attribute(name, value, type, from_user:)
272289
end
273290
_default_attributes[name] = default_attribute
274291
end
275-
276-
def decorate_attribute_type(attr_name, **default)
277-
type, options = attributes_to_define_after_schema_loads[attr_name]
278-
279-
default.with_defaults!(default: options[:default]) if options&.key?(:default)
280-
281-
attribute(attr_name, **default) do |cast_type|
282-
if type && !type.is_a?(Proc)
283-
cast_type = _lookup_cast_type(attr_name, type, options)
284-
end
285-
286-
yield cast_type
287-
end
288-
end
289-
290-
def _lookup_cast_type(name, type, options)
291-
case type
292-
when Symbol
293-
adapter_name = ActiveRecord::Type.adapter_name_from(self)
294-
ActiveRecord::Type.lookup(type, **options.except(:default), adapter: adapter_name)
295-
when Proc
296-
type[type_for_attribute(name)]
297-
else
298-
type || type_for_attribute(name)
299-
end
300-
end
301292
end
302293
end
303294
end

activerecord/lib/active_record/enum.rb

+5-2
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)
@@ -181,7 +183,8 @@ def enum(definitions)
181183

182184
attr = attribute_alias?(name) ? attribute_alias(name) : name
183185

184-
decorate_attribute_type(attr, **default) do |subtype|
186+
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/attributes_test.rb

+18
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ class CustomPropertiesTest < ActiveRecord::TestCase
6868
assert_equal "the overloaded default", klass.new.overloaded_string_with_limit
6969
end
7070

71+
test "attributes with overridden types keep their type when a default value is configured separately" do
72+
child = Class.new(OverloadedType) do
73+
attribute :overloaded_float, default: "123"
74+
end
75+
76+
assert_equal OverloadedType.type_for_attribute("overloaded_float"), child.type_for_attribute("overloaded_float")
77+
assert_equal 123, child.new.overloaded_float
78+
end
79+
7180
test "extra options are forwarded to the type caster constructor" do
7281
klass = Class.new(OverloadedType) do
7382
attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1, default: -> { Time.now.utc }
@@ -295,6 +304,15 @@ def deserialize(*)
295304
assert_equal 123, model.non_existent_decimal
296305
end
297306

307+
test "attributes not backed by database columns keep their type when a default value is configured separately" do
308+
child = Class.new(OverloadedType) do
309+
attribute :non_existent_decimal, default: "123"
310+
end
311+
312+
assert_equal OverloadedType.type_for_attribute("non_existent_decimal"), child.type_for_attribute("non_existent_decimal")
313+
assert_equal 123, child.new.non_existent_decimal
314+
end
315+
298316
test "attributes not backed by database columns properly interact with mutation and dirty" do
299317
child = Class.new(ActiveRecord::Base) do
300318
self.table_name = "topics"

activerecord/test/cases/serialized_attribute_test.rb

+12
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ def test_decorated_type_with_type_for_attribute
442442
ActiveRecord::Type.registry = old_registry
443443
end
444444

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+
445457
def test_mutation_detection_does_not_double_serialize
446458
coder = Object.new
447459
def coder.dump(value)

0 commit comments

Comments
 (0)