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

Remove internal attribute decoration code

The internal attribute decoration is almost covered by the `attribute`
with block, we don't need to have much code and extra class attribute.
This commit is contained in:
Ryuta Kamizono 2020-07-20 13:13:55 +09:00
parent c885086bd0
commit d784043d80
6 changed files with 24 additions and 233 deletions

View file

@ -1,88 +0,0 @@
# frozen_string_literal: true
module ActiveRecord
module AttributeDecorators # :nodoc:
extend ActiveSupport::Concern
included do
class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal:
end
module ClassMethods # :nodoc:
# This method is an internal API used to create class macros such as
# +serialize+, and features like time zone aware attributes.
#
# Used to wrap the type of an attribute in a new type.
# When the schema for a model is loaded, attributes with the same name as
# +column_name+ will have their type yielded to the given block. The
# return value of that block will be used instead.
#
# Subsequent calls where +column_name+ and +decorator_name+ are the same
# will override the previous decorator, not decorate twice. This can be
# used to create idempotent class macros like +serialize+
def decorate_attribute_type(column_name, decorator_name, &block)
matcher = ->(name, _) { name == column_name.to_s }
key = "_#{column_name}_#{decorator_name}"
decorate_matching_attribute_types(matcher, key, &block)
end
# This method is an internal API used to create higher level features like
# time zone aware attributes.
#
# When the schema for a model is loaded, +matcher+ will be called for each
# attribute with its name and type. If the matcher returns a truthy value,
# the type will then be yielded to the given block, and the return value
# of that block will replace the type.
#
# Subsequent calls to this method with the same value for +decorator_name+
# will replace the previous decorator, not decorate twice. This can be
# used to ensure that class macros are idempotent.
def decorate_matching_attribute_types(matcher, decorator_name, &block)
reload_schema_from_cache
decorator_name = decorator_name.to_s
# Create new hashes so we don't modify parent classes
self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
end
private
def load_schema!
super
attribute_types.each do |name, type|
decorated_type = attribute_type_decorations.apply(name, type)
define_attribute(name, decorated_type)
end
end
end
class TypeDecorator # :nodoc:
delegate :clear, to: :@decorations
def initialize(decorations = {})
@decorations = decorations
end
def merge(*args)
TypeDecorator.new(@decorations.merge(*args))
end
def apply(name, type)
decorations = decorators_for(name, type)
decorations.inject(type) do |new_type, block|
block.call(new_type)
end
end
private
def decorators_for(name, type)
matching(name, type).map(&:last)
end
def matching(name, type)
@decorations.values.select do |(matcher, _)|
matcher.call(name, type)
end
end
end
end
end

View file

@ -69,12 +69,19 @@ module ActiveRecord
Coders::YAMLColumn.new(attr_name, class_name_or_coder)
end
decorate_attribute_type(attr_name, :serialize) do |type|
if type_incompatible_with_serialize?(type, class_name_or_coder)
raise ColumnNotSerializableError.new(attr_name, type)
attr_name = attr_name.to_s
type, options = attributes_to_define_after_schema_loads[attr_name]
attribute(attr_name) do |cast_type|
if type && !type.is_a?(Proc)
cast_type = _lookup_cast_type(attr_name, type, options)
end
Type::Serialized.new(type, coder)
if type_incompatible_with_serialize?(cast_type, class_name_or_coder)
raise ColumnNotSerializableError.new(attr_name, cast_type)
end
Type::Serialized.new(cast_type, coder)
end
end

View file

@ -246,17 +246,7 @@ module ActiveRecord
def load_schema! # :nodoc:
super
attributes_to_define_after_schema_loads.each do |name, (type, options)|
case type
when Symbol
adapter_name = ActiveRecord::Type.adapter_name_from(self)
type = ActiveRecord::Type.lookup(type, **options.except(:default), adapter: adapter_name)
when Proc
type = type[type_for_attribute(name)]
else
type ||= type_for_attribute(name)
end
define_attribute(name, type, **options.slice(:default))
define_attribute(name, _lookup_cast_type(name, type, options), **options.slice(:default))
end
end
@ -279,6 +269,18 @@ module ActiveRecord
end
_default_attributes[name] = default_attribute
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

View file

@ -5,7 +5,6 @@ require "active_support/dependencies"
require "active_support/descendants_tracker"
require "active_support/time"
require "active_support/core_ext/class/subclasses"
require "active_record/attribute_decorators"
require "active_record/log_subscriber"
require "active_record/explain_subscriber"
require "active_record/relation/delegation"
@ -293,7 +292,6 @@ module ActiveRecord #:nodoc:
include Validations
include CounterCache
include Attributes
include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods

View file

@ -1,127 +0,0 @@
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class AttributeDecoratorsTest < ActiveRecord::TestCase
class Model < ActiveRecord::Base
self.table_name = "attribute_decorators_model"
end
class StringDecorator < SimpleDelegator
def initialize(delegate, decoration = "decorated!")
@decoration = decoration
super(delegate)
end
def cast(value)
"#{super} #{@decoration}"
end
alias deserialize cast
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table :attribute_decorators_model, force: true do |t|
t.string :a_string
end
end
teardown do
return unless @connection
@connection.drop_table "attribute_decorators_model", if_exists: true
Model.attribute_type_decorations.clear
Model.reset_column_information
end
test "attributes can be decorated" do
model = Model.new(a_string: "Hello")
assert_equal "Hello", model.a_string
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
model = Model.new(a_string: "Hello")
assert_equal "Hello decorated!", model.a_string
end
test "decoration does not eagerly load existing columns" do
Model.reset_column_information
assert_no_queries do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
end
end
test "undecorated columns are not touched" do
Model.attribute :another_string, :string, default: "something or other"
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
assert_equal "something or other", Model.new.another_string
end
test "decorators can be chained" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
model = Model.new(a_string: "Hello!")
assert_equal "Hello! decorated! decorated!", model.a_string
end
test "decoration of the same type multiple times is idempotent" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
model = Model.new(a_string: "Hello")
assert_equal "Hello decorated!", model.a_string
end
test "decorations occur in order of declaration" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) do |type|
StringDecorator.new(type, "decorated again!")
end
model = Model.new(a_string: "Hello!")
assert_equal "Hello! decorated! decorated again!", model.a_string
end
test "decorating attributes does not modify parent classes" do
Model.attribute :another_string, :string, default: "whatever"
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
child_class = Class.new(Model)
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
model = Model.new(a_string: "Hello!")
child = child_class.new(a_string: "Hello!")
assert_equal "Hello! decorated!", model.a_string
assert_equal "whatever", model.another_string
assert_equal "Hello! decorated! decorated!", child.a_string
assert_equal "whatever decorated!", child.another_string
end
class Multiplier < SimpleDelegator
def cast(value)
return if value.nil?
value * 2
end
alias deserialize cast
end
test "decorating with a proc" do
Model.attribute :an_int, :integer
type_is_integer = proc { |_, type| type.type == :integer }
Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
Multiplier.new(type)
end
model = Model.new(a_string: "whatever", an_int: 1)
assert_equal "whatever", model.a_string
assert_equal 2, model.an_int
end
end
end

View file

@ -14,7 +14,6 @@ module ActiveRecord
@klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do
def self.superclass; Base; end
def self.base_class?; true; end
def self.decorate_matching_attribute_types(*); end
include ActiveRecord::AttributeMethods