aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
authorSean Griffin <sean@thoughtbot.com>2014-06-16 14:55:01 -0600
committerSean Griffin <sean@thoughtbot.com>2014-06-16 15:09:39 -0600
commit74af9f7fd781501e7f4a4746afd91b1bd5f77ddc (patch)
tree47c95a656a5b0aa0a8a6c72c1282832e13122004 /activerecord/lib/active_record
parent88714deb677f598fa40f6e7b61a083a5461d07fd (diff)
downloadrails-74af9f7fd781501e7f4a4746afd91b1bd5f77ddc.tar.gz
rails-74af9f7fd781501e7f4a4746afd91b1bd5f77ddc.tar.bz2
rails-74af9f7fd781501e7f4a4746afd91b1bd5f77ddc.zip
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.
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb36
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb7
-rw-r--r--activerecord/lib/active_record/base.rb4
-rw-r--r--activerecord/lib/active_record/model_schema.rb20
4 files changed, 35 insertions, 32 deletions
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 928d8cacae..92627f8d5d 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -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
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index abad949ef4..188d5d2aab 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -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
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e4d0abb8ef..0b788ea1f9 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -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)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 9e1afd32e6..b9558b0752 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -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