refactored error message handling and specs

This commit is contained in:
Adam Meehan 2008-12-02 19:36:03 +11:00
parent aa42fb76b6
commit d71f581e10
7 changed files with 167 additions and 181 deletions

View File

@ -21,3 +21,31 @@ Date.send(:include, ValidatesTimeliness::CoreExtensions::Date)
DateTime.send(:include, ValidatesTimeliness::CoreExtensions::DateTime) DateTime.send(:include, ValidatesTimeliness::CoreExtensions::DateTime)
ValidatesTimeliness::Formats.compile_format_expressions ValidatesTimeliness::Formats.compile_format_expressions
module ValidatesTimeliness
mattr_accessor :ignore_datetime_restriction_errors
mattr_accessor :date_time_error_value_formats
mattr_accessor :default_error_messages
@@ignore_datetime_restriction_errors = false
@@date_time_error_value_formats = {
:time => '%H:%M:%S',
:date => '%Y-%m-%d',
:datetime => '%Y-%m-%d %H:%M:%S'
}
@@default_error_messages = {
:empty => "cannot be empty",
:blank => "cannot be blank",
:invalid_date => "is not a valid date",
:invalid_time => "is not a valid time",
:invalid_datetime => "is not a valid datetime",
:before => "must be before %s",
:on_or_before => "must be on or before %s",
:after => "must be after %s",
:on_or_after => "must be on or after %s"
}
end

View File

