module Shoulda # :nodoc:
module Matchers
module ActiveModel # :nodoc:
# Ensure that the attribute is numeric.
#
# Options:
# * with_message - value the test expects to find in
# errors.on(:attribute). Regexp or string. Defaults to the
# translation for :not_a_number.
# * only_integer - allows only integer values
# * odd - Specifies the value must be an odd number.
# * even - Specifies the value must be an even number.
# * allow_nil - allows nil values
#
# Examples:
# it { should validate_numericality_of(:price) }
# it { should validate_numericality_of(:age).only_integer }
# it { should validate_numericality_of(:frequency).odd }
# it { should validate_numericality_of(:frequency).even }
# it { should validate_numericality_of(:rank).is_less_than_or_equal_to(10).allow_nil }
#
def validate_numericality_of(attr)
ValidateNumericalityOfMatcher.new(attr)
end
class ValidateNumericalityOfMatcher
NUMERIC_NAME = 'numbers'
NON_NUMERIC_VALUE = 'abcd'
DEFAULT_DIFF_TO_COMPARE = 0.000_000_000_001
attr_reader :diff_to_compare
def initialize(attribute)
@attribute = attribute
@submatchers = []
@diff_to_compare = DEFAULT_DIFF_TO_COMPARE
add_disallow_value_matcher
end
def only_integer
prepare_submatcher(
NumericalityMatchers::OnlyIntegerMatcher.new(@attribute)
)
self
end
def allow_nil
prepare_submatcher(
AllowValueMatcher.new(nil)
.for(@attribute)
.with_message(:not_a_number)
)
self
end
def odd
prepare_submatcher(
NumericalityMatchers::OddNumberMatcher.new(@attribute)
)
self
end
def even
prepare_submatcher(
NumericalityMatchers::EvenNumberMatcher.new(@attribute)
)
self
end
def is_greater_than(value)
prepare_submatcher(comparison_matcher_for(value, :>).for(@attribute))
self
end
def is_greater_than_or_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :>=).for(@attribute))
self
end
def is_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :==).for(@attribute))
self
end
def is_less_than(value)
prepare_submatcher(comparison_matcher_for(value, :<).for(@attribute))
self
end
def is_less_than_or_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :<=).for(@attribute))
self
end
def with_message(message)
@submatchers.each { |matcher| matcher.with_message(message) }
self
end
def matches?(subject)
@subject = subject
submatchers_match?
end
def description
"only allow #{allowed_types} for #{@attribute}#{comparison_descriptions}"
end
def failure_message
submatcher_failure_messages_for_should.last
end
alias failure_message_for_should failure_message
def failure_message_when_negated
submatcher_failure_messages_for_should_not.last
end
alias failure_message_for_should_not failure_message_when_negated
private
def add_disallow_value_matcher
disallow_value_matcher = DisallowValueMatcher.new(NON_NUMERIC_VALUE).
for(@attribute).
with_message(:not_a_number)
add_submatcher(disallow_value_matcher)
end
def prepare_submatcher(submatcher)
add_submatcher(submatcher)
if submatcher.respond_to?(:diff_to_compare)
update_diff_to_compare(submatcher)
end
end
def comparison_matcher_for(value, operator)
NumericalityMatchers::ComparisonMatcher
.new(self, value, operator)
.for(@attribute)
end
def add_submatcher(submatcher)
@submatchers << submatcher
end
def update_diff_to_compare(matcher)
@diff_to_compare = [@diff_to_compare, matcher.diff_to_compare].max
end
def submatchers_match?
failing_submatchers.empty?
end
def submatcher_failure_messages_for_should
failing_submatchers.map(&:failure_message)
end
def submatcher_failure_messages_for_should_not
failing_submatchers.map(&:failure_message_when_negated)
end
def failing_submatchers
@failing_submatchers ||= @submatchers.select { |matcher| !matcher.matches?(@subject) }
end
def allowed_types
allowed_array = submatcher_allowed_types
allowed_array.empty? ? NUMERIC_NAME : allowed_array.join(', ')
end
def submatcher_allowed_types
@submatchers.inject([]){|m, s| m << s.allowed_type if s.respond_to?(:allowed_type); m }
end
def comparison_descriptions
description_array = submatcher_comparison_descriptions
description_array.empty? ? '' : ' which are ' + submatcher_comparison_descriptions.join(' and ')
end
def submatcher_comparison_descriptions
@submatchers.inject([]) do |arr, submatcher|
if submatcher.respond_to? :comparison_description
arr << submatcher.comparison_description
end
arr
end
end
end
end
end
end