diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb index 2b4993d..13694d1 100644 --- a/lib/validates_timeliness/attribute_methods.rb +++ b/lib/validates_timeliness/attribute_methods.rb @@ -20,13 +20,6 @@ module ValidatesTimeliness :datetime end - def undefine_attribute_methods - super - undefine_timeliness_attribute_methods - end - - protected - def define_timeliness_methods(before_type_cast=false) return if timeliness_validated_attributes.blank? timeliness_validated_attributes.each do |attr_name| @@ -34,55 +27,62 @@ module ValidatesTimeliness end end + def generated_timeliness_methods + @generated_timeliness_methods ||= Module.new { |m| + extend Mutex_m + }.tap { |mod| include mod } + end + + def undefine_attribute_methods + super.tap { undefine_timeliness_attribute_methods } + end + + def undefine_timeliness_attribute_methods + generated_timeliness_methods.synchronize do + generated_timeliness_methods.module_eval do + instance_methods.each { |m| undef_method(m) } + end + end + end + + protected + def define_attribute_timeliness_methods(attr_name, before_type_cast=false) define_timeliness_write_method(attr_name) define_timeliness_before_type_cast_method(attr_name) if before_type_cast end def define_timeliness_write_method(attr_name) - method_body, line = <<-EOV, __LINE__ + 1 + generated_timeliness_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def #{attr_name}=(value) - original_value = value - @timeliness_cache ||= {} - @timeliness_cache["#{attr_name}"] = original_value - #{ "if value.is_a?(String)\n#{timeliness_type_cast_code(attr_name, 'value')}\nend" if ValidatesTimeliness.use_plugin_parser } - - super(value) + write_timeliness_attribute('#{attr_name}', value) end - EOV - generated_timeliness_methods.module_eval(method_body, __FILE__, line) + STR end def define_timeliness_before_type_cast_method(attr_name) - method_body, line = <<-EOV, __LINE__ + 1 + generated_timeliness_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def #{attr_name}_before_type_cast - _timeliness_raw_value_for('#{attr_name}') || @attributes['#{attr_name}'] + read_timeliness_attribute_before_type_cast('#{attr_name}') end - EOV - generated_timeliness_methods.module_eval(method_body, __FILE__, line) - end - - def timeliness_type_cast_code(attr_name, var_name) - type = timeliness_attribute_type(attr_name) - timezone_aware = timeliness_attribute_timezone_aware?(attr_name) - timezone = :current if timezone_aware - - "#{var_name} = Timeliness::Parser.parse(#{var_name}, :#{type}, :zone => #{timezone.inspect})" - end - - def generated_timeliness_methods - @generated_timeliness_methods ||= Module.new.tap { |m| include(m) } - end - - def undefine_timeliness_attribute_methods - generated_timeliness_methods.module_eval do - instance_methods.each { |m| undef_method(m) } - end + STR end end - def _timeliness_raw_value_for(attr_name) - @timeliness_cache && @timeliness_cache[attr_name] + def write_timeliness_attribute(attr_name, value) + @timeliness_cache ||= {} + @timeliness_cache[attr_name] = value + + if ValidatesTimeliness.use_plugin_parser + timezone = :current if self.class.timeliness_attribute_timezone_aware?(attr_name) + value = Timeliness::Parser.parse(value, self.class.timeliness_attribute_type(attr_name), :zone => timezone) + end + + @attributes[attr_name] = value + end + + def read_timeliness_attribute_before_type_cast(attr_name) + @timeliness_cache && @timeliness_cache[attr_name] || @attributes[attr_name] end def _clear_timeliness_cache diff --git a/lib/validates_timeliness/orm/active_record.rb b/lib/validates_timeliness/orm/active_record.rb index 1bc68c4..cd04872 100644 --- a/lib/validates_timeliness/orm/active_record.rb +++ b/lib/validates_timeliness/orm/active_record.rb @@ -14,30 +14,74 @@ module ValidatesTimeliness timeliness_column_for_attribute(attr_name).type end - def timeliness_column_for_attribute(attr_name) - columns_hash.fetch(attr_name.to_s) do |attr_name| - validation_type = _validators[attr_name.to_sym].find {|v| v.kind == :timeliness }.type - ::ActiveRecord::ConnectionAdapters::Column.new(attr_name, nil, validation_type.to_s) + if ActiveModel.version >= Gem::Version.new('4.2') + def timeliness_column_for_attribute(attr_name) + columns_hash.fetch(attr_name.to_s) do |attr_name| + validation_type = _validators[attr_name.to_sym].find {|v| v.kind == :timeliness }.type.to_s + ::ActiveRecord::ConnectionAdapters::Column.new(attr_name, nil, lookup_cast_type(validation_type), validation_type) + end + end + + def lookup_cast_type(sql_type) + case sql_type + when 'datetime' then ::ActiveRecord::Type::DateTime.new + when 'date' then ::ActiveRecord::Type::Date.new + when 'time' then ::ActiveRecord::Type::Time.new + end + end + else + def timeliness_column_for_attribute(attr_name) + columns_hash.fetch(attr_name.to_s) do |attr_name| + validation_type = _validators[attr_name.to_sym].find {|v| v.kind == :timeliness }.type.to_s + ::ActiveRecord::ConnectionAdapters::Column.new(attr_name, nil, validation_type) + end end end def define_attribute_methods - super.tap do |attribute_methods_generated| - return false if @timeliness_methods_generated - define_timeliness_methods true - @timeliness_methods_generated = true - end + super.tap { + generated_timeliness_methods.synchronize do + return if @timeliness_methods_generated + define_timeliness_methods true + @timeliness_methods_generated = true + end + } end - protected - - def timeliness_type_cast_code(attr_name, var_name) - type = timeliness_attribute_type(attr_name) - - method_body = super - method_body << "\n#{var_name} = #{var_name}.to_date if #{var_name}" if type == :date - method_body + def undefine_attribute_methods + super.tap { + generated_timeliness_methods.synchronize do + return unless @timeliness_methods_generated + undefine_timeliness_attribute_methods + @timeliness_methods_generated = true + end + } end + # Override to overwrite methods in ActiveRecord attribute method module because in AR 4+ + # there is curious code which calls the method directly from the generated methods module + # via bind inside method_missing. This means our method in the formerly custom timeliness + # methods module was never reached. + def generated_timeliness_methods + generated_attribute_methods + end + end + + def write_timeliness_attribute(attr_name, value) + @timeliness_cache ||= {} + @timeliness_cache[attr_name] = value + + if ValidatesTimeliness.use_plugin_parser + type = self.class.timeliness_attribute_type(attr_name) + timezone = :current if self.class.timeliness_attribute_timezone_aware?(attr_name) + value = Timeliness::Parser.parse(value, type, :zone => timezone) + value = value.to_date if value && type == :date + end + + write_attribute(attr_name, value) + end + + def read_timeliness_attribute_before_type_cast(attr_name) + @timeliness_cache && @timeliness_cache[attr_name] || read_attribute_before_type_cast(attr_name) end def reload(*args)