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,7 +64,7 @@ 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
@ -68,18 +75,12 @@ module Spec
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
@ -115,6 +116,7 @@ module Spec
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
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)
rescue Exception => e
@errors << configuration["invalid_#{configuration[:type]}_message".to_sym]
end
end
private
@ -65,26 +34,41 @@ 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
@ -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

View File

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

View File

@ -1,14 +1,9 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "ValidateTimeliness matcher" do
attr_accessor :no_validation, :with_validation
class NoValidation < Person
end
before do
class NoValidation < Person
alias_attribute :birth_datetime, :birth_date_and_time
end
class WithValidation < Person
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'
@ -19,8 +14,20 @@ describe "ValidateTimeliness matcher" do
: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
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
attr_accessor :no_validation, :with_validation
before do
@no_validation = NoValidation.new
@with_validation = WithValidation.new
end
@ -134,14 +141,8 @@ describe "ValidateTimeliness matcher" do
end
describe "custom messages" 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
end

View File

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