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:
parent
5fc0c4c562
commit
70259f5e51
5 changed files with 60 additions and 35 deletions
|
@ -128,11 +128,12 @@ module ActiveRecord
|
|||
Coders::YAMLColumn.new(attr_name, class_name_or_coder)
|
||||
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)
|
||||
raise ColumnNotSerializableError.new(attr_name, cast_type)
|
||||
end
|
||||
|
||||
cast_type = cast_type.subtype if Type::Serialized === cast_type
|
||||
Type::Serialized.new(cast_type, coder)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -208,14 +208,30 @@ module ActiveRecord
|
|||
# tracking is performed. The methods +changed?+ and +changed_in_place?+
|
||||
# will be called from ActiveModel::Dirty. See the documentation for those
|
||||
# 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
|
||||
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 =
|
||||
attributes_to_define_after_schema_loads.merge(
|
||||
name => [cast_type || block, options]
|
||||
)
|
||||
attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
|
||||
end
|
||||
|
||||
# This is the low level API which sits beneath +attribute+. It only
|
||||
|
@ -248,8 +264,9 @@ module ActiveRecord
|
|||
|
||||
def load_schema! # :nodoc:
|
||||
super
|
||||
attributes_to_define_after_schema_loads.each do |name, (type, options)|
|
||||
define_attribute(name, _lookup_cast_type(name, type, options), **options.slice(:default))
|
||||
attributes_to_define_after_schema_loads.each do |name, (cast_type, default)|
|
||||
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
|
||||
define_attribute(name, cast_type, default: default)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -272,32 +289,6 @@ module ActiveRecord
|
|||
end
|
||||
_default_attributes[name] = default_attribute
|
||||
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
|
||||
|
|
|
@ -153,8 +153,10 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
attr_reader :subtype
|
||||
|
||||
private
|
||||
attr_reader :name, :mapping, :subtype
|
||||
attr_reader :name, :mapping
|
||||
end
|
||||
|
||||
def enum(definitions)
|
||||
|
@ -181,7 +183,8 @@ module ActiveRecord
|
|||
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -68,6 +68,15 @@ module ActiveRecord
|
|||
assert_equal "the overloaded default", klass.new.overloaded_string_with_limit
|
||||
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
|
||||
klass = Class.new(OverloadedType) do
|
||||
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
|
||||
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
|
||||
child = Class.new(ActiveRecord::Base) do
|
||||
self.table_name = "topics"
|
||||
|
|
|
@ -442,6 +442,18 @@ class SerializedAttributeTest < ActiveRecord::TestCase
|
|||
ActiveRecord::Type.registry = old_registry
|
||||
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
|
||||
coder = Object.new
|
||||
def coder.dump(value)
|
||||
|
|
Loading…
Reference in a new issue