module Remarkable
module ActiveRecord
module Matchers
class ValidateNumericalityOfMatcher < Remarkable::ActiveRecord::Base #:nodoc:
arguments :collection => :attributes, :as => :attribute
optional :equal_to, :greater_than_or_equal_to, :greater_than,
:less_than_or_equal_to, :less_than, :message
optional :only_integer, :odd, :even, :allow_nil, :allow_blank, :default => true
collection_assertions :only_numeric_values?, :allow_blank?, :allow_nil?,
:only_integer?, :only_odd?, :only_even?, :equals_to?,
:less_than_minimum?, :more_than_maximum?
# Before assertions, we rearrange the values.
#
# Notice that :less_than gives a maximum value while :more_than given
# a minimum value. While :equal_to generate both.
#
before_assert do
@maximum_values = {}
@minimum_values = {}
if value = @options[:equal_to]
@maximum_values[:equal_to] = value
@minimum_values[:equal_to] = value
elsif value = @options[:less_than]
@maximum_values[:less_than] = value - 1
elsif value = @options[:greater_than]
@minimum_values[:greater_than] = value + 1
elsif value = @options[:less_than_or_equal_to]
@maximum_values[:less_than_or_equal_to] = value
elsif value = @options[:greater_than_or_equal_to]
@minimum_values[:greater_than_or_equal_to] = value
end
end
private
def allow_nil?
super(default_message_for(:not_a_number))
end
def allow_blank?
super(default_message_for(:not_a_number))
end
def only_numeric_values?
bad?("abcd", default_message_for(:not_a_number))
end
def only_integer?
assert_bad_or_good_if_key(:only_integer, valid_value_for_test.to_f, default_message_for(:not_a_number))
end
# In ActiveRecord, when we supply :even, does not matter the value, it
# considers that should even values should be accepted.
#
def only_even?
return true unless @options[:even]
bad?(even_valid_value_for_test + 1, default_message_for(:even))
end
# In ActiveRecord, when we supply :odd, does not matter the value, it
# considers that should odd values should be accepted.
#
def only_odd?
return true unless @options[:odd]
bad?(even_valid_value_for_test, default_message_for(:odd))
end
# Check equal_to for all registered values.
#
def equals_to?
values = {}
@maximum_values.each { |k, v| values[k] = v }
@minimum_values.each { |k, v| values[k] = v }
values.each do |key, value|
return false, :count => value unless good?(value, default_message_for(key))
end
true
end
# Check more_than_maximum? for equal_to, less_than and
# less_than_or_equal_to options.
#
def more_than_maximum?
@maximum_values.each do |key, value|
return false, :count => value unless bad?(value + 1, default_message_for(key))
end
true
end
# Check less_than_minimum? for equal_to, more_than and
# more_than_or_equal_to options.
#
def less_than_minimum?
@minimum_values.each do |key, value|
return false, :count => value unless bad?(value - 1, default_message_for(key))
end
true
end
# Returns a valid value for test.
#
def valid_value_for_test
value = @options[:equal_to] || @options[:less_than_or_equal_to] || @options[:greater_than_or_equal_to]
value ||= @options[:less_than] - 1 if @options[:less_than]
value ||= @options[:greater_than] + 1 if @options[:greater_than]
value ||= 10
if @options[:even]
value = (value / 2) * 2
elsif @options[:odd]
value = ((value / 2) * 2) + 1
end
value
end
# Returns a valid even value for test.
# The method valid_value_for_test checks for :even option but does not
# return necessarily an even value
#
def even_valid_value_for_test
(valid_value_for_test / 2) * 2
end
# Returns the default message for each key (:odd, :even, :equal_to, ...).
# If the user provided a message, we use it, otherwise we should use
# the given key as message.
#
# For example, a default_message_for(:odd), if none is provided, will be
# :odd. So we have create :odd_message in the options hash, that when
# called later, will return :odd.
#
def default_message_for(key)
if @options[:message]
:message
else
@options[:"#{key}_message"] = key
:"#{key}_message"
end
end
end
# Ensures that the given attributes accepts only numbers.
#
# == Options
#
# * :only_integer - when supplied, checks if it accepts only integers or not
# * :odd - when supplied, checks if it accepts only odd values or not
# * :even - when supplied, checks if it accepts only even values or not
# * :equal_to - when supplied, checks if attributes are only valid when equal to given value
# * :less_than - when supplied, checks if attributes are only valid when less than given value
# * :greater_than - when supplied, checks if attributes are only valid when greater than given value
# * :less_than_or_equal_to - when supplied, checks if attributes are only valid when less than or equal to given value
# * :greater_than_or_equal_to - when supplied, checks if attributes are only valid when greater than or equal to given value
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp, string or symbol. Default = I18n.translate('activerecord.errors.messages.not_a_number')
#
# == Examples
#
# it { should validate_numericality_of(:age).odd }
# it { should validate_numericality_of(:age).even }
# it { should validate_numericality_of(:age).only_integer }
# it { should validate_numericality_of(:age, :odd => true) }
# it { should validate_numericality_of(:age, :even => true) }
#
# should_validate_numericality_of :age, :price
# should_validate_numericality_of :price, :only_integer => false, :greater_than => 10
#
# should_validate_numericality_of :price do |m|
# m.only_integer = false
# m.greater_than = 10
# end
#
def validate_numericality_of(*attributes, &block)
ValidateNumericalityOfMatcher.new(*attributes, &block).spec(self)
end
end
end
end