Tuesday, 21 May 2013

Backporting "in_array" to older versions of should'a "ensure_inclusion_of"

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

No comments: