lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb
module Shoulda # :nodoc: module Matchers module ActiveModel # :nodoc: # Ensure that the attribute's value is in the range specified # # Options: # * <tt>in_array</tt> - the array of allowed values for this attribute # * <tt>in_range</tt> - the range of allowed values for this attribute # * <tt>with_low_message</tt> - value the test expects to find in # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the # translation for :inclusion. # * <tt>with_high_message</tt> - value the test expects to find in # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the # translation for :inclusion. # # Example: # it { should ensure_inclusion_of(:age).in_range(0..100) } # def ensure_inclusion_of(attr) EnsureInclusionOfMatcher.new(attr) end class EnsureInclusionOfMatcher < ValidationMatcher # :nodoc: ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring' ARBITRARY_OUTSIDE_FIXNUM = 123456789 ARBITRARY_OUTSIDE_DECIMAL = 0.123456789 BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT You are using `ensure_inclusion_of` to assert that a boolean column allows boolean values and disallows non-boolean ones. Assuming you are using `validates_format_of` in your model, be aware that it is not possible to fully test this, and in fact the validation is superfluous, as boolean columns will automatically convert non-boolean values to boolean ones. Hence, you should consider removing this test and the corresponding validation. EOT BOOLEAN_ALLOWS_NIL_MESSAGE = <<EOT You are using `ensure_inclusion_of` to assert that a boolean column allows nil. Be aware that it is not possible to fully test this, as anything other than true, false or nil will be converted to false. Hence, you should consider removing this test and the corresponding validation. EOT BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE = <<EOT You have specified that your model's #{@attribute} should ensure inclusion of nil. However, #{@attribute} is a boolean column which does not allow null values. Hence, this test will fail and there is no way to make it pass. EOT def initialize(attribute) super(attribute) @options = {} end def in_array(array) @array = array self end def in_range(range) @range = range @minimum = range.first @maximum = range.max self end 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 def with_message(message) if message @low_message = message @high_message = message end self end def with_low_message(message) @low_message = message if message self end def with_high_message(message) @high_message = message if message self end def description "ensure inclusion of #{@attribute} in #{inspect_message}" end 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 = "#{@array} doesn't match array in validation" false end end end private 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 def inspect_message @range.nil? ? @array.inspect : @range.inspect end def allows_all_values_in_array? @array.all? do |value| allows_value_of(value, @low_message) end end def disallows_lower_value @minimum == 0 || disallows_value_of(@minimum - 1, @low_message) end def disallows_higher_value disallows_value_of(@maximum + 1, @high_message) end def allows_minimum_value allows_value_of(@minimum, @low_message) end def allows_maximum_value allows_value_of(@maximum, @high_message) end def disallows_value_outside_of_array? if attribute_column.type == :boolean case @array when [true, false] Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE return true when [nil] if attribute_column.null Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE return true else raise NonNullableBooleanError, BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE end end end !allows_value_of(*values_outside_of_array) end def values_outside_of_array if !(@array & outside_values).empty? raise CouldNotDetermineValueOutsideOfArray else outside_values end end def outside_values case attribute_column.type when :boolean boolean_outside_values when :integer, :float [ARBITRARY_OUTSIDE_FIXNUM] when :decimal [ARBITRARY_OUTSIDE_DECIMAL] else [ARBITRARY_OUTSIDE_STRING] end end def boolean_outside_values values = [] values << case @array when [true] then false when [false] then true else raise CouldNotDetermineValueOutsideOfArray end if attribute_column.null values << nil end values end def attribute_column @subject.class.columns_hash[@attribute.to_s] end end end end end