Here's another shoulda backport I added recently. If you're still stuck using a legacy Rails system, this backport will let you use "in_array" in the "ensures_inclusion_of" Matcher.
Save it into something like: config/initializers/shoulda_monkeypatches.rb, then use it like this:
should ensure_inclusion_of(:widget_status).in_array(Widget::VALID_STATUSES).allow_blank.with_message(:is_invalid).use_integer_test_value
# backport the "in_array" method for the ensure_inclusion_of matcher
# While we're at it, add allow_blank and allow_nil too
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
module Matchers
class EnsureInclusionOfMatcher
ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring'
ARBITRARY_OUTSIDE_INT = -999999999
# to initialize the options
def initialize(attribute)
super(attribute)
@options = {}
end
# add the method we want to allow us to pass in arrays instead of
# just ranges
def in_array(array)
@array = array
self
end
# might as well also add the allow_blank and allow_nil methods too
def allow_blank(allow_blank = true)
@options[:allow_blank] = allow_blank
self
end
def allow_nil(allow_nil = true)
@options[:allow_nil] = allow_nil
self
end
# This is a method of my own addition to point out that the
# test-value must be an Int, not a String... because a string can
# evaluate to 0 which is a valid Int... which will make the test
# pass where it shouldn't :P
def use_integer_test_value(only_integer = true)
@options[:use_integer_test_value] = only_integer
self
end
# override description so it doesn't just try to inspect the range
def description
"ensure inclusion of #{@attribute} in #{inspect_message}"
end
# override the matches method to allow arrays as well as ranges
def matches?(subject)
super(subject)
if @range
@low_message ||= :inclusion
@high_message ||= :inclusion
disallows_lower_value &&
allows_minimum_value &&
disallows_higher_value &&
allows_maximum_value
elsif @array
if allows_all_values_in_array? && allows_blank_value? && allows_nil_value? && disallows_value_outside_of_array?
true
else
@failure_message_for_should = "#{@array} doesn't match array in validation"
false
end
end
end
private
# provide the message-inspect method to use either array or range
def inspect_message
@range.nil? ? @array.inspect : @range.inspect
end
# array helper methods
def allows_all_values_in_array?
@array.all? do |value|
allows_value_of(value, @low_message)
end
end
def disallows_value_outside_of_array?
disallows_value_of(value_outside_of_array)
end
def value_outside_of_array
test_val = @options[:use_integer_test_value] ? ARBITRARY_OUTSIDE_INT : ARBITRARY_OUTSIDE_STRING
if @array.include?(test_val)
raise CouldNotDetermineValueOutsideOfArray
else
test_val
end
end
# blank and nil helper methods
def allows_blank_value?
if @options.key?(:allow_blank)
blank_values = ['', ' ', "\n", "\r", "\t", "\f"]
@options[:allow_blank] == blank_values.all? { |value| allows_value_of(value) }
else
true
end
end
def allows_nil_value?
if @options.key?(:allow_nil)
@options[:allow_nil] == allows_value_of(nil)
else
true
end
end
end
end
end
end