diff --git a/lib/validates_timeliness/locale/en.yml b/lib/validates_timeliness/locale/en.yml index 1c5e63c..83e544b 100644 --- a/lib/validates_timeliness/locale/en.yml +++ b/lib/validates_timeliness/locale/en.yml @@ -9,3 +9,4 @@ en: on_or_before: "must be on or before {{restriction}}" after: "must be after {{restriction}}" on_or_after: "must be on or after {{restriction}}" + between: "must be between {{earliest}} and {{latest}}" diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 1c2fcf1..0ae431d 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -11,6 +11,14 @@ module ValidatesTimeliness :datetime => '%Y-%m-%d %H:%M:%S' } + RESTRICTION_METHODS = { + :before => :<, + :after => :>, + :on_or_before => :<=, + :on_or_after => :>=, + :between => lambda {|v, r| (r.first..r.last).include?(v) } + } + attr_reader :configuration, :type def initialize(configuration) @@ -40,21 +48,17 @@ module ValidatesTimeliness end def validate_restrictions(record, attr_name, value) - restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='} - - display = self.class.error_value_formats[type] - value = type_cast_value(value) - restriction_methods.each do |option, method| + RESTRICTION_METHODS.each do |option, method| next unless restriction = configuration[option] begin - compare = restriction_value(restriction, record) - next if compare.nil? - compare = type_cast_value(compare) + restriction = restriction_value(restriction, record) + next if restriction.nil? + restriction = type_cast_value(restriction) - unless value.send(method, compare) - add_error(record, attr_name, option, :restriction => compare.strftime(display)) + unless evaluate_restriction(restriction, value, method) + add_error(record, attr_name, option, interpolation_values(option, restriction)) end rescue unless self.class.ignore_restriction_errors @@ -63,15 +67,41 @@ module ValidatesTimeliness end end end + + def interpolation_values(option, restriction) + format = self.class.error_value_formats[type] + restriction = [restriction] unless restriction.is_a?(Array) + + if defined?(I18n) + message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option] + subs = message.scan(/\{\{([^\}]*)\}\}/) + interpolations = {} + subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) } + interpolations + else + restriction.map {|r| r.strftime(format) } + end + end + + def evaluate_restriction(restriction, value, comparator) + return true if restriction.nil? + + case comparator + when Symbol + value.send(comparator, restriction) + when Proc + comparator.call(value, restriction) + end + end - def add_error(record, attr_name, message, interpolate={}) + def add_error(record, attr_name, message, interpolate=nil) if defined?(I18n) # use i18n support in AR for message or use custom message passed to validation method custom = custom_error_messages[message] - record.errors.add(attr_name, custom || message, interpolate) + record.errors.add(attr_name, custom || message, interpolate || {}) else message = error_messages[message] if message.is_a?(Symbol) - message = message % interpolate.values unless interpolate.empty? + message = message % interpolate record.errors.add(attr_name, message) end end @@ -83,7 +113,12 @@ module ValidatesTimeliness def custom_error_messages return @custom_error_messages if defined?(@custom_error_messages) - @custom_error_messages = configuration.inject({}) {|h, (k, v)| h[$1.to_sym] = v if k.to_s =~ /(.*)_message$/;h } + @custom_error_messages = configuration.inject({}) {|msgs, (k, v)| + if md = /(.*)_message$/.match(k.to_s) + msgs[md[0].to_sym] = v + end + msgs + } end def restriction_value(restriction, record) @@ -94,13 +129,20 @@ module ValidatesTimeliness restriction_value(record.send(restriction), record) when Proc restriction_value(restriction.call(record), record) + when Array + restriction.map {|r| restriction_value(r, record) }.sort + when Range + restriction_value([restriction.first, restriction.last], record) else record.class.parse_date_time(restriction, type, false) end end def type_cast_value(value) - case type + if value.is_a?(Array) + value.map {|v| type_cast_value(v) } + else + case type when :time value.to_dummy_time when :date @@ -109,10 +151,11 @@ module ValidatesTimeliness if value.is_a?(DateTime) || value.is_a?(Time) value.to_time else - value.to_time(ValidatesTimelines.default_timezone) + value.to_time(ValidatesTimeliness.default_timezone) end else nil + end end end diff --git a/spec/validator_spec.rb b/spec/validator_spec.rb index 620f1dd..d4750c9 100644 --- a/spec/validator_spec.rb +++ b/spec/validator_spec.rb @@ -43,6 +43,25 @@ describe ValidatesTimeliness::Validator do restriction_value(lambda {"2007-01-01 12:00"}, :datetime).should be_kind_of(Time) end + it "should return array of Time objects when restriction is array of Time objects" do + time1, time2 = Time.now, 1.day.ago + restriction_value([time1, time2], :datetime).should == [time2, time1] + end + + it "should return array of Time objects when restriction is array of strings" do + time1, time2 = "2000-01-02", "2000-01-01" + restriction_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] + end + + it "should return array of Time objects when restriction is Range of Time objects" do + time1, time2 = Time.now, 1.day.ago + restriction_value(time1..time2, :datetime).should == [time2, time1] + end + + it "should return array of Time objects when restriction is Range of time strings" do + time1, time2 = "2000-01-02", "2000-01-01" + restriction_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] + end def restriction_value(restriction, type) configure_validator(:type => type) validator.send(:restriction_value, restriction, person) @@ -212,83 +231,101 @@ describe ValidatesTimeliness::Validator do end end - describe "instance with on_or_before and on_or_after restrictions" do + describe "instance with between restriction" do describe "for datetime type" do before do - configure_validator(:on_or_before => Time.now.at_midnight, :on_or_after => 1.day.ago) + configure_validator(:between => [1.day.ago.at_midnight, 1.day.from_now.at_midnight]) end - it "should have error when value is past :on_or_before restriction" do - validate_with(:birth_date_and_time, Time.now.at_midnight + 1) - should_have_error(:birth_date_and_time, :on_of_before) + it "should have error when value is before earlist :between restriction" do + validate_with(:birth_date_and_time, 2.days.ago) + should_have_error(:birth_date_and_time, :between) end - it "should be valid when value is equal to :on_or_before restriction" do - validate_with(:birth_date_and_time, Time.now.at_midnight) - should_have_no_error(:birth_date_and_time, :on_of_before) + it "should have error when value is after latest :between restriction" do + validate_with(:birth_date_and_time, 2.days.from_now) + should_have_error(:birth_date_and_time, :between) end - it "should have error when value is before :on_or_after restriction" do - validate_with(:birth_date_and_time, 1.days.ago - 1) - should_have_error(:birth_date_and_time, :on_of_after) + it "should be valid when value is equal to earliest :between restriction" do + validate_with(:birth_date_and_time, 1.day.ago.at_midnight) + should_have_no_error(:birth_date_and_time, :between) end - it "should be valid when value is value equal to :on_or_after restriction" do - validate_with(:birth_date_and_time, 1.day.ago) - should_have_no_error(:birth_date_and_time, :on_of_after) + it "should be valid when value is equal to latest :between restriction" do + validate_with(:birth_date_and_time, 1.day.from_now.at_midnight) + should_have_no_error(:birth_date_and_time, :between) + end + + it "should allow a range for between restriction" do + configure_validator(:type => :datetime, :between => (1.day.ago.at_midnight)..(1.day.from_now.at_midnight)) + validate_with(:birth_date_and_time, 1.day.from_now.at_midnight) + should_have_no_error(:birth_date_and_time, :between) end end describe "for date type" do - before :each do - configure_validator(:on_or_before => 1.day.from_now, :on_or_after => 1.day.ago, :type => :date) + before do + configure_validator(:type => :date, :between => [1.day.ago.to_date, 1.day.from_now.to_date]) end - it "should have error when value is past :on_or_before restriction" do - validate_with(:birth_date, 2.days.from_now) - should_have_error(:birth_date, :on_or_before) + it "should have error when value is before earlist :between restriction" do + validate_with(:birth_date, 2.days.ago.to_date) + should_have_error(:birth_date, :between) end - it "should have error when value is before :on_or_after restriction" do - validate_with(:birth_date, 2.days.ago) - should_have_error(:birth_date, :on_or_after) + it "should have error when value is after latest :between restriction" do + validate_with(:birth_date, 2.days.from_now.to_date) + should_have_error(:birth_date, :between) end - it "should be valid when value is equal to :on_or_before restriction" do - validate_with(:birth_date, 1.day.from_now) - should_have_no_error(:birth_date, :on_or_before) + it "should be valid when value is equal to earliest :between restriction" do + validate_with(:birth_date, 1.day.ago.to_date) + should_have_no_error(:birth_date, :between) end - it "should be valid when value value is equal to :on_or_after restriction" do - validate_with(:birth_date, 1.day.ago) - should_have_no_error(:birth_date, :on_or_before) + it "should be valid when value is equal to latest :between restriction" do + validate_with(:birth_date, 1.day.from_now.to_date) + should_have_no_error(:birth_date, :between) + end + + it "should allow a range for between restriction" do + configure_validator(:type => :date, :between => (1.day.ago.to_date)..(1.day.from_now.to_date)) + validate_with(:birth_date, 1.day.from_now.to_date) + should_have_no_error(:birth_date, :between) end end describe "for time type" do - before :each do - configure_validator(:on_or_before => "23:00", :on_or_after => "06:00", :type => :time) + before do + configure_validator(:type => :time, :between => ["09:00", "17:00"]) end - it "should have error when value is past :on_or_before restriction" do - validate_with(:birth_time, "23:01") - should_have_error(:birth_time, :on_or_before) + it "should have error when value is before earlist :between restriction" do + validate_with(:birth_time, "08:59") + should_have_error(:birth_time, :between) end - it "should have error when value is before :on_or_after restriction" do - validate_with(:birth_time, "05:59") - should_have_error(:birth_time, :on_or_after) + it "should have error when value is after latest :between restriction" do + validate_with(:birth_time, "17:01") + should_have_error(:birth_time, :between) end - it "should be valid when value is on boundary of :on_or_before restriction" do - validate_with(:birth_time, "23:00") - should_have_no_error(:birth_time, :on_or_before) + it "should be valid when value is equal to earliest :between restriction" do + validate_with(:birth_time, "09:00") + should_have_no_error(:birth_time, :between) end - it "should be valid when value is on boundary of :on_or_after restriction" do - validate_with(:birth_time, "06:00") - should_have_no_error(:birth_time, :on_or_after) + it "should be valid when value is equal to latest :between restriction" do + validate_with(:birth_time, "17:00") + should_have_no_error(:birth_time, :between) + end + + it "should allow a range for between restriction" do + configure_validator(:type => :time, :between => "09:00".."17:00") + validate_with(:birth_time, "17:00") + should_have_no_error(:birth_time, :between) end end end @@ -433,6 +470,6 @@ describe ValidatesTimeliness::Validator do def error_messages return @error_messages if defined?(@error_messages) messages = validator.send(:error_messages) - @error_messages = messages.inject({}) {|h, (k, v)| h[k] = v.gsub(/ (\%s|\{\{\w*\}\})/, ''); h } + @error_messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(/ (\%s|\{\{\w*\}\}).*/, ''); h } end end