Move conversion module methods in Converter class

Encapsulate conversion helper methods
This commit is contained in:
Adam Meehan 2018-05-19 16:28:35 +10:00
parent e5bb096161
commit 093e33fbed
4 changed files with 106 additions and 70 deletions

View File

@ -62,7 +62,7 @@ module ValidatesTimeliness
def self.parser; Timeliness end def self.parser; Timeliness end
end end
require 'validates_timeliness/conversion' require 'validates_timeliness/converter'
require 'validates_timeliness/validator' require 'validates_timeliness/validator'
require 'validates_timeliness/helper_methods' require 'validates_timeliness/helper_methods'
require 'validates_timeliness/attribute_methods' require 'validates_timeliness/attribute_methods'

View File

@ -1,10 +1,18 @@
module ValidatesTimeliness module ValidatesTimeliness
module Conversion class Converter
attr_reader :type, :format, :ignore_usec
def type_cast_value(value, type) def initialize(type:, format: nil, ignore_usec: false, time_zone_aware: false)
@type = type
@format = format
@ignore_usec = ignore_usec
@time_zone_aware = time_zone_aware
end
def type_cast_value(value)
return nil if value.nil? || !value.respond_to?(:to_time) return nil if value.nil? || !value.respond_to?(:to_time)
value = value.in_time_zone if value.acts_like?(:time) && @timezone_aware value = value.in_time_zone if value.acts_like?(:time) && time_zone_aware?
value = case type value = case type
when :time when :time
dummy_time(value) dummy_time(value)
@ -15,8 +23,8 @@ module ValidatesTimeliness
else else
value value
end end
if options[:ignore_usec] && value.is_a?(Time) if ignore_usec && value.is_a?(Time)
Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if @timezone_aware)) Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if time_zone_aware?))
else else
value value
end end
@ -24,30 +32,30 @@ module ValidatesTimeliness
def dummy_time(value) def dummy_time(value)
time = if value.acts_like?(:time) time = if value.acts_like?(:time)
value = value.in_time_zone if @timezone_aware value = value.in_time_zone if time_zone_aware?
[value.hour, value.min, value.sec] [value.hour, value.min, value.sec]
else else
[0,0,0] [0,0,0]
end end
values = ValidatesTimeliness.dummy_date_for_time_type + time values = ValidatesTimeliness.dummy_date_for_time_type + time
Timeliness::Parser.make_time(values, (:current if @timezone_aware)) Timeliness::Parser.make_time(values, (:current if time_zone_aware?))
end end
def evaluate_option_value(value, record) def evaluate(value, scope=nil)
case value case value
when Time, Date when Time, Date
value value
when String when String
parse(value) parse(value)
when Symbol when Symbol
if !record.respond_to?(value) && restriction_shorthand?(value) if !scope.respond_to?(value) && restriction_shorthand?(value)
ValidatesTimeliness.restriction_shorthand_symbols[value].call ValidatesTimeliness.restriction_shorthand_symbols[value].call
else else
evaluate_option_value(record.send(value), record) evaluate(scope.send(value))
end end
when Proc when Proc
result = value.arity > 0 ? value.call(record) : value.call result = value.arity > 0 ? value.call(scope) : value.call
evaluate_option_value(result, record) evaluate(result, scope)
else else
value value
end end
@ -59,14 +67,18 @@ module ValidatesTimeliness
def parse(value) def parse(value)
return nil if value.nil? return nil if value.nil?
if ValidatesTimeliness.use_plugin_parser if ValidatesTimeliness.use_plugin_parser
Timeliness::Parser.parse(value, @type, :zone => (:current if @timezone_aware), :format => options[:format], :strict => false) Timeliness::Parser.parse(value, type, zone: (:current if time_zone_aware?), format: format, strict: false)
else else
@timezone_aware ? Time.zone.parse(value) : value.to_time(ValidatesTimeliness.default_timezone) time_zone_aware? ? Time.zone.parse(value) : value.to_time(ValidatesTimeliness.default_timezone)
end end
rescue ArgumentError, TypeError rescue ArgumentError, TypeError
nil nil
end end
def time_zone_aware?
@time_zone_aware
end
end end
end end

