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

Promote time zone aware attributes to a first class type decorator

This refactoring revealed the need for another form of decoration, which
takes a proc to select which it applies to (There's a *lot* of cases
where this form can be used). To avoid duplication, we can re-implement
the old decoration in terms of the proc-based decoration.

The reason we're `instance_exec`ing the matcher is for cases such as
time zone aware attributes, where a decorator is defined in a parent
class, and a method called in the matcher is overridden by a child
class. The matcher will close over the parent, and evaluate in its
context, which is not the behavior we want.
This commit is contained in:
Sean Griffin 2014-06-16 14:55:01 -06:00
parent 88714deb67
commit 74af9f7fd7
6 changed files with 57 additions and 32 deletions

View file

@ -9,18 +9,24 @@ module ActiveRecord
module ClassMethods
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
def decorate_matching_attribute_types(matcher, decorator_name, &block)
clear_caches_calculated_from_columns
column_name = column_name.to_s
decorator_name = decorator_name.to_s
# Create new hashes so we don't modify parent classes
self.attribute_type_decorations = attribute_type_decorations.merge(column_name, decorator_name, block)
self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
end
private
def add_user_provided_columns(*)
super.map do |column|
decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
decorated_type = attribute_type_decorations.apply(self, column.name, column.cast_type)
column.with_type(decorated_type)
end
end
@ -29,22 +35,32 @@ module ActiveRecord
class TypeDecorator
delegate :clear, to: :@decorations
def initialize(decorations = Hash.new({}))
def initialize(decorations = {})
@decorations = decorations
end
def merge(attribute_name, decorator_name, block)
decorations_for_attribute = @decorations[attribute_name]
new_decorations = decorations_for_attribute.merge(decorator_name.to_s => block)
TypeDecorator.new(@decorations.merge(attribute_name => new_decorations))
def merge(*args)
TypeDecorator.new(@decorations.merge(*args))
end
def apply(attribute_name, type)
decorations = @decorations[attribute_name].values
def apply(context, name, type)
decorations = decorators_for(context, name, type)
decorations.inject(type) do |new_type, block|
block.call(new_type)
end
end
private
def decorators_for(context, name, type)
matching(context, name, type).map(&:last)
end
def matching(context, name, type)
@decorations.values.select do |(matcher, _)|
context.instance_exec(name, type, &matcher)
end
end
end
end
end

View file

@ -1,7 +1,7 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
class Type < SimpleDelegator # :nodoc:
class TimeZoneConverter < SimpleDelegator # :nodoc:
def type_cast_from_database(value)
convert_time_to_time_zone(super)
end
@ -33,6 +33,11 @@ module ActiveRecord
class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
self.skip_time_zone_conversion_for_attributes = []
matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
TimeZoneConverter.new(type)
end
end
module ClassMethods

View file

@ -310,6 +310,8 @@ module ActiveRecord #:nodoc:
include CounterCache
include Locking::Optimistic
include Locking::Pessimistic
include Attributes
include AttributeDecorators
include AttributeMethods
include Callbacks
include Timestamp
@ -323,8 +325,6 @@ module ActiveRecord #:nodoc:
include Reflection
include Serialization
include Store
include Attributes
include AttributeDecorators
end
ActiveSupport.run_load_hooks(:active_record, Base)

View file

@ -220,27 +220,13 @@ module ActiveRecord
end
def column_types # :nodoc:
@column_types ||= decorate_types(build_types_hash)
@column_types ||= Hash[columns.map { |column| [column.name, column.cast_type] }]
end
def type_for_attribute(attr_name) # :nodoc:
column_types.fetch(attr_name) { Type::Value.new }
end
def decorate_types(types) # :nodoc:
return if types.empty?
@time_zone_column_names ||= self.columns_hash.find_all do |name, col|
create_time_zone_conversion_attribute?(name, col)
end.map!(&:first)
@time_zone_column_names.each do |name|
types[name] = AttributeMethods::TimeZoneConversion::Type.new(types[name])
end
types
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
@ -335,10 +321,6 @@ module ActiveRecord
base.table_name
end
end
def build_types_hash
Hash[columns.map { |column| [column.name, column.cast_type] }]
end
end
end
end

View file

@ -110,5 +110,26 @@ module ActiveRecord
assert_equal 'whatever decorated!', column.default
end
class Multiplier < SimpleDelegator
def type_cast_from_user(value)
return if value.nil?
value * 2
end
alias type_cast_from_database type_cast_from_user
end
test "decorating with a proc" do
Model.attribute :an_int, Type::Integer.new
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

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