diff --git a/README.rdoc b/README.rdoc index 374816f..b657c2b 100644 --- a/README.rdoc +++ b/README.rdoc @@ -55,50 +55,51 @@ validation method end The list of validation methods available are as follows: - - * validates_date - validate value as date - - * validates_time - validate value as time only i.e. '12:20pm' - - * validates_datetime - validate value as a full date and time + validates_date - validate value as date + validates_time - validate value as time only i.e. '12:20pm' + validates_datetime - validate value as a full date and time The validation methods take the usual options plus some specific ones to restrict the valid range of dates or times allowed - Temporal options (or restrictions): - :before - Attribute must be before this value to be valid - :on_or_before - Attribute must be equal to or before this value to be valid - :after - Attribute must be after this value to be valid - :on_or_after - Attribute must be equal to or after this value to be valid - :between - Attribute must be between the values to be valid +Temporal options (or restrictions): + :before - Attribute must be before this value to be valid + :on_or_before - Attribute must be equal to or before this value to be valid + :after - Attribute must be after this value to be valid + :on_or_after - Attribute must be equal to or after this value to be valid + :between - Attribute must be between the values to be valid. Takes an array of two values or a range - Regular validation options: - :allow_nil - Allow a nil value to be valid - :allow_blank - Allows a nil or empty string value to be valid - :if - Execute validation when :if evaluates true - :unless - Execute validation when :unless evaluates false +Regular validation options: + :allow_nil - Allow a nil value to be valid + :allow_blank - Allows a nil or empty string value to be valid + :if - Execute validation when :if evaluates true + :unless - Execute validation when :unless evaluates false - Message options: - Use these to override the default error messages - :invalid_date_message - :invalid_time_message - :invalid_datetime_message - :before_message - :on_or_before_message - :after_message - :on_or_after_message - :between_message +Special options: + :with_time - Validate a date attribute value combined with a time value against any temporal restrictions + :with_date - Validate a time attribute value combined with a date value against any temporal restrictions + +Message options: - Use these to override the default error messages + :invalid_date_message + :invalid_time_message + :invalid_datetime_message + :before_message + :on_or_before_message + :after_message + :on_or_after_message + :between_message -The temporal restrictions can take 4 different value types: - - * String value - * Date, Time, or DateTime object value - * Proc or lambda object - * A symbol matching the method name in the model - * Between option takes an array of two values or a range +The temporal restrictions, with_date and with_time can take 4 different value types: +* String value +* Date, Time, or DateTime object value +* Proc or lambda object which may take an optional parameter being the record object +* A symbol matching the method name in the model When an attribute value is compared to temporal restrictions, they are compared as the same type as the validation method type. So using validates_date means all -values are compared as dates. +values are compared as dates. This is except in the case of with_time and with_date +options which effectively force the value to validated as a datetime against the +temporal options. == EXAMPLES: @@ -111,6 +112,10 @@ values are compared as dates. :allow_nil => true validates_datetime :appointment_date, :before => Proc.new { 1.week.from_now } + + validates_datetime :appointment_date, :before => Proc.new { 1.week.from_now } + + validates_date :entry_date, :with_time => '17:00', :on_or_before => :competition_closing === DATE/TIME FORMATS: @@ -124,44 +129,43 @@ be happy to know that is exactly the format you can use to define your own if you want. No complex regular expressions or duck punching (monkey patching) the plugin is needed. - Time formats: - hh:nn:ss - hh-nn-ss - h:nn - h.nn - h nn - h-nn - h:nn_ampm - h.nn_ampm - h nn_ampm - h-nn_ampm - h_ampm - - NOTE: Any time format without a meridian token (the 'ampm' token) is considered - in 24 hour time. - - Date formats: - yyyy/mm/dd - yyyy-mm-dd - yyyy.mm.dd - m/d/yy OR d/m/yy - m\d\yy OR d\m\yy - d-m-yy - d.m.yy - d mmm yy - - NOTE: To use non-US date formats see US/EURO FORMATS section - - Datetime formats: - m/d/yy h:nn:ss OR d/m/yy hh:nn:ss - m/d/yy h:nn OR d/m/yy h:nn - m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm - yyyy-mm-dd hh:nn:ss - yyyy-mm-dd h:nn - ddd mmm d hh:nn:ss zo yyyy # Ruby time string - yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601 +Time formats: + hh:nn:ss + hh-nn-ss + h:nn + h.nn + h nn + h-nn + h:nn_ampm + h.nn_ampm + h nn_ampm + h-nn_ampm + h_ampm - NOTE: To use non-US date formats see US/EURO FORMATS section +NOTE: Any time format without a meridian token (the 'ampm' token) is considered in 24 hour time. + +Date formats: + yyyy/mm/dd + yyyy-mm-dd + yyyy.mm.dd + m/d/yy OR d/m/yy + m\d\yy OR d\m\yy + d-m-yy + d.m.yy + d mmm yy + +NOTE: To use non-US date formats see US/EURO FORMATS section + +Datetime formats: + m/d/yy h:nn:ss OR d/m/yy hh:nn:ss + m/d/yy h:nn OR d/m/yy h:nn + m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm + yyyy-mm-dd hh:nn:ss + yyyy-mm-dd h:nn + ddd mmm d hh:nn:ss zo yyyy # Ruby time string + yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601 + +NOTE: To use non-US date formats see US/EURO FORMATS section Here is what each format token means: @@ -223,7 +227,7 @@ Done! That format is no longer considered valid. Easy! Ok, now I hear you say "Well I have format that I want to use but you don't have it". Ahh, then add it yourself. Again stick this in an initializer file - ValidatesTimeliness::Formats.add_formats(:time, "d o'clock") + ValidatesTimeliness::Formats.add_formats(:time, "d o'clock") Now "10 o'clock" will be a valid value. So easy, no more whingeing! @@ -238,7 +242,7 @@ with an existing format, will mean your format is ignored. If you need to make your new format higher precedence than an existing format, you can include the before option like so - ValidatesTimeliness::Formats.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss') + ValidatesTimeliness::Formats.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss') Now a time of '59:30:23' will be interpreted as 11:30:59 pm. This option saves you adding a new one and deleting an old one to get it to work. diff --git a/TODO b/TODO index c85ebe7..3e79475 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ - :format option -- :with_date and :with_time options - valid formats could come from locale file - add replace_formats instead add_formats :before diff --git a/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb b/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb index 2616d77..f215554 100644 --- a/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +++ b/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb @@ -92,8 +92,8 @@ module Spec end def parse_and_cast(value) - value = @validator.send(:restriction_value, value, @record) - @validator.send(:type_cast_value, value) + value = @validator.class.send(:evaluate_option_value, value, @type, @record) + @validator.class.send(:type_cast_value, value, @type) end def error_matching(value, option) @@ -117,11 +117,11 @@ module Spec def error_message_for(option) msg = @validator.send(:error_messages)[option] - restriction = @validator.send(:restriction_value, @validator.configuration[option], @record) + restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[option], @type, @record) if restriction restriction = [restriction] unless restriction.is_a?(Array) - restriction.map! {|r| @validator.send(:type_cast_value, r) } + restriction.map! {|r| @validator.class.send(:type_cast_value, r, @type) } interpolate = @validator.send(:interpolation_values, option, restriction ) # get I18n message if defined and has interpolation keys in msg if defined?(I18n) && !@validator.send(:custom_error_messages).include?(option) diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index d2bce54..bde905a 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -20,7 +20,8 @@ module ValidatesTimeliness } VALID_OPTIONS = [ - :on, :if, :unless, :allow_nil, :empty, :allow_blank, :blank, :invalid_time_message, :invalid_date_message, :invalid_datetime_message + :on, :if, :unless, :allow_nil, :empty, :allow_blank, :blank, :with_time, :with_date, + :invalid_time_message, :invalid_date_message, :invalid_datetime_message ] + RESTRICTION_METHODS.keys.map {|option| [option, "#{option}_message".to_sym] }.flatten attr_reader :configuration, :type @@ -52,14 +53,21 @@ module ValidatesTimeliness end def validate_restrictions(record, attr_name, value) - value = type_cast_value(value) - + value = if @configuration[:with_time] || @configuration[:with_date] + restriction_type = :datetime + combine_date_and_time(value, record) + else + restriction_type = type + self.class.type_cast_value(value, type) + end + return if value.nil? + RESTRICTION_METHODS.each do |option, method| next unless restriction = configuration[option] begin - restriction = restriction_value(restriction, record) + restriction = self.class.evaluate_option_value(restriction, restriction_type, record) next if restriction.nil? - restriction = type_cast_value(restriction) + restriction = self.class.type_cast_value(restriction, restriction_type) unless evaluate_restriction(restriction, value, method) add_error(record, attr_name, option, interpolation_values(option, restriction)) @@ -123,49 +131,67 @@ module ValidatesTimeliness } end - def restriction_value(restriction, record) - case restriction - when Time, Date, DateTime - restriction - when Symbol - 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) + def combine_date_and_time(value, record) + if type == :date + date = value + time = @configuration[:with_time] else - record.class.parse_date_time(restriction, type, false) - end - end - - def type_cast_value(value) - if value.is_a?(Array) - value.map {|v| type_cast_value(v) } - else - case type - when :time - value.to_dummy_time - when :date - value.to_date - when :datetime - if value.is_a?(DateTime) || value.is_a?(Time) - value.to_time - else - value.to_time(ValidatesTimeliness.default_timezone) - end - else - nil - end + date = @configuration[:with_date] + time = value end + date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record) + return if date.nil? || time.nil? + record.class.send(:make_time, [date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec]) end def validate_options(options) - invalid_types = [:time, :date, :datetime] - invalid_types.delete(@type) - valid_options = VALID_OPTIONS.reject {|option| invalid_types.include?("#{option}_message".to_sym) } - options.assert_valid_keys(valid_options) + invalid_for_type = ([:time, :date, :datetime] - [@type]).map {|k| "invalid_#{k}_message".to_sym } + invalid_for_type << :with_date unless @type == :time + invalid_for_type << :with_time unless @type == :date + options.assert_valid_keys(VALID_OPTIONS - invalid_for_type) + end + + # class methods + class << self + + def evaluate_option_value(value, type, record) + case value + when Time, Date, DateTime + value + when Symbol + evaluate_option_value(record.send(value), type, record) + when Proc + evaluate_option_value(value.call(record), type, record) + when Array + value.map {|r| evaluate_option_value(r, type, record) }.sort + when Range + evaluate_option_value([value.first, value.last], type, record) + else + record.class.parse_date_time(value, type, false) + end + end + + def type_cast_value(value, type) + if value.is_a?(Array) + value.map {|v| type_cast_value(v, type) } + else + case type + when :time + value.to_dummy_time + when :date + value.to_date + when :datetime + if value.is_a?(DateTime) || value.is_a?(Time) + value.to_time + else + value.to_time(ValidatesTimeliness.default_timezone) + end + else + nil + end + end + end + end end diff --git a/spec/validator_spec.rb b/spec/validator_spec.rb index 623fc17..c2618d0 100644 --- a/spec/validator_spec.rb +++ b/spec/validator_spec.rb @@ -18,9 +18,8 @@ describe ValidatesTimeliness::Validator do describe "option keys validation" do before do - @valid_options = ValidatesTimeliness::Validator::VALID_OPTIONS.inject({}) {|hash, opt| hash[opt] = nil; hash } - @valid_options.delete(:invalid_date_message) - @valid_options.delete(:invalid_time_message) + keys = ValidatesTimeliness::Validator::VALID_OPTIONS - [:invalid_date_message, :invalid_time_message, :with_date, :with_time] + @valid_options = keys.inject({}) {|hash, opt| hash[opt] = nil; hash } end it "should raise error if invalid option key passed" do @@ -33,55 +32,55 @@ describe ValidatesTimeliness::Validator do end end - describe "restriction_value" do + describe "evaluate_option_value" do it "should return Time object when restriction is Time object" do - restriction_value(Time.now, :datetime).should be_kind_of(Time) + evaluate_option_value(Time.now, :datetime).should be_kind_of(Time) end it "should return Time object when restriction is string" do - restriction_value("2007-01-01 12:00", :datetime).should be_kind_of(Time) + evaluate_option_value("2007-01-01 12:00", :datetime).should be_kind_of(Time) end it "should return Time object when restriction is method and method returns Time object" do person.stub!(:datetime_attr).and_return(Time.now) - restriction_value(:datetime_attr, :datetime).should be_kind_of(Time) + evaluate_option_value(:datetime_attr, :datetime).should be_kind_of(Time) end it "should return Time object when restriction is method and method returns string" do person.stub!(:datetime_attr).and_return("2007-01-01 12:00") - restriction_value(:datetime_attr, :datetime).should be_kind_of(Time) + evaluate_option_value(:datetime_attr, :datetime).should be_kind_of(Time) end it "should return Time object when restriction is proc which returns Time object" do - restriction_value(lambda { Time.now }, :datetime).should be_kind_of(Time) + evaluate_option_value(lambda { Time.now }, :datetime).should be_kind_of(Time) end it "should return Time object when restriction is proc which returns string" do - restriction_value(lambda {"2007-01-01 12:00"}, :datetime).should be_kind_of(Time) + evaluate_option_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] + evaluate_option_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)] + evaluate_option_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] + evaluate_option_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)] + evaluate_option_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] end - def restriction_value(restriction, type) + def evaluate_option_value(restriction, type) configure_validator(:type => type) - validator.send(:restriction_value, restriction, person) + validator.class.send(:evaluate_option_value, restriction, type, person) end end @@ -347,6 +346,37 @@ describe ValidatesTimeliness::Validator do end end + describe "instance with :with_time option" do + + it "should validate date attribute as datetime combining value of :with_time against restrictions " do + configure_validator(:type => :date, :with_time => '12:31', :on_or_before => Time.mktime(2000,1,1,12,30)) + validate_with(:birth_date, "2000-01-01") + should_have_error(:birth_date, :on_or_before) + end + + it "should skip restriction validation if :with_time value is nil" do + configure_validator(:type => :date, :with_time => nil, :on_or_before => Time.mktime(2000,1,1,12,30)) + validate_with(:birth_date, "2000-01-01") + should_have_no_error(:birth_date, :on_or_before) + end + + end + + describe "instance with :with_date option" do + + it "should validate time attribute as datetime combining value of :with_date against restrictions " do + configure_validator(:type => :time, :with_date => '2009-01-01', :on_or_before => Time.mktime(2000,1,1,12,30)) + validate_with(:birth_date, "12:30") + should_have_error(:birth_date, :on_or_before) + end + + it "should skip restriction validation if :with_date value is nil" do + configure_validator(:type => :time, :with_date => nil, :on_or_before => Time.mktime(2000,1,1,12,30)) + validate_with(:birth_date, "12:30") + should_have_no_error(:birth_date, :on_or_before) + end + end + describe "instance with mixed value and restriction types" do it "should validate datetime attribute with Date restriction" do