View File

@ -3,9 +3,7 @@ require 'active_model/validator'
module ValidatesTimeliness module ValidatesTimeliness
class Validator < ActiveModel::EachValidator class Validator < ActiveModel::EachValidator
include Conversion attr_reader :type, :attributes, :converter
attr_reader :type, :attributes
RESTRICTIONS = { RESTRICTIONS = {
:is_at => :==, :is_at => :==,
@ -59,9 +57,10 @@ module ValidatesTimeliness
raw_value = attribute_raw_value(record, attr_name) || value raw_value = attribute_raw_value(record, attr_name) || value
return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?) return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?)
@timezone_aware = timezone_aware?(record, attr_name) @converter = initialize_converter(record, attr_name)
value = parse(raw_value) if value.is_a?(String) || options[:format]
value = type_cast_value(value, @type) value = @converter.parse(raw_value) if value.is_a?(String) || options[:format]
value = @converter.type_cast_value(value)
add_error(record, attr_name, :"invalid_#{@type}") and return if value.blank? add_error(record, attr_name, :"invalid_#{@type}") and return if value.blank?
@ -71,7 +70,7 @@ module ValidatesTimeliness
def validate_restrictions(record, attr_name, value) def validate_restrictions(record, attr_name, value)
@restrictions_to_check.each do |restriction| @restrictions_to_check.each do |restriction|
begin begin
restriction_value = type_cast_value(evaluate_option_value(options[restriction], record), @type) restriction_value = @converter.type_cast_value(@converter.evaluate(options[restriction], record))
unless value.send(RESTRICTIONS[restriction], restriction_value) unless value.send(RESTRICTIONS[restriction], restriction_value)
add_error(record, attr_name, restriction, restriction_value) and break add_error(record, attr_name, restriction, restriction_value) and break
end end
@ -100,10 +99,20 @@ module ValidatesTimeliness
record.read_timeliness_attribute_before_type_cast(attr_name.to_s) record.read_timeliness_attribute_before_type_cast(attr_name.to_s)
end end
def timezone_aware?(record, attr_name) def time_zone_aware?(record, attr_name)
record.class.respond_to?(:skip_time_zone_conversion_for_attributes) && record.class.respond_to?(:skip_time_zone_conversion_for_attributes) &&
!record.class.skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) !record.class.skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym)
end end
def initialize_converter(record, attr_name)
ValidatesTimeliness::Converter.new(
type: @type,
time_zone_aware: time_zone_aware?(record, attr_name),
format: options[:format],
ignore_usec: options[:ignore_usec]
)
end
end end
end end

View File