@ -2,26 +2,33 @@ module Spec
module Rails module Rails
module Matchers module Matchers
class ValidateTimeliness class ValidateTimeliness
cattr_accessor :test_values
@@test_values = {
:date => {:pass => '2000-01-01', :fail => '2000-01-32'},
:time => {:pass => '12:00', :fail => '25:00'},
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
}
def initialize(attribute, options) def initialize(attribute, options)
@expected, @options = attribute, options @expected, @options = attribute, options
@options.reverse_merge!(error_messages) compile_error_messages
end end
def compile_error_messages
validator = ValidatesTimeliness::Validator.new(options)
messages = validator.send(:error_messages)
@messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(' %s', ''); h }
end
def matches?(record) def matches?(record)
@record = record @record = record
type = options[:type] type = options[:type]
test_values = { invalid_value = @@test_values[type][:fail]
:date => {:pass => '2000-01-01', :fail => '2000-01-32'}, valid_value = parse_and_cast(@@test_values[type][:pass])
:time => {:pass => '12:00', :fail => '25:00'}, valid = error_matching(invalid_value, /#{messages["invalid_#{type}".to_sym]}/) &&
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'} no_error_matching(valid_value, /#{messages["invalid_#{type}".to_sym]}/)
}
invalid_value = test_values[type][:fail]
valid_value = parse_and_cast(test_values[type][:pass])
valid = error_matching(invalid_value, /#{options["invalid_#{type}_message".to_sym]}/) &&
no_error_matching(valid_value, /#{options["invalid_#{type}_message".to_sym]}/)
valid = test_option(:before, :-) if options[:before] && valid valid = test_option(:before, :-) if options[:before] && valid
valid = test_option(:after, :+) if options[:after] && valid valid = test_option(:after, :+) if options[:after] && valid
@ -45,7 +52,7 @@ module Spec
end end
private private
attr_reader :actual, :expected, :record, :options, :last_failure attr_reader :actual, :expected, :record, :options, :messages, :last_failure
def test_option(option, modifier, settings={}) def test_option(option, modifier, settings={})
settings.reverse_merge!(:modify_on => :valid) settings.reverse_merge!(:modify_on => :valid)
@ -57,29 +64,23 @@ module Spec
[ boundary, boundary.send(modifier, 1) ] [ boundary, boundary.send(modifier, 1) ]
end end
message = options["#{option}_message".to_sym] message = messages[option]
error_matching(invalid_value, /#{message}/) && error_matching(invalid_value, /#{message}/) &&
no_error_matching(valid_value, /#{message}/) no_error_matching(valid_value, /#{message}/)
end end
def parse_and_cast(value) def parse_and_cast(value)
value = ValidatesTimeliness::Validator.send(:restriction_value, value, record, options[:type]) value = ValidatesTimeliness::Validator.send(:restriction_value, value, record, options[:type])
cast_method = ValidatesTimeliness::Validator.send(:restriction_type_cast_method, options[:type]) cast_method = ValidatesTimeliness::Validator.send(:restriction_type_cast_method, options[:type])
value.send(cast_method) rescue nil value.send(cast_method) rescue nil
end end
def error_messages
messages = ValidatesTimeliness::Validator.send(:mapped_default_error_messages)
messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(' %s', ''); h }
@options.reverse_merge!(messages)
end
def error_matching(value, match) def error_matching(value, match)
record.send("#{expected}=", value) record.send("#{expected}=", value)
record.valid? record.valid?
errors = record.errors.on(expected) errors = record.errors.on(expected)
pass = [ errors ].flatten.any? {|error| match === error } pass = [ errors ].flatten.any? {|error| match === error }
@last_failure = "error matching #{match.inspect} when value is #{format_value(value)}" unless pass @last_failure = "error matching #{match.inspect} when value is #{format_value(value)} #{errors.inspect}" unless pass
pass pass
end end
@ -91,7 +92,7 @@ module Spec
def format_value(value) def format_value(value)
return value if value.is_a?(String) return value if value.is_a?(String)
value.strftime(ValidatesTimeliness::Validator.date_time_error_value_formats[options[:type]]) value.strftime(ValidatesTimeliness.date_time_error_value_formats[options[:type]])
end end
end end
@ -112,9 +113,10 @@ module Spec
private private
def validate_timeliness_of(attribute, options={}) def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options) ValidateTimeliness.new(attribute, options)
end end
end end
end end
end end

View File

@ -68,8 +68,6 @@ module ValidatesTimeliness
validates_each(attr_names, configuration) do |record, attr_name, value| validates_each(attr_names, configuration) do |record, attr_name, value|
raw_value = record.send("#{attr_name}_before_type_cast") raw_value = record.send("#{attr_name}_before_type_cast")
validator.call(record, attr_name, raw_value) validator.call(record, attr_name, raw_value)
errors = validator.errors
add_errors(record, attr_name, errors) unless errors.empty?
end end
end end
@ -88,9 +86,6 @@ module ValidatesTimeliness
end end
end end
def add_errors(record, attr_name, errors)
errors.each {|e| record.errors.add(attr_name, e) }
end
end end
end end

View File

@ -4,57 +4,26 @@ module ValidatesTimeliness
# The validity of values can be restricted to be before or after certain dates # The validity of values can be restricted to be before or after certain dates
# or times. # or times.
class Validator class Validator
attr_accessor :configuration, :errors attr_reader :configuration, :type, :messages
cattr_accessor :ignore_datetime_restriction_errors
cattr_accessor :date_time_error_value_formats
cattr_accessor :default_error_messages
@@ignore_datetime_restriction_errors = false
@@date_time_error_value_formats = {
:time => '%H:%M:%S',
:date => '%Y-%m-%d',
:datetime => '%Y-%m-%d %H:%M:%S'
}
@@default_error_messages = {
:empty => "cannot be empty",
:blank => "cannot be blank",
:invalid_date => "is not a valid date",
:invalid_time => "is not a valid time",
:invalid_datetime => "is not a valid datetime",
:before => "must be before %s",
:on_or_before => "must be on or before %s",
:after => "must be after %s",
:on_or_after => "must be on or after %s"
}
def initialize(configuration) def initialize(configuration)
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false } defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
defaults.update(self.class.mapped_default_error_messages)
@configuration = defaults.merge(configuration) @configuration = defaults.merge(configuration)
@errors = [] @type = @configuration.delete(:type)
end end
# The main validation method which can be used directly or called through # The main validation method which can be used directly or called through
# the other specific type validation methods. # the other specific type validation methods.
def call(record, attr_name, value) def call(record, attr_name, value)
@errors = []
return if (value.nil? && configuration[:allow_nil]) || (value.blank? && configuration[:allow_blank]) return if (value.nil? && configuration[:allow_nil]) || (value.blank? && configuration[:allow_blank])
@errors << configuration[:blank_message] and return if value.blank? add_error(record, attr_name, :blank) and return if value.blank?
begin time = record.class.parse_date_time(value, type)
unless time = record.class.parse_date_time(value, configuration[:type]) unless time
@errors << configuration["invalid_#{configuration[:type]}_message".to_sym] add_error(record, attr_name, "invalid_#{type}".to_sym) and return
return
end
validate_restrictions(record, attr_name, time)
rescue Exception => e
@errors << configuration["invalid_#{configuration[:type]}_message".to_sym]
end end
validate_restrictions(record, attr_name, time)
end end
private private
@ -65,27 +34,42 @@ module ValidatesTimeliness
def validate_restrictions(record, attr_name, value) def validate_restrictions(record, attr_name, value)
restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='} restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='}
type_cast_method = self.class.restriction_type_cast_method(configuration[:type]) type_cast_method = self.class.restriction_type_cast_method(type)
display = @@date_time_error_value_formats[configuration[:type]] display = ValidatesTimeliness.date_time_error_value_formats[type]
value = value.send(type_cast_method) value = value.send(type_cast_method)
restriction_methods.each do |option, method| restriction_methods.each do |option, method|
next unless restriction = configuration[option] next unless restriction = configuration[option]
begin begin
compare = self.class.restriction_value(restriction, record, configuration[:type]) compare = self.class.restriction_value(restriction, record, type)
next if compare.nil? next if compare.nil?
compare = compare.send(type_cast_method) compare = compare.send(type_cast_method)
@errors << (configuration["#{option}_message".to_sym] % compare.strftime(display)) unless value.send(method, compare)
unless value.send(method, compare)
add_error(record, attr_name, error_messages[option] % compare.strftime(display))
end
rescue rescue
@errors << "restriction '#{option}' value was invalid" unless self.ignore_datetime_restriction_errors unless ValidatesTimeliness.ignore_datetime_restriction_errors
add_error(record, attr_name, "restriction '#{option}' value was invalid")
end
end end
end end
end end
def add_error(record, attr_name, message)
message = error_messages[message] if message.is_a?(Symbol)
record.errors.add(attr_name, message)
end
def error_messages
return @error_messages if defined?(@error_messages)
custom = {}
configuration.each {|k, v| custom[$1.to_sym] = v if k.to_s =~ /(.*)_message$/ }
@error_messages = ValidatesTimeliness.default_error_messages.merge(custom)
end
def self.restriction_value(restriction, record, type) def self.restriction_value(restriction, record, type)
case restriction case restriction
when Time, Date, DateTime when Time, Date, DateTime
@ -106,12 +90,6 @@ module ValidatesTimeliness
when :datetime then :to_time when :datetime then :to_time
end end
end end
# Map error message keys to *_message to merge with validation options
def self.mapped_default_error_messages
returning({}) do |messages|
@@default_error_messages.each {|k, v| messages["#{k}_message".to_sym] = v }
end
end
end end
end end

View File

@ -27,8 +27,6 @@ require 'active_record/version'
require 'action_controller' require 'action_controller'
require 'action_view' require 'action_view'
ActiveSupport::Deprecation.silenced = true
require 'spec/rails' require 'spec/rails'
require 'time_travel/time_travel' require 'time_travel/time_travel'
require 'validates_timeliness' require 'validates_timeliness'

View File

@ -1,26 +1,33 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper') require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
class NoValidation < Person
end
class WithValidation < Person
validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02'
validates_time :birth_time,
:before => '23:00', :after => '09:00',
:on_or_before => '22:00', :on_or_after => '10:00'
validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09:00'
end
class CustomMessages < Person
validates_date :birth_date, :invalid_date_message => 'is not really a date',
:before => '2000-01-10', :before_message => 'is too late',
:after => '2000-01-01', :after_message => 'is too early',
:on_or_before=> '2000-01-09', :on_or_before_message => 'is just too late',
:on_or_after => '2000-01-02', :on_or_after_message => 'is just too early'
end
describe "ValidateTimeliness matcher" do describe "ValidateTimeliness matcher" do
attr_accessor :no_validation, :with_validation attr_accessor :no_validation, :with_validation
before do before do
class NoValidation < Person
alias_attribute :birth_datetime, :birth_date_and_time
end
class WithValidation < Person
validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02'
validates_time :birth_time,
:before => '23:00', :after => '09:00',
:on_or_before => '22:00', :on_or_after => '10:00'
validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09:00'
alias_attribute :birth_datetime, :birth_date_and_time
end
@no_validation = NoValidation.new @no_validation = NoValidation.new
@with_validation = WithValidation.new @with_validation = WithValidation.new
end end
@ -134,14 +141,8 @@ describe "ValidateTimeliness matcher" do
end end
describe "custom messages" do describe "custom messages" do
before do before do
class CustomMessages < Person
validates_date :birth_date, :invalid_date_message => 'is not really a date',
:before => '2000-01-10', :before_message => 'is too late',
:after => '2000-01-01', :after_message => 'is too early',
:on_or_before=> '2000-01-09', :on_or_before_message => 'is just too late',
:on_or_after => '2000-01-02', :on_or_after_message => 'is just too early'
end
@person = CustomMessages.new @person = CustomMessages.new
end end

View File

@ -48,12 +48,10 @@ describe ValidatesTimeliness::ValidationMethods do
describe "with no restrictions" do describe "with no restrictions" do
before :all do class BasicValidation < Person
class BasicValidation < Person validates_datetime :birth_date_and_time, :allow_blank => true
validates_datetime :birth_date_and_time, :allow_blank => true validates_date :birth_date, :allow_blank => true
validates_date :birth_date, :allow_blank => true validates_time :birth_time, :allow_blank => true
validates_time :birth_time, :allow_blank => true
end
end end
before :each do before :each do
@ -104,7 +102,7 @@ describe ValidatesTimeliness::ValidationMethods do
it "should be valid with nil values when allow_blank is true" do it "should be valid with nil values when allow_blank is true" do
person.birth_date_and_time = nil person.birth_date_and_time = nil
person.birth_date = nil person.birth_date = nil
person.birth_time = nil person.birth_time = nil
person.should be_valid person.should be_valid
end end
end end
@ -112,11 +110,9 @@ describe ValidatesTimeliness::ValidationMethods do
describe "for datetime type" do describe "for datetime type" do
describe "with before and after restrictions" do describe "with before and after restrictions" do
before :all do class DateTimeBeforeAfter < Person
class DateTimeBeforeAfter < Person validates_datetime :birth_date_and_time,
validates_datetime :birth_date_and_time, :before => lambda { Time.now }, :after => lambda { 1.day.ago}
:before => lambda { Time.now }, :after => lambda { 1.day.ago}
end
end end
before :each do before :each do
@ -149,12 +145,10 @@ describe ValidatesTimeliness::ValidationMethods do
end end
describe "with on_or_before and on_or_after restrictions" do describe "with on_or_before and on_or_after restrictions" do
before :all do class DateTimeOnOrBeforeAndAfter < Person
class DateTimeOnOrBeforeAndAfter < Person validates_datetime :birth_date_and_time, :type => :datetime,
validates_datetime :birth_date_and_time, :type => :datetime, :on_or_before => lambda { Time.now.at_midnight },
:on_or_before => lambda { Time.now.at_midnight }, :on_or_after => lambda { 1.day.ago }
:on_or_after => lambda { 1.day.ago }
end
end end
before do before do
@ -180,7 +174,7 @@ describe ValidatesTimeliness::ValidationMethods do
it "should be valid when value equal to :on_or_after restriction" do it "should be valid when value equal to :on_or_after restriction" do
person.birth_date_and_time = 1.day.ago person.birth_date_and_time = 1.day.ago
person.should be_valid person.should be_valid
end end
end end
@ -190,11 +184,11 @@ describe ValidatesTimeliness::ValidationMethods do
describe "with before and after restrictions" do describe "with before and after restrictions" do
before :all do before :all do
class DateBeforeAfter < Person class DateBeforeAfter < Person
validates_date :birth_date, validates_date :birth_date,
:before => 1.day.from_now, :before => 1.day.from_now,
:after => 1.day.ago :after => 1.day.ago
end end
end end
before :each do before :each do
@ -216,13 +210,13 @@ describe ValidatesTimeliness::ValidationMethods do
describe "with on_or_before and on_or_after restrictions" do describe "with on_or_before and on_or_after restrictions" do
before :all do before :all do
class DateOnOrBeforeAndAfter < Person class DateOnOrBeforeAndAfter < Person
validates_date :birth_date, validates_date :birth_date,
:on_or_before => 1.day.from_now, :on_or_before => 1.day.from_now,
:on_or_after => 1.day.ago :on_or_after => 1.day.ago
end
end end
end
before :each do before :each do
@person = DateOnOrBeforeAndAfter.new @person = DateOnOrBeforeAndAfter.new
end end
@ -254,12 +248,10 @@ describe ValidatesTimeliness::ValidationMethods do
describe "for time type" do describe "for time type" do
describe "with before and after restrictions" do describe "with before and after restrictions" do
before :all do class TimeBeforeAfter < Person
class TimeBeforeAfter < Person validates_time :birth_time,
validates_time :birth_time, :before => "23:00",
:before => "23:00", :after => "06:00"
:after => "06:00"
end
end end
before :each do before :each do
@ -302,12 +294,10 @@ describe ValidatesTimeliness::ValidationMethods do
end end
describe "with on_or_before and on_or_after restrictions" do describe "with on_or_before and on_or_after restrictions" do
before :all do class TimeOnOrBeforeAndAfter < Person
class TimeOnOrBeforeAndAfter < Person validates_time :birth_time,
validates_time :birth_time, :on_or_before => "23:00",
:on_or_before => "23:00", :on_or_after => "06:00"
:on_or_after => "06:00"
end
end end
before :each do before :each do
@ -339,16 +329,13 @@ describe ValidatesTimeliness::ValidationMethods do
end end
describe "with mixed value and restriction types" do describe "with mixed value and restriction types" do
before :all do class MixedBeforeAndAfter < Person
validates_datetime :birth_date_and_time,
class MixedBeforeAndAfter < Person :before => Date.new(2000,1,2),
validates_datetime :birth_date_and_time, :after => lambda { "2000-01-01" }
:before => Date.new(2000,1,2), validates_date :birth_date,
:after => lambda { "2000-01-01" } :on_or_before => lambda { "2000-01-01" },
validates_date :birth_date, :on_or_after => :birth_date_and_time
:on_or_before => lambda { "2000-01-01" },
:on_or_after => :birth_date_and_time
end
end end
before :each do before :each do
@ -384,11 +371,16 @@ describe ValidatesTimeliness::ValidationMethods do
end end
describe "ignoring restriction errors" do describe "ignoring restriction errors" do
class BadRestriction < Person
validates_date :birth_date, :before => Proc.new { raise }
end
before :all do before :all do
ValidatesTimeliness::Validator.ignore_datetime_restriction_errors = true ValidatesTimeliness.ignore_datetime_restriction_errors = true
class BadRestriction < Person end
validates_date :birth_date, :before => Proc.new { raise }
end after :all do
ValidatesTimeliness.ignore_datetime_restriction_errors = false
end end
before :each do before :each do
@ -402,17 +394,15 @@ describe ValidatesTimeliness::ValidationMethods do
end end
describe "restriction value error message" do describe "restriction value error message" do
describe "default formats" do class ValueFormats < Person
before :all do validates_datetime :birth_date_and_time, :allow_blank => true, :after => 1.day.from_now
class DefaultFormats < Person validates_date :birth_date, :allow_blank => true, :after => 1.day.from_now
validates_datetime :birth_date_and_time, :allow_blank => true, :after => 1.day.from_now validates_time :birth_time, :allow_blank => true, :after => '23:59:59'
validates_date :birth_date, :allow_blank => true, :after => 1.day.from_now end
validates_time :birth_time, :allow_blank => true, :after => '23:59:59'
end
end
describe "default formats" do
before :each do before :each do
@person = DefaultFormats.new @person = ValueFormats.new
end end
it "should format datetime value of restriction" do it "should format datetime value of restriction" do
@ -436,13 +426,7 @@ describe ValidatesTimeliness::ValidationMethods do
describe "custom formats" do describe "custom formats" do
before :all do before :all do
class CustomFormats < Person ValidatesTimeliness.date_time_error_value_formats = {
validates_datetime :birth_date_and_time, :allow_blank => true, :after => 1.day.from_now
validates_date :birth_date, :allow_blank => true, :after => 1.day.from_now
validates_time :birth_time, :allow_blank => true, :after => '23:59:59'
end
ValidatesTimeliness::Validator.date_time_error_value_formats = {
:time => '%H:%M %p', :time => '%H:%M %p',
:date => '%d-%m-%Y', :date => '%d-%m-%Y',
:datetime => '%d-%m-%Y %H:%M %p' :datetime => '%d-%m-%Y %H:%M %p'
@ -450,7 +434,7 @@ describe ValidatesTimeliness::ValidationMethods do
end end
before :each do before :each do
@person = CustomFormats.new @person = ValueFormats.new
end end
it "should format datetime value of restriction" do it "should format datetime value of restriction" do