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)
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 Matchers
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)
@expected, @options = attribute, options
@options.reverse_merge!(error_messages)
compile_error_messages
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)
@record = record
type = options[:type]
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'}
}
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]}/)
invalid_value = @@test_values[type][:fail]
valid_value = parse_and_cast(@@test_values[type][:pass])
valid = error_matching(invalid_value, /#{messages["invalid_#{type}".to_sym]}/) &&
no_error_matching(valid_value, /#{messages["invalid_#{type}".to_sym]}/)
valid = test_option(:before, :-) if options[:before] && valid
valid = test_option(:after, :+) if options[:after] && valid
@@ -45,7 +52,7 @@ module Spec
end
private
attr_reader :actual, :expected, :record, :options, :last_failure
attr_reader :actual, :expected, :record, :options, :messages, :last_failure
def test_option(option, modifier, settings={})
settings.reverse_merge!(:modify_on => :valid)
@@ -57,29 +64,23 @@ module Spec
[ boundary, boundary.send(modifier, 1) ]
end
message = options["#{option}_message".to_sym]
message = messages[option]
error_matching(invalid_value, /#{message}/) &&
no_error_matching(valid_value, /#{message}/)
end
def parse_and_cast(value)
def parse_and_cast(value)
value = ValidatesTimeliness::Validator.send(:restriction_value, value, record, options[:type])
cast_method = ValidatesTimeliness::Validator.send(:restriction_type_cast_method, options[:type])
value.send(cast_method) rescue nil
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)
record.send("#{expected}=", value)
record.valid?
errors = record.errors.on(expected)
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
end
@@ -91,7 +92,7 @@ module Spec
def format_value(value)
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
@@ -112,9 +113,10 @@ module Spec
private
def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options)
end
def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options)
end
end
end
end

View File

@@ -68,8 +68,6 @@ module ValidatesTimeliness
validates_each(attr_names, configuration) do |record, attr_name, value|
raw_value = record.send("#{attr_name}_before_type_cast")
validator.call(record, attr_name, raw_value)
errors = validator.errors
add_errors(record, attr_name, errors) unless errors.empty?
end
end
@@ -88,9 +86,6 @@ module ValidatesTimeliness
end
end
def add_errors(record, attr_name, errors)
errors.each {|e| record.errors.add(attr_name, e) }
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
# or times.
class Validator
attr_accessor :configuration, :errors
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"
}
attr_reader :configuration, :type, :messages
def initialize(configuration)
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
defaults.update(self.class.mapped_default_error_messages)
@configuration = defaults.merge(configuration)
@errors = []
@type = @configuration.delete(:type)
end
# The main validation method which can be used directly or called through
# the other specific type validation methods.
def call(record, attr_name, value)
@errors = []
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
unless time = record.class.parse_date_time(value, configuration[:type])
@errors << configuration["invalid_#{configuration[:type]}_message".to_sym]
return
end
validate_restrictions(record, attr_name, time)
rescue Exception => e
@errors << configuration["invalid_#{configuration[:type]}_message".to_sym]
time = record.class.parse_date_time(value, type)
unless time
add_error(record, attr_name, "invalid_#{type}".to_sym) and return
end
validate_restrictions(record, attr_name, time)
end
private
@@ -65,27 +34,42 @@ module ValidatesTimeliness
def validate_restrictions(record, attr_name, value)
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)
restriction_methods.each do |option, method|
next unless restriction = configuration[option]
begin
compare = self.class.restriction_value(restriction, record, configuration[:type])
compare = self.class.restriction_value(restriction, record, type)
next if compare.nil?
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
@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
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)
case restriction
when Time, Date, DateTime
@@ -106,12 +90,6 @@ module ValidatesTimeliness
when :datetime then :to_time
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