diff --git a/NEWS.md b/NEWS.md
index 8f6f827e..27c1c486 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,6 @@
# HEAD
+* Add `:odd` and `:even` options to the `validate_numericality_of` matcher.
* Add `:touch` option to the association matcher.
* Ruby 2.0.0 is now officially supported.
* Fixes the issue where using %{attribute} or %{model} in I18n translations
diff --git a/lib/shoulda/matchers/active_model.rb b/lib/shoulda/matchers/active_model.rb
index 8745862a..2d950bf0 100644
--- a/lib/shoulda/matchers/active_model.rb
+++ b/lib/shoulda/matchers/active_model.rb
@@ -5,6 +5,7 @@ require 'shoulda/matchers/active_model/exception_message_finder'
require 'shoulda/matchers/active_model/allow_value_matcher'
require 'shoulda/matchers/active_model/disallow_value_matcher'
require 'shoulda/matchers/active_model/only_integer_matcher'
+require 'shoulda/matchers/active_model/odd_even_number_matcher'
require 'shoulda/matchers/active_model/ensure_length_of_matcher'
require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher'
require 'shoulda/matchers/active_model/ensure_exclusion_of_matcher'
diff --git a/lib/shoulda/matchers/active_model/odd_even_number_matcher.rb b/lib/shoulda/matchers/active_model/odd_even_number_matcher.rb
new file mode 100644
index 00000000..c84dcdea
--- /dev/null
+++ b/lib/shoulda/matchers/active_model/odd_even_number_matcher.rb
@@ -0,0 +1,43 @@
+module Shoulda # :nodoc:
+ module Matchers
+ module ActiveModel # :nodoc:
+ class OddEvenNumberMatcher # :nodoc:
+ NON_EVEN_NUMBER_VALUE = 1
+ NON_ODD_NUMBER_VALUE = 2
+
+ def initialize(attribute, options = {})
+ @attribute = attribute
+ options[:odd] ||= true
+ options[:even] ||= false
+
+ if options[:odd] && !options[:even]
+ @disallow_value_matcher = DisallowValueMatcher.new(NON_ODD_NUMBER_VALUE).
+ for(@attribute).
+ with_message(:odd)
+ else
+ @disallow_value_matcher = DisallowValueMatcher.new(NON_EVEN_NUMBER_VALUE).
+ for(@attribute).
+ with_message(:even)
+ end
+ end
+
+ def matches?(subject)
+ @disallow_value_matcher.matches?(subject)
+ end
+
+ def with_message(message)
+ @disallow_value_matcher.with_message(message)
+ self
+ end
+
+ def allowed_types
+ 'integer'
+ end
+
+ def failure_message_for_should
+ @disallow_value_matcher.failure_message_for_should
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb
index 8d2c249b..743eb648 100644
--- a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb
+++ b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb
@@ -8,10 +8,14 @@ module Shoulda # :nodoc:
# 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.
#
# 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 }
#
def validate_numericality_of(attr)
ValidateNumericalityOfMatcher.new(attr)
@@ -22,7 +26,6 @@ module Shoulda # :nodoc:
def initialize(attribute)
@attribute = attribute
- @options = {}
@submatchers = []
add_disallow_value_matcher
@@ -34,6 +37,18 @@ module Shoulda # :nodoc:
self
end
+ def odd
+ odd_number_matcher = OddEvenNumberMatcher.new(@attribute, :odd => true)
+ add_submatcher(odd_number_matcher)
+ self
+ end
+
+ def even
+ even_number_matcher = OddEvenNumberMatcher.new(@attribute, :even => true)
+ add_submatcher(even_number_matcher)
+ self
+ end
+
def with_message(message)
@submatchers.each { |matcher| matcher.with_message(message) }
self
diff --git a/spec/shoulda/matchers/active_model/odd_even_number_matcher_spec.rb b/spec/shoulda/matchers/active_model/odd_even_number_matcher_spec.rb
new file mode 100644
index 00000000..8c18cfbb
--- /dev/null
+++ b/spec/shoulda/matchers/active_model/odd_even_number_matcher_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Shoulda::Matchers::ActiveModel::OddEvenNumberMatcher do
+ context 'given an attribute that only allows odd number values' do
+ it 'matches' do
+ validating_odd_number.should new_odd_matcher
+ end
+
+ it 'returns itself when given a message' do
+ matcher = new_odd_matcher
+ matcher.with_message('some message').should == matcher
+ end
+ end
+
+ context 'given an attribute that only allows even number values' do
+ it 'matches' do
+ validating_even_number.should new_even_matcher
+ end
+
+ it 'returns itself when given a message' do
+ matcher = new_even_matcher
+ matcher.with_message('some message').should == matcher
+ end
+ end
+
+ context 'given an attribute that only allows odd number values with a custom validation message' do
+ it 'only accepts odd number values for that attribute with that message' do
+ validating_odd_number(:message => 'custom').should new_odd_matcher.with_message(/custom/)
+ end
+
+ it 'rejects odd number values for that attribute with another message' do
+ validating_odd_number(:message => 'custom').should_not new_odd_matcher.with_message(/wrong/)
+ end
+ end
+
+ context 'given an attribute that only allows even number values with a custom validation message' do
+ it 'only accepts even number values for that attribute with that message' do
+ validating_even_number(:message => 'custom').should new_even_matcher.with_message(/custom/)
+ end
+
+ it 'rejects even number values for that attribute with another message' do
+ validating_even_number(:message => 'custom').should_not new_even_matcher.with_message(/wrong/)
+ end
+ end
+
+ context 'when the model does not have an odd validation' do
+ it 'does not match' do
+ define_model(:example, :attr => :string).new.should_not new_odd_matcher
+ end
+
+ it 'fails with the ActiveRecord :odd message' do
+ matcher = new_odd_matcher
+
+ matcher.matches?(define_model(:example, :attr => :string).new)
+
+ matcher.failure_message_for_should.should include 'Expected errors to include "must be odd"'
+ end
+ end
+
+ context 'when the model does not have an even validation' do
+ it 'does not match' do
+ define_model(:example, :attr => :string).new.should_not new_even_matcher
+ end
+
+ it 'fails with the ActiveRecord :even message' do
+ matcher = new_even_matcher
+
+ matcher.matches?(define_model(:example, :attr => :string).new)
+
+ matcher.failure_message_for_should.should include 'Expected errors to include "must be even"'
+ end
+ end
+
+ def new_odd_matcher
+ described_class.new(:attr, :odd => true)
+ end
+
+ def new_even_matcher
+ described_class.new(:attr, :even => true)
+ end
+
+ def validating_odd_number(options = {})
+ define_model :example, :attr => :string do
+ validates_numericality_of :attr, { :odd => true }.merge(options)
+ end.new
+ end
+
+ def validating_even_number(options = {})
+ define_model :example, :attr => :string do
+ validates_numericality_of :attr, { :even => true }.merge(options)
+ end.new
+ end
+end
diff --git a/spec/shoulda/matchers/active_model/only_integer_matcher_spec.rb b/spec/shoulda/matchers/active_model/only_integer_matcher_spec.rb
index e0ac9177..dec23ed5 100644
--- a/spec/shoulda/matchers/active_model/only_integer_matcher_spec.rb
+++ b/spec/shoulda/matchers/active_model/only_integer_matcher_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Shoulda::Matchers::ActiveModel::OnlyIntegerMatcher do
context 'given an attribute that only allows integer values' do
it 'matches' do
- only_integer.should new_matcher
+ validating_only_integer.should new_matcher
end
it 'allows integer types' do
@@ -18,11 +18,11 @@ describe Shoulda::Matchers::ActiveModel::OnlyIntegerMatcher do
context 'given an attribute that only allows integer values with a custom validation message' do
it 'only accepts integer values for that attribute with that message' do
- only_integer(:message => 'custom').should new_matcher.with_message(/custom/)
+ validating_only_integer(:message => 'custom').should new_matcher.with_message(/custom/)
end
it 'rejects integer values for that attribute with another message' do
- only_integer(:message => 'custom').should_not new_matcher.with_message(/wrong/)
+ validating_only_integer(:message => 'custom').should_not new_matcher.with_message(/wrong/)
end
end
@@ -44,7 +44,7 @@ describe Shoulda::Matchers::ActiveModel::OnlyIntegerMatcher do
described_class.new(:attr)
end
- def only_integer(options = {})
+ def validating_only_integer(options = {})
define_model :example, :attr => :string do
validates_numericality_of :attr, { :only_integer => true }.merge(options)
end.new
diff --git a/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb b/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb
index f9b9af65..a1da0412 100644
--- a/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb
+++ b/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb
@@ -29,6 +29,22 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do
the_matcher.failure_message_for_should.should include 'Expected errors to include "must be an integer"'
end
+
+ it 'rejects with the ActiveRecord :odd message' do
+ the_matcher = matcher.odd
+
+ the_matcher.matches?(define_model(:example, :attr => :string).new)
+
+ the_matcher.failure_message_for_should.should include 'Expected errors to include "must be odd"'
+ end
+
+ it 'rejects with the ActiveRecord :even message' do
+ the_matcher = matcher.even
+
+ the_matcher.matches?(define_model(:example, :attr => :string).new)
+
+ the_matcher.failure_message_for_should.should include 'Expected errors to include "must be even"'
+ end
end
context 'with the only_integer option' do
@@ -49,6 +65,42 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do
end
end
+ context 'with the odd option' do
+ it 'allows odd number values for that attribute' do
+ validating_numericality(:odd => true).should matcher.odd
+ end
+
+ it 'rejects when the model does not enforce odd number values' do
+ validating_numericality.should_not matcher.odd
+ end
+
+ it 'rejects with the ActiveRecord :odd message' do
+ the_matcher = matcher.odd
+
+ the_matcher.matches?(validating_numericality)
+
+ the_matcher.failure_message_for_should.should include 'Expected errors to include "must be odd"'
+ end
+ end
+
+ context 'with the even option' do
+ it 'allows even number values for that attribute' do
+ validating_numericality(:even => true).should matcher.even
+ end
+
+ it 'rejects when the model does not enforce even number values' do
+ validating_numericality.should_not matcher.even
+ end
+
+ it 'rejects with the ActiveRecord :even message' do
+ the_matcher = matcher.even
+
+ the_matcher.matches?(validating_numericality)
+
+ the_matcher.failure_message_for_should.should include 'Expected errors to include "must be even"'
+ end
+ end
+
context 'with a custom validation message' do
it 'accepts when the messages match' do
validating_numericality(:message => 'custom').