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:
parent
761fea3822
commit
4cc9c0f504
4 changed files with 42 additions and 35 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue