require "active_support/core_ext/string/strip" module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] 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], options[:maximum] = range.min, range.max end if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil? options[:minimum] = 1 end if options[:tokenizer] ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. You can achieve the same functionality by defining an instance method with the value that you want to validate the length of. For example, validates_length_of :essay, minimum: 100, tokenizer: ->(str) { str.scan(/\w+/) } should be written as validates_length_of :words_in_essay, minimum: 100 private def words_in_essay essay.scan(/\w+/) end EOS end super end def check_validity! keys = CHECKS.keys & options.keys if keys.empty? raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.' end keys.each do |key| value = options[key] unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity" end end end def validate_each(record, attribute, value) value = tokenize(record, value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length errors_options = options.except(*RESERVED_OPTIONS) CHECKS.each do |key, validity_check| next unless check_value = options[key] if !value.nil? || skip_nil_check?(key) next if value_length.send(validity_check, check_value) end errors_options[:count] = check_value default_message = options[MESSAGES[key]] errors_options[:message] ||= default_message if default_message record.errors.add(attribute, MESSAGES[key], errors_options) end end private def tokenize(record, value) tokenizer = options[:tokenizer] if tokenizer && value.kind_of?(String) if tokenizer.kind_of?(Proc) tokenizer.call(value) elsif record.respond_to?(tokenizer) record.send(tokenizer, value) end end || value end def skip_nil_check?(key) key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil? end end module HelperMethods # Validates that the specified attributes match the length restrictions # supplied. Only one constraint option can be used at a time apart from # +:minimum+ and +:maximum+ that can be combined together: # # class Person < ActiveRecord::Base # validates_length_of :first_name, maximum: 30 # validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind" # validates_length_of :fax, in: 7..32, allow_nil: true # validates_length_of :phone, in: 7..32, allow_blank: true # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' # # private # # def words_in_essay # essay.scan(/\w+/) # end # end # # Constraint options: # # * :minimum - The minimum size of the attribute. # * :maximum - The maximum size of the attribute. Allows +nil+ by # default if not used with +:minimum+. # * :is - The exact size of the attribute. # * :within - A range specifying the minimum and maximum size of # the attribute. # * :in - A synonym (or alias) for :within. # # Other options: # # * :allow_nil - Attribute may be +nil+; skip validation. # * :allow_blank - Attribute may be blank; skip validation. # * :too_long - The error message if the attribute goes over the # maximum (default is: "is too long (maximum is %{count} characters)"). # * :too_short - The error message if the attribute goes under the # minimum (default is: "is too short (min is %{count} characters)"). # * :wrong_length - The error message if using the :is # method and the attribute is the wrong size (default is: "is the wrong # length (should be %{count} characters)"). # * :message - The error message to use for a :minimum, # :maximum, or :is violation. An alias of the appropriate # too_long/too_short/wrong_length message. # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. # See ActiveModel::Validation#validates for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end alias_method :validates_size_of, :validates_length_of end end end