Rails now supports infinite range options in LengthValidators


Rails model validations have a host of options that allow developers to write complex rules for clean data entry. A common validation is to ensure that the length of a string is within a certain range. For example, a user’s name should be between 2 and 20 characters. This can be achieved using the length validator.

app/models/user.rb

  class User
    validates_length_of :first_name, in: 2..30
  end

For most part this gets the job done. When a user model is created with a name less than 2 characters or more than 30 characters, an appropriate error is thrown,

  irb(main):001:0> User.create! first_name: "A"
  /Users/swaathi/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/bundler/gems/rails-c41e2f1bca65/activerecord/lib/active_record/validations.rb:82:in `raise_validation_error': Validation failed: First name is too short (minimum is 2 characters) (ActiveRecord::RecordInvalid)

  irb(main):002:0> User.create! first_name: "A"*31
  /Users/swaathi/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/bundler/gems/rails-c41e2f1bca65/activerecord/lib/active_record/validations.rb:82:in `raise_validation_error': Validation failed: First name is too long (maximum is 30 characters) (ActiveRecord::RecordInvalid)

Before

However not all attributes require a minimum or maximum length. For example, a middle name can have a maximum length of 30 characters, but it can be empty. This is not possible with the current implementation of the length validator.

The length validation or a custom one would have to be written.

  class User
    validates :middle_name, length: { maximum: 30 }
    validate :middle_name_length

    private

    def middle_name_length
      if middle_name.present? && middle_name.length > 30
        errors.add(:middle_name, "is too long (maximum is 30 characters)")
      end
    end
  end

After

Thanks to this PR which introduces extensions to the Range class, the first or the last value of a range can be infinite. This allows us to write the following validations,

  class User
    validates_length_of :first_name, in: 2..
    validates_length_of :middle_name, in: ..30
  end

This allows validations to be written in a more concise way. The length validator now supports infinite ranges in the :in and :within options.

This change is possible due to the Float::INFINITY option which is a number that is always the largest in a comparison. This is how the code looks now,

activemodel/lib/active_model/validations/length.rb

  def initialize(options)
    if range = (options.delete(:in) || options.delete(:within))
      raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
      options[:minimum] = range.min if range.begin
      options[:maximum] = (range.exclude_end? ? range.end - 1 : range.end) if range.end
    end

    if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?

    ...

  end
    
  def check_validity!
    keys.each do |key|
      value = options[key]

      unless (value.is_a?(Integer) && value >= 0) ||
              value == Float::INFINITY || value == -Float::INFINITY ||
              value.is_a?(Symbol) || value.is_a?(Proc)
        raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
      end
    end

    ...

  end

Join Our Newsletter