From 093e33fbed59090d78525254588ee61cfb1836da Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 19 May 2018 16:28:35 +1000 Subject: [PATCH] Move conversion module methods in Converter class Encapsulate conversion helper methods --- lib/validates_timeliness.rb | 2 +- .../{conversion.rb => converter.rb} | 40 ++++--- lib/validates_timeliness/validator.rb | 25 ++-- .../{conversion_spec.rb => converter_spec.rb} | 109 ++++++++++-------- 4 files changed, 106 insertions(+), 70 deletions(-) rename lib/validates_timeliness/{conversion.rb => converter.rb} (52%) rename spec/validates_timeliness/{conversion_spec.rb => converter_spec.rb} (62%) diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index 79abf31..2a15907 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -62,7 +62,7 @@ module ValidatesTimeliness def self.parser; Timeliness end end -require 'validates_timeliness/conversion' +require 'validates_timeliness/converter' require 'validates_timeliness/validator' require 'validates_timeliness/helper_methods' require 'validates_timeliness/attribute_methods' diff --git a/lib/validates_timeliness/conversion.rb b/lib/validates_timeliness/converter.rb similarity index 52% rename from lib/validates_timeliness/conversion.rb rename to lib/validates_timeliness/converter.rb index 1b10b37..3b84ec9 100644 --- a/lib/validates_timeliness/conversion.rb +++ b/lib/validates_timeliness/converter.rb @@ -1,10 +1,18 @@ 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) - 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 when :time dummy_time(value) @@ -15,8 +23,8 @@ module ValidatesTimeliness else value end - if options[:ignore_usec] && value.is_a?(Time) - Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if @timezone_aware)) + if ignore_usec && value.is_a?(Time) + Timeliness::Parser.make_time(Array(value).reverse[4..9], (:current if time_zone_aware?)) else value end @@ -24,30 +32,30 @@ module ValidatesTimeliness def dummy_time(value) 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] else [0,0,0] end 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 - def evaluate_option_value(value, record) + def evaluate(value, scope=nil) case value when Time, Date value when String parse(value) when Symbol - if !record.respond_to?(value) && restriction_shorthand?(value) + if !scope.respond_to?(value) && restriction_shorthand?(value) ValidatesTimeliness.restriction_shorthand_symbols[value].call else - evaluate_option_value(record.send(value), record) + evaluate(scope.send(value)) end when Proc - result = value.arity > 0 ? value.call(record) : value.call - evaluate_option_value(result, record) + result = value.arity > 0 ? value.call(scope) : value.call + evaluate(result, scope) else value end @@ -59,14 +67,18 @@ module ValidatesTimeliness def parse(value) return nil if value.nil? + 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 - @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 rescue ArgumentError, TypeError nil end + def time_zone_aware? + @time_zone_aware + end end end diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 357069e..0a53a82 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -3,9 +3,7 @@ require 'active_model/validator' module ValidatesTimeliness class Validator < ActiveModel::EachValidator - include Conversion - - attr_reader :type, :attributes + attr_reader :type, :attributes, :converter RESTRICTIONS = { :is_at => :==, @@ -59,9 +57,10 @@ module ValidatesTimeliness raw_value = attribute_raw_value(record, attr_name) || value return if (@allow_nil && raw_value.nil?) || (@allow_blank && raw_value.blank?) - @timezone_aware = timezone_aware?(record, attr_name) - value = parse(raw_value) if value.is_a?(String) || options[:format] - value = type_cast_value(value, @type) + @converter = initialize_converter(record, attr_name) + + 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? @@ -71,7 +70,7 @@ module ValidatesTimeliness def validate_restrictions(record, attr_name, value) @restrictions_to_check.each do |restriction| 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) add_error(record, attr_name, restriction, restriction_value) and break end @@ -100,10 +99,20 @@ module ValidatesTimeliness record.read_timeliness_attribute_before_type_cast(attr_name.to_s) 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.skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) 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 diff --git a/spec/validates_timeliness/conversion_spec.rb b/spec/validates_timeliness/converter_spec.rb similarity index 62% rename from spec/validates_timeliness/conversion_spec.rb rename to spec/validates_timeliness/converter_spec.rb index 2505ff3..db68a8f 100644 --- a/spec/validates_timeliness/conversion_spec.rb +++ b/spec/validates_timeliness/converter_spec.rb @@ -1,94 +1,109 @@ -RSpec.describe ValidatesTimeliness::Conversion do - include ValidatesTimeliness::Conversion +RSpec.describe ValidatesTimeliness::Converter do + subject(:converter) { described_class.new(type: type, time_zone_aware: time_zone_aware, ignore_usec: ignore_usec) } let(:options) { Hash.new } + let(:type) { :date } + let(:time_zone_aware) { false } + let(:ignore_usec) { false } before do Timecop.freeze(Time.mktime(2010, 1, 1, 0, 0, 0)) end + delegate :type_cast_value, :evaluate, :parse, :dummy_time, to: :converter + describe "#type_cast_value" do describe "for date type" do + let(:type) { :date } + 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 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 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 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 describe "for time type" do + let(:type) { :time } + 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 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 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 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 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 describe "for datetime type" do + let(:type) { :datetime } + let(:time_zone_aware) { true } + 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 it "should return same Time value" do 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 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 it "should return same Time in correct zone if timezone aware" do - @timezone_aware = true 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.zone).to eq('AEDT') end 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 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 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 - 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) - result = type_cast_value(value, :datetime) - expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56)) - expect(result.zone).to eq('AEDT') + context do + let(:time_zone_aware) { true } + + it "should ignore usec and return time in correct zone if timezone aware" do + value = Time.utc(2010, 1, 1, 12, 34, 56, 10000) + result = type_cast_value(value) + expect(result).to eq(Time.zone.local(2010, 1, 1, 23, 34, 56)) + expect(result.zone).to eq('AEDT') + end end end end @@ -103,7 +118,6 @@ RSpec.describe ValidatesTimeliness::Conversion do end 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)) end @@ -120,61 +134,64 @@ RSpec.describe ValidatesTimeliness::Conversion do end end - describe "#evaluate_option_value" do + describe "#evaluate" do let(:person) { Person.new } it 'should return Date object as is' do value = Date.new(2010,1,1) - expect(evaluate_option_value(value, person)).to eq(value) + expect(evaluate(value, person)).to eq(value) end it 'should return Time object as is' do value = Time.mktime(2010,1,1) - expect(evaluate_option_value(value, person)).to eq(value) + expect(evaluate(value, person)).to eq(value) end it 'should return DateTime object as is' do 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 it 'should return Time value returned from proc with 0 arity' do 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 it 'should return Time value returned by record attribute call in proc arity of 1' do value = Time.mktime(2010,1,1) 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 it 'should return Time value for attribute method symbol which returns Time' do value = Time.mktime(2010,1,1) person.birth_datetime = value - expect(evaluate_option_value(:birth_datetime, person)).to eq(value) + expect(evaluate(:birth_datetime, person)).to eq(value) end it 'should return Time value is default zone from string time value' do 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 - 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' - expect(evaluate_option_value(value, person)).to eq(Time.zone.local(2010,1,1,12,0,0)) + 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 + value = '2010-01-01 12:00:00' + expect(evaluate(value, person)).to eq(Time.zone.local(2010,1,1,12,0,0)) + end end it 'should return Time value in default zone from proc which returns string time' do 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 - 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' 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 context "restriction shorthand" do @@ -183,17 +200,17 @@ RSpec.describe ValidatesTimeliness::Conversion do end 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 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 it 'should not use shorthand if symbol if is record method' do time = 1.day.from_now 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 @@ -212,13 +229,11 @@ RSpec.describe ValidatesTimeliness::Conversion do with_config(:use_plugin_parser, false) it 'should use Time.zone.parse attribute is timezone aware' do - @timezone_aware = true - expect(Time.zone).to receive(:parse) + expect(Timeliness::Parser).to_not receive(:parse) parse('2000-01-01') end 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' expect(value).to receive(:to_time) parse(value)