1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Allow multiple decorations which can be used by such like EncryptedType

This commit is contained in:
Ryuta Kamizono 2021-01-17 09:12:25 +09:00
parent 5fc0c4c562
commit 70259f5e51
5 changed files with 60 additions and 35 deletions

View file

@ -128,11 +128,12 @@ module ActiveRecord
Coders::YAMLColumn.new(attr_name, class_name_or_coder) Coders::YAMLColumn.new(attr_name, class_name_or_coder)
end end
decorate_attribute_type(attr_name.to_s, **options) do |cast_type| attribute(attr_name, **options) do |cast_type|
if type_incompatible_with_serialize?(cast_type, class_name_or_coder) if type_incompatible_with_serialize?(cast_type, class_name_or_coder)
raise ColumnNotSerializableError.new(attr_name, cast_type) raise ColumnNotSerializableError.new(attr_name, cast_type)
end end
cast_type = cast_type.subtype if Type::Serialized === cast_type
Type::Serialized.new(cast_type, coder) Type::Serialized.new(cast_type, coder)
end end
end end

View file

@ -208,14 +208,30 @@ module ActiveRecord
# tracking is performed. The methods +changed?+ and +changed_in_place?+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
# will be called from ActiveModel::Dirty. See the documentation for those # will be called from ActiveModel::Dirty. See the documentation for those
# methods in ActiveModel::Type::Value for more details. # methods in ActiveModel::Type::Value for more details.
def attribute(name, cast_type = nil, **options, &block) def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options, &block)
name = name.to_s name = name.to_s
reload_schema_from_cache reload_schema_from_cache
case cast_type
when Symbol
type = cast_type
cast_type = -> _ { Type.lookup(type, **options, adapter: Type.adapter_name_from(self)) }
when nil
if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name])
default = prev_default if default == NO_DEFAULT_PROVIDED
cast_type = if block_given?
-> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type }
else
prev_cast_type
end
else
cast_type = block || -> subtype { subtype }
end
end
self.attributes_to_define_after_schema_loads = self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge( attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
name => [cast_type || block, options]
)
end end
# This is the low level API which sits beneath +attribute+. It only # This is the low level API which sits beneath +attribute+. It only
@ -248,8 +264,9 @@ module ActiveRecord
def load_schema! # :nodoc: def load_schema! # :nodoc:
super super
attributes_to_define_after_schema_loads.each do |name, (type, options)| attributes_to_define_after_schema_loads.each do |name, (cast_type, default)|
define_attribute(name, _lookup_cast_type(name, type, options), **options.slice(:default)) cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
define_attribute(name, cast_type, default: default)
end end
end end
@ -272,32 +289,6 @@ module ActiveRecord
end end
_default_attributes[name] = default_attribute _default_attributes[name] = default_attribute
end end
def decorate_attribute_type(attr_name, **default)
type, options = attributes_to_define_after_schema_loads[attr_name]
default.with_defaults!(default: options[:default]) if options&.key?(:default)
attribute(attr_name, **default) do |cast_type|
if type && !type.is_a?(Proc)
cast_type = _lookup_cast_type(attr_name, type, options)
end
yield cast_type
end
end
def _lookup_cast_type(name, type, options)
case type
when Symbol
adapter_name = ActiveRecord::Type.adapter_name_from(self)
ActiveRecord::Type.lookup(type, **options.except(:default), adapter: adapter_name)
when Proc
type[type_for_attribute(name)]
else
type || type_for_attribute(name)
end
end
end end
end end
end end

View file

@ -153,8 +153,10 @@ module ActiveRecord
end end
end end
attr_reader :subtype
private private
attr_reader :name, :mapping, :subtype attr_reader :name, :mapping
end end
def enum(definitions) def enum(definitions)
@ -181,7 +183,8 @@ module ActiveRecord
attr = attribute_alias?(name) ? attribute_alias(name) : name attr = attribute_alias?(name) ? attribute_alias(name) : name
decorate_attribute_type(attr, **default) do |subtype| attribute(attr, **default) do |subtype|
subtype = subtype.subtype if EnumType === subtype
EnumType.new(attr, enum_values, subtype) EnumType.new(attr, enum_values, subtype)
end end

View file

@ -68,6 +68,15 @@ module ActiveRecord
assert_equal "the overloaded default", klass.new.overloaded_string_with_limit assert_equal "the overloaded default", klass.new.overloaded_string_with_limit
end end
test "attributes with overridden types keep their type when a default value is configured separately" do
child = Class.new(OverloadedType) do
attribute :overloaded_float, default: "123"
end
assert_equal OverloadedType.type_for_attribute("overloaded_float"), child.type_for_attribute("overloaded_float")
assert_equal 123, child.new.overloaded_float
end
test "extra options are forwarded to the type caster constructor" do test "extra options are forwarded to the type caster constructor" do
klass = Class.new(OverloadedType) do klass = Class.new(OverloadedType) do
attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1, default: -> { Time.now.utc } attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1, default: -> { Time.now.utc }
@ -295,6 +304,15 @@ module ActiveRecord
assert_equal 123, model.non_existent_decimal assert_equal 123, model.non_existent_decimal
end end
test "attributes not backed by database columns keep their type when a default value is configured separately" do
child = Class.new(OverloadedType) do
attribute :non_existent_decimal, default: "123"
end
assert_equal OverloadedType.type_for_attribute("non_existent_decimal"), child.type_for_attribute("non_existent_decimal")
assert_equal 123, child.new.non_existent_decimal
end
test "attributes not backed by database columns properly interact with mutation and dirty" do test "attributes not backed by database columns properly interact with mutation and dirty" do
child = Class.new(ActiveRecord::Base) do child = Class.new(ActiveRecord::Base) do
self.table_name = "topics" self.table_name = "topics"

View file

@ -442,6 +442,18 @@ class SerializedAttributeTest < ActiveRecord::TestCase
ActiveRecord::Type.registry = old_registry ActiveRecord::Type.registry = old_registry
end end
def test_decorated_type_with_decorator_block
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
store :content
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) }
end
topic = klass.create!(content: { trial: true })
assert_equal({ "trial" => true }, topic.content)
end
def test_mutation_detection_does_not_double_serialize def test_mutation_detection_does_not_double_serialize
coder = Object.new coder = Object.new
def coder.dump(value) def coder.dump(value)