Change multiparamter extension to use AR type classes

Multiparamter extension used to do a lot of crappy work to ensure that
the invalid Date dates were not accepted, but also to store a
before_type_cast value in cases when it was invalid. We no longer need
to handle the before_type_cast value as ActiveRecord does that nicely.

Now we just ensure values to cast with a invalid date parts is not
accepted.
This commit is contained in:
Adam Meehan 2018-05-18 08:58:00 +10:00
parent a1f42fce28
commit 402a6b6e3e
4 changed files with 57 additions and 75 deletions

View File

@ -1,6 +1,6 @@
= [UNRELEASED]
* Fix removed namespace ActionRecord::AttributeAssignment::MultiparameterAttribute (AquisTech)
* Fix DateTimeSelect extension support (AquisTech)
* Update Multiparameter extension to use ActiveRecord type classes with multiparameter handling
= 4.0.2 [2016-01-07]
* Fix undefine_generated_methods ivar guard setting to false

View File

@ -1,74 +1,55 @@
ActiveRecord::AttributeAssignment.class_eval do
private
module ValidatesTimeliness
module Extensions
class AcceptsMultiparameterTime < Module
# Yield if date values are valid
def validate_multiparameter_date_values(set_values)
if set_values[0..2].all?{ |v| v.present? } && Date.valid_civil?(*set_values[0..2])
yield
else
invalid_multiparameter_date_or_time_as_string(set_values)
end
end
def initialize(defaults: {})
def invalid_multiparameter_date_or_time_as_string(values)
value = [values[0], *values[1..2].map {|s| s.to_s.rjust(2,"0")} ].join("-")
value += ' ' + values[3..5].map {|s| s.to_s.rjust(2, "0") }.join(":") unless values[3..5].empty?
value
end
define_method(:cast) do |value|
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
def instantiate_time_object(set_values)
raise if set_values.any?(&:nil?)
define_method(:assert_valid_value) do |value|
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
validate_multiparameter_date_values(set_values) {
set_values = set_values.map {|v| v.is_a?(String) ? v.strip : v }
define_method(:value_from_multiparameter_assignment) do |values_hash|
defaults.each do |k, v|
values_hash[k] ||= v
end
return unless values_hash.values_at(1,2,3).all?{ |v| v.present? } &&
Date.valid_civil?(*values_hash.values_at(1,2,3))
values = values_hash.sort.map(&:last)
::Time.send(default_timezone, *values)
end
private :value_from_multiparameter_assignment
if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type_or_column)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
end
}
rescue
invalid_multiparameter_date_or_time_as_string(set_values)
end
def read_time
# If column is a :time (and not :date or :timestamp) there is no need to validate if
# there are year/month/day fields
if cast_type_or_column.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
end
end
max_position = extract_max_param(6)
set_values = values.values_at(*(1..max_position))
instantiate_time_object(set_values)
end
def read_date
set_values = values.values_at(1,2,3).map {|v| v.is_a?(String) ? v.strip : v }
if set_values.any? { |v| v.is_a?(String) }
Timeliness.parse(set_values.join('-'), :date).try(:to_date) or raise TypeError
else
Date.new(*set_values)
end
rescue TypeError, ArgumentError, NoMethodError => ex # if Date.new raises an exception on an invalid date
# Date.new with nil values throws NoMethodError
raise ex if ex.is_a?(NoMethodError) && ex.message !~ /undefined method `div' for/
invalid_multiparameter_date_or_time_as_string(set_values)
end
# Cast type is v4.2 and column before
def cast_type_or_column
@cast_type || @column
end
def timezone_conversion_attribute?
object.class.send(:create_time_zone_conversion_attribute?, name, column)
end
end
ActiveModel::Type::Date.class_eval do
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new
end
ActiveModel::Type::Time.class_eval do
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new(
defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
)
end
ActiveModel::Type::DateTime.class_eval do
include ValidatesTimeliness::Extensions::AcceptsMultiparameterTime.new(
defaults: { 4 => 0, 5 => 0 }
)
end

View File

@ -57,6 +57,7 @@ end
ActiveRecord::Base.default_timezone = :utc
ActiveRecord::Base.time_zone_aware_attributes = true
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
ActiveRecord::Base.time_zone_aware_types = [:datetime, :time]
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(:version => 1) do
create_table :employees, :force => true do |t|

View File

@ -1,36 +1,36 @@
RSpec.describe 'ValidatesTimeliness::Extensions::MultiparameterHandler' do
context "time column" do
it 'should assign a string value for invalid date portion' do
it 'should be nil invalid date portion' do
employee = record_with_multiparameter_attribute(:birth_datetime, [2000, 2, 31, 12, 0, 0])
expect(employee.birth_datetime_before_type_cast).to eq '2000-02-31 12:00:00'
expect(employee.birth_datetime).to be_nil
end
it 'should assign a Time value for valid datetimes' do
employee = record_with_multiparameter_attribute(:birth_datetime, [2000, 2, 28, 12, 0, 0])
expect(employee.birth_datetime_before_type_cast).to eq Time.zone.local(2000, 2, 28, 12, 0, 0)
expect(employee.birth_datetime).to eq Time.zone.local(2000, 2, 28, 12, 0, 0)
end
it 'should assign a string value for incomplete time' do
it 'should be nil for incomplete date portion' do
employee = record_with_multiparameter_attribute(:birth_datetime, [2000, nil, nil])
expect(employee.birth_datetime_before_type_cast).to eq '2000-00-00'
expect(employee.birth_datetime).to be_nil
end
end
context "date column" do
it 'should assign a string value for invalid date' do
it 'should assign nil for invalid date' do
employee = record_with_multiparameter_attribute(:birth_date, [2000, 2, 31])
expect(employee.birth_date_before_type_cast).to eq '2000-02-31'
expect(employee.birth_date).to be_nil
end
it 'should assign a Date value for valid date' do
employee = record_with_multiparameter_attribute(:birth_date, [2000, 2, 28])
expect(employee.birth_date_before_type_cast).to eq Date.new(2000, 2, 28)
expect(employee.birth_date).to eq Date.new(2000, 2, 28)
end
it 'should assign a string value for incomplete date' do
it 'should assign hash values for incomplete date' do
employee = record_with_multiparameter_attribute(:birth_date, [2000, nil, nil])
expect(employee.birth_date_before_type_cast).to eq '2000-00-00'
expect(employee.birth_date).to be_nil
end
end