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

Unify decorate_attribute_type and attribute

Follow-up to 75c309c7ad.

As a result of these changes, attributes can have their type and default
value configured separately.  Similar behavior was implemented in #39380,
but only for attributes that derive (and do not override) their type
from the database.
This commit is contained in:
Jonathan Hefner 2020-07-26 12:58:45 -05:00
parent 761fea3822
commit 4cc9c0f504
4 changed files with 42 additions and 35 deletions

View file

@ -129,7 +129,7 @@ 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

View file

@ -208,14 +208,21 @@ 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, **options, &decorator)
name = name.to_s name = name.to_s
reload_schema_from_cache reload_schema_from_cache
self.attributes_to_define_after_schema_loads = prev_cast_type, prev_options, prev_decorator = attributes_to_define_after_schema_loads[name]
attributes_to_define_after_schema_loads.merge(
name => [cast_type || block, options] unless cast_type && prev_cast_type
) cast_type ||= prev_cast_type
options = prev_options || options if options.empty?
decorator ||= prev_decorator
end
self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge(
name => [cast_type, options, decorator]
)
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 +255,16 @@ 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, (type, options, decorator)|
define_attribute(name, _lookup_cast_type(name, type, options), **options.slice(:default)) if type.is_a?(Symbol)
type = ActiveRecord::Type.lookup(type, **options.except(:default), adapter: ActiveRecord::Type.adapter_name_from(self))
elsif type.nil?
type = type_for_attribute(name)
end
type = decorator[type] if decorator
define_attribute(name, type, **options.slice(:default))
end end
end end
@ -272,32 +287,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

@ -183,7 +183,7 @@ 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|
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"