@ -1,97 +1,112 @@
RSpec.describe ValidatesTimeliness::Conversion do RSpec.describe ValidatesTimeliness::Converter do
include ValidatesTimeliness::Conversion subject(:converter) { described_class.new(type: type, time_zone_aware: time_zone_aware, ignore_usec: ignore_usec) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:type) { :date }
let(:time_zone_aware) { false }
let(:ignore_usec) { false }
before do before do
Timecop.freeze(Time.mktime(2010, 1, 1, 0, 0, 0)) Timecop.freeze(Time.mktime(2010, 1, 1, 0, 0, 0))
end end
delegate :type_cast_value, :evaluate, :parse, :dummy_time, to: :converter
describe "#type_cast_value" do describe "#type_cast_value" do
describe "for date type" do describe "for date type" do
let(:type) { :date }
it "should return same value for date value" do it "should return same value for date value" do
expect(type_cast_value(Date.new(2010, 1, 1), :date)).to eq(Date.new(2010, 1, 1)) expect(type_cast_value(Date.new(2010, 1, 1))).to eq(Date.new(2010, 1, 1))
end end
it "should return date part of time value" do it "should return date part of time value" do
expect(type_cast_value(Time.mktime(2010, 1, 1, 0, 0, 0), :date)).to eq(Date.new(2010, 1, 1)) expect(type_cast_value(Time.mktime(2010, 1, 1, 0, 0, 0))).to eq(Date.new(2010, 1, 1))
end end
it "should return date part of datetime value" do it "should return date part of datetime value" do
expect(type_cast_value(DateTime.new(2010, 1, 1, 0, 0, 0), :date)).to eq(Date.new(2010, 1, 1)) expect(type_cast_value(DateTime.new(2010, 1, 1, 0, 0, 0))).to eq(Date.new(2010, 1, 1))
end end
it 'should return nil for invalid value types' do it 'should return nil for invalid value types' do
expect(type_cast_value(12, :date)).to eq(nil) expect(type_cast_value(12)).to eq(nil)
end end
end end
describe "for time type" do describe "for time type" do
let(:type) { :time }
it "should return same value for time value matching dummy date part" do it "should return same value for time value matching dummy date part" do
expect(type_cast_value(Time.utc(2000, 1, 1, 0, 0, 0), :time)).to eq(Time.utc(2000, 1, 1, 0, 0, 0)) expect(type_cast_value(Time.utc(2000, 1, 1, 0, 0, 0))).to eq(Time.utc(2000, 1, 1, 0, 0, 0))
end end
it "should return dummy time value with same time part for time value with different date" do it "should return dummy time value with same time part for time value with different date" do
expect(type_cast_value(Time.utc(2010, 1, 1, 0, 0, 0), :time)).to eq(Time.utc(2000, 1, 1, 0, 0, 0)) expect(type_cast_value(Time.utc(2010, 1, 1, 0, 0, 0))).to eq(Time.utc(2000, 1, 1, 0, 0, 0))
end end
it "should return dummy time only for date value" do it "should return dummy time only for date value" do
expect(type_cast_value(Date.new(2010, 1, 1), :time)).to eq(Time.utc(2000, 1, 1, 0, 0, 0)) expect(type_cast_value(Date.new(2010, 1, 1))).to eq(Time.utc(2000, 1, 1, 0, 0, 0))
end end
it "should return dummy date with time part for datetime value" do it "should return dummy date with time part for datetime value" do
expect(type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56), :time)).to eq(Time.utc(2000, 1, 1, 12, 34, 56)) expect(type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56))).to eq(Time.utc(2000, 1, 1, 12, 34, 56))
end end
it 'should return nil for invalid value types' do it 'should return nil for invalid value types' do
expect(type_cast_value(12, :time)).to eq(nil) expect(type_cast_value(12)).to eq(nil)
end end
end end
describe "for datetime type" do describe "for datetime type" do
let(:type) { :datetime }
let(:time_zone_aware) { true }
it "should return Date as Time value" do it "should return Date as Time value" do
expect(type_cast_value(Date.new(2010, 1, 1), :datetime)).to eq(Time.local(2010, 1, 1, 0, 0, 0)) expect(type_cast_value(Date.new(2010, 1, 1))).to eq(Time.local(2010, 1, 1, 0, 0, 0))
end end
it "should return same Time value" do it "should return same Time value" do
value = Time.utc(2010, 1, 1, 12, 34, 56) value = Time.utc(2010, 1, 1, 12, 34, 56)
expect(type_cast_value(Time.utc(2010, 1, 1, 12, 34, 56), :datetime)).to eq(value) expect(type_cast_value(Time.utc(2010, 1, 1, 12, 34, 56))).to eq(value)
end end
it "should return as Time with same component values" do it "should return as Time with same component values" do
expect(type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56), :datetime)).to eq(Time.utc(2010, 1, 1, 12, 34, 56)) expect(type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56))).to eq(Time.utc(2010, 1, 1, 12, 34, 56))
end end
it "should return same Time in correct zone if timezone aware" do it "should return same Time in correct zone if timezone aware" do
@timezone_aware = true
value = Time.utc(2010, 1, 1, 12, 34, 56) value = Time.utc(2010, 1, 1, 12, 34, 56)
result = type_cast_value(value, :datetime) result = type_cast_value(value)
expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56)) expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56))
expect(result.zone).to eq('AEDT') expect(result.zone).to eq('AEDT')
end end
it 'should return nil for invalid value types' do it 'should return nil for invalid value types' do
expect(type_cast_value(12, :datetime)).to eq(nil) expect(type_cast_value(12)).to eq(nil)
end end
end end
describe "ignore_usec option" do describe "ignore_usec option" do
let(:options) { {:ignore_usec => true} } let(:type) { :datetime }
let(:ignore_usec) { true }
it "should ignore usec on time values when evaluated" do it "should ignore usec on time values when evaluated" do
value = Time.utc(2010, 1, 1, 12, 34, 56, 10000) value = Time.utc(2010, 1, 1, 12, 34, 56, 10000)
expect(type_cast_value(value, :datetime)).to eq(Time.utc(2010, 1, 1, 12, 34, 56)) expect(type_cast_value(value)).to eq(Time.utc(2010, 1, 1, 12, 34, 56))
end end
context do
let(:time_zone_aware) { true }
it "should ignore usec and return time in correct zone if timezone aware" do it "should ignore usec and return time in correct zone if timezone aware" do
@timezone_aware = true
value = Time.utc(2010, 1, 1, 12, 34, 56, 10000) value = Time.utc(2010, 1, 1, 12, 34, 56, 10000)
result = type_cast_value(value, :datetime) result = type_cast_value(value)
expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56)) expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56))
expect(result.zone).to eq('AEDT') expect(result.zone).to eq('AEDT')
end end
end end
end end
end
describe "#dummy_time" do describe "#dummy_time" do
it 'should return Time with dummy date values but same time components' do it 'should return Time with dummy date values but same time components' do
@ -103,7 +118,6 @@ RSpec.describe ValidatesTimeliness::Conversion do
end end
it 'should return time component values shifted to current zone if timezone aware' do it 'should return time component values shifted to current zone if timezone aware' do
@timezone_aware = true
expect(dummy_time(Time.utc(2000, 1, 1, 12, 34, 56))).to eq(Time.zone.local(2000, 1, 1, 23, 34, 56)) expect(dummy_time(Time.utc(2000, 1, 1, 12, 34, 56))).to eq(Time.zone.local(2000, 1, 1, 23, 34, 56))
end end
@ -120,61 +134,64 @@ RSpec.describe ValidatesTimeliness::Conversion do
end end
end end
describe "#evaluate_option_value" do describe "#evaluate" do
let(:person) { Person.new } let(:person) { Person.new }
it 'should return Date object as is' do it 'should return Date object as is' do
value = Date.new(2010,1,1) value = Date.new(2010,1,1)
expect(evaluate_option_value(value, person)).to eq(value) expect(evaluate(value, person)).to eq(value)
end end
it 'should return Time object as is' do it 'should return Time object as is' do
value = Time.mktime(2010,1,1) value = Time.mktime(2010,1,1)
expect(evaluate_option_value(value, person)).to eq(value) expect(evaluate(value, person)).to eq(value)
end end
it 'should return DateTime object as is' do it 'should return DateTime object as is' do
value = DateTime.new(2010,1,1,0,0,0) value = DateTime.new(2010,1,1,0,0,0)
expect(evaluate_option_value(value, person)).to eq(value) expect(evaluate(value, person)).to eq(value)
end end
it 'should return Time value returned from proc with 0 arity' do it 'should return Time value returned from proc with 0 arity' do
value = Time.mktime(2010,1,1) value = Time.mktime(2010,1,1)
expect(evaluate_option_value(lambda { value }, person)).to eq(value) expect(evaluate(lambda { value }, person)).to eq(value)
end end
it 'should return Time value returned by record attribute call in proc arity of 1' do it 'should return Time value returned by record attribute call in proc arity of 1' do
value = Time.mktime(2010,1,1) value = Time.mktime(2010,1,1)
person.birth_time = value person.birth_time = value
expect(evaluate_option_value(lambda {|r| r.birth_time }, person)).to eq(value) expect(evaluate(lambda {|r| r.birth_time }, person)).to eq(value)
end end
it 'should return Time value for attribute method symbol which returns Time' do it 'should return Time value for attribute method symbol which returns Time' do
value = Time.mktime(2010,1,1) value = Time.mktime(2010,1,1)
person.birth_datetime = value person.birth_datetime = value
expect(evaluate_option_value(:birth_datetime, person)).to eq(value) expect(evaluate(:birth_datetime, person)).to eq(value)
end end
it 'should return Time value is default zone from string time value' do it 'should return Time value is default zone from string time value' do
value = '2010-01-01 12:00:00' value = '2010-01-01 12:00:00'
expect(evaluate_option_value(value, person)).to eq(Time.utc(2010,1,1,12,0,0)) expect(evaluate(value, person)).to eq(Time.utc(2010,1,1,12,0,0))
end end
context do
let(:converter) { described_class.new(type: :date, time_zone_aware: true) }
it 'should return Time value is current zone from string time value if timezone aware' do it 'should return Time value is current zone from string time value if timezone aware' do
@timezone_aware = true
value = '2010-01-01 12:00:00' value = '2010-01-01 12:00:00'
expect(evaluate_option_value(value, person)).to eq(Time.zone.local(2010,1,1,12,0,0)) expect(evaluate(value, person)).to eq(Time.zone.local(2010,1,1,12,0,0))
end
end end
it 'should return Time value in default zone from proc which returns string time' do it 'should return Time value in default zone from proc which returns string time' do
value = '2010-11-12 13:00:00' value = '2010-11-12 13:00:00'
expect(evaluate_option_value(lambda { value }, person)).to eq(Time.utc(2010,11,12,13,0,0)) expect(evaluate(lambda { value }, person)).to eq(Time.utc(2010,11,12,13,0,0))
end end
skip 'should return Time value for attribute method symbol which returns string time value' do it 'should return Time value for attribute method symbol which returns string time value' do
value = '13:00:00' value = '13:00:00'
person.birth_time = value person.birth_time = value
expect(evaluate_option_value(:birth_time, person)).to eq(Time.utc(2000,1,1,13,0,0)) expect(evaluate(:birth_time, person)).to eq(Time.utc(2000,1,1,13,0,0))
end end
context "restriction shorthand" do context "restriction shorthand" do
@ -183,17 +200,17 @@ RSpec.describe ValidatesTimeliness::Conversion do
end end
it 'should evaluate :now as current time' do it 'should evaluate :now as current time' do
expect(evaluate_option_value(:now, person)).to eq(Time.now) expect(evaluate(:now, person)).to eq(Time.now)
end end
it 'should evaluate :today as current time' do it 'should evaluate :today as current time' do
expect(evaluate_option_value(:today, person)).to eq(Date.today) expect(evaluate(:today, person)).to eq(Date.today)
end end
it 'should not use shorthand if symbol if is record method' do it 'should not use shorthand if symbol if is record method' do
time = 1.day.from_now time = 1.day.from_now
allow(person).to receive(:now).and_return(time) allow(person).to receive(:now).and_return(time)
expect(evaluate_option_value(:now, person)).to eq(time) expect(evaluate(:now, person)).to eq(time)
end end
end end
end end
@ -212,13 +229,11 @@ RSpec.describe ValidatesTimeliness::Conversion do
with_config(:use_plugin_parser, false) with_config(:use_plugin_parser, false)
it 'should use Time.zone.parse attribute is timezone aware' do it 'should use Time.zone.parse attribute is timezone aware' do
@timezone_aware = true expect(Timeliness::Parser).to_not receive(:parse)
expect(Time.zone).to receive(:parse)
parse('2000-01-01') parse('2000-01-01')
end end
it 'should use value#to_time if use_plugin_parser setting is false and attribute is not timezone aware' do it 'should use value#to_time if use_plugin_parser setting is false and attribute is not timezone aware' do
@timezone_aware = false
value = '2000-01-01' value = '2000-01-01'
expect(value).to receive(:to_time) expect(value).to receive(:to_time)
parse(value) parse(value)