From f1085f41287687835659fa23079080204fe32e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Dec 2009 00:36:51 +0100 Subject: Move validations in ActiveModel to validators, however all validatity checks are still in the class method. --- activemodel/lib/active_model.rb | 5 +- .../lib/active_model/validations/acceptance.rb | 18 ++-- .../lib/active_model/validations/confirmation.rb | 20 ++-- .../lib/active_model/validations/exclusion.rb | 21 +++-- activemodel/lib/active_model/validations/format.rb | 28 +++--- .../lib/active_model/validations/inclusion.rb | 21 +++-- activemodel/lib/active_model/validations/length.rb | 91 +++++++++--------- .../lib/active_model/validations/numericality.rb | 105 +++++++++++---------- .../lib/active_model/validations/presence.rb | 15 +-- activemodel/lib/active_model/validations/with.rb | 5 +- activemodel/lib/active_model/validator.rb | 24 ++++- .../test/cases/validations/with_validation_test.rb | 4 +- 12 files changed, 200 insertions(+), 157 deletions(-) (limited to 'activemodel') diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index e0de27b96d..6aa80da23f 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -35,9 +35,9 @@ module ActiveModel autoload :Dirty autoload :Errors autoload :Lint - autoload :Name, 'active_model/naming' + autoload :Name, 'active_model/naming' autoload :Naming - autoload :Observer, 'active_model/observing' + autoload :Observer, 'active_model/observing' autoload :Observing autoload :Serialization autoload :StateMachine @@ -45,6 +45,7 @@ module ActiveModel autoload :Validations autoload :ValidationsRepairHelper autoload :Validator + autoload :EachValidator, 'active_model/validator' autoload :VERSION module Serializers diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index b65c9b933d..a5de58cd41 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -1,5 +1,13 @@ module ActiveModel module Validations + class AcceptanceValidator < EachValidator + def validate_each(record, attribute, value) + unless value == options[:accept] + record.errors.add(attribute, :accepted, :default => options[:message]) + end + end + end + module ClassMethods # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: # @@ -25,8 +33,8 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - configuration = { :allow_nil => true, :accept => "1" } - configuration.update(attr_names.extract_options!) + options = { :allow_nil => true, :accept => "1" } + options.update(attr_names.extract_options!) db_cols = begin column_names @@ -37,11 +45,7 @@ module ActiveModel names = attr_names.reject { |name| db_cols.include?(name.to_s) } attr_accessor(*names) - validates_each(attr_names,configuration) do |record, attr_name, value| - unless value == configuration[:accept] - record.errors.add(attr_name, :accepted, :default => configuration[:message]) - end - end + validates_with AcceptanceValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index d414224dd2..b06effdceb 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -1,5 +1,13 @@ module ActiveModel module Validations + class ConfirmationValidator < EachValidator + def validate_each(record, attribute, value) + confirmed = record.send(:"#{attribute}_confirmation") + return if confirmed.nil? || value == confirmed + record.errors.add(attribute, :confirmation, :default => options[:message]) + end + end + module ClassMethods # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: # @@ -30,15 +38,9 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - configuration = attr_names.extract_options! - - attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") - record.errors.add(attr_name, :confirmation, :default => configuration[:message]) - end - end + options = attr_names.extract_options! + attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" })) + validates_with ConfirmationValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 2cfdec97a5..209b1c76f9 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,5 +1,12 @@ module ActiveModel module Validations + class ExclusionValidator < EachValidator + def validate_each(record, attribute, value) + return unless options[:in].include?(value) + record.errors.add(attribute, :exclusion, :default => options[:message], :value => value) + end + end + module ClassMethods # Validates that the value of the specified attribute is not in a particular enumerable object. # @@ -21,17 +28,13 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - configuration = attr_names.extract_options! - - enum = configuration[:in] || configuration[:within] + options = attr_names.extract_options! + options[:in] ||= options.delete(:within) - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) + raise ArgumentError, "An object with the method include? is required must be supplied as the " << + ":in option of the configuration hash" unless options[:in].respond_to?(:include?) - validates_each(attr_names, configuration) do |record, attr_name, value| - if enum.include?(value) - record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) - end - end + validates_with ExclusionValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index c670dafc7c..d5427c2b03 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,5 +1,15 @@ module ActiveModel module Validations + class FormatValidator < EachValidator + def validate_each(record, attribute, value) + if options[:with] && value.to_s !~ options[:with] + record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + elsif options[:without] && value.to_s =~ options[:without] + record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + end + end + end + module ClassMethods # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided. # You can require that the attribute matches the regular expression: @@ -33,29 +43,21 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = attr_names.extract_options! + options = attr_names.extract_options! - unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or" + unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" raise ArgumentError, "Either :with or :without must be supplied (but not both)" end - if configuration[:with] && !configuration[:with].is_a?(Regexp) + if options[:with] && !options[:with].is_a?(Regexp) raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" end - if configuration[:without] && !configuration[:without].is_a?(Regexp) + if options[:without] && !options[:without].is_a?(Regexp) raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" end - if configuration[:with] - validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with] - end - elsif configuration[:without] - validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without] - end - end + validates_with FormatValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 0d7dc5cd64..d42c95357c 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,5 +1,12 @@ module ActiveModel module Validations + class InclusionValidator < EachValidator + def validate_each(record, attribute, value) + return if options[:in].include?(value) + record.errors.add(attribute, :inclusion, :default => options[:message], :value => value) + end + end + module ClassMethods # Validates whether the value of the specified attribute is available in a particular enumerable object. # @@ -21,17 +28,13 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - configuration = attr_names.extract_options! - - enum = configuration[:in] || configuration[:within] + options = attr_names.extract_options! + options[:in] ||= options.delete(:within) - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) + raise ArgumentError, "An object with the method include? is required must be supplied as the " << + ":in option of the configuration hash" unless options[:in].respond_to?(:include?) - validates_each(attr_names, configuration) do |record, attr_name, value| - unless enum.include?(value) - record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) - end - end + validates_with InclusionValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index e91841bd1c..66b2ae5b18 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,7 +1,44 @@ module ActiveModel module Validations + class LengthValidator < EachValidator + MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze + CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze + + attr_reader :type + + def initialize(options) + @type = options.delete(:type) + super + end + + def validate_each(record, attribute, value) + checks = options.slice(:minimum, :maximum, :is) + value = options[:tokenizer].call(value) if value.kind_of?(String) + + if [:within, :in].include?(type) + range = options[type] + checks[:minimum], checks[:maximum] = range.begin, range.end + checks[:maximum] -= 1 if range.exclude_end? + end + + checks.each do |key, check_value| + custom_message = options[:message] || options[MESSAGES[key]] + validity_check = CHECKS[key] + + valid_value = if key == :maximum + value.nil? || value.size.send(validity_check, check_value) + else + value && value.size.send(validity_check, check_value) + end + + record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) unless valid_value + end + end + end + module ClassMethods ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze + DEFAULT_TOKENIZER = lambda { |value| value.split(//) } # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # @@ -38,62 +75,32 @@ module ActiveModel # * :tokenizer - Specifies how to split up the attribute string. (e.g. :tokenizer => lambda {|str| str.scan(/\w+/)} to # count words as in above example.) # Defaults to lambda{ |value| value.split(//) } which counts individual characters. - def validates_length_of(*attrs) - # Merge given options with defaults. - options = { :tokenizer => lambda {|value| value.split(//)} } - options.update(attrs.extract_options!.symbolize_keys) + def validates_length_of(*attr_names) + options = { :tokenizer => DEFAULT_TOKENIZER } + options.update(attr_names.extract_options!) # Ensure that one and only one range option is specified. range_options = ALL_RANGE_OPTIONS & options.keys case range_options.size when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' when 1 # Valid number of options; do nothing. else - raise ArgumentError, 'Too many range options specified. Choose only one.' + raise ArgumentError, 'Too many range options specified. Choose only one.' end - # Get range option and value. - option = range_options.first - option_value = options[range_options.first] - key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option] - custom_message = options[:message] || options[key] + type = range_options.first + value = options[type] - case option + case type when :within, :in - raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - - validates_each(attrs, options) do |record, attr, value| - value = options[:tokenizer].call(value) if value.kind_of?(String) - - min, max = option_value.begin, option_value.end - max = max - 1 if option_value.exclude_end? - - if value.nil? || value.size < min - record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => min) - elsif value.size > max - record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => max) - end - end + raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range) when :is, :minimum, :maximum - raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - - # Declare different validations per option. - validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - - validates_each(attrs, options) do |record, attr, value| - value = options[:tokenizer].call(value) if value.kind_of?(String) - - valid_value = if option == :maximum - value.nil? || value.size.send(validity_checks[option], option_value) - else - value && value.size.send(validity_checks[option], option_value) - end - - record.errors.add(attr, key, :default => custom_message, :count => option_value) unless valid_value - end + raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0 end + + validates_with LengthValidator, options.merge(:attributes => attr_names, :type => type) end alias_method :validates_size_of, :validates_length_of diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 32dbcd82d0..8ffa395a2d 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,9 +1,55 @@ module ActiveModel module Validations + class NumericalityValidator < EachValidator + CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', + :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', + :odd => 'odd?', :even => 'even?' }.freeze + + def validate_each(record, attr_name, value) + before_type_cast = "#{attr_name}_before_type_cast" + + if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send("#{attr_name}_before_type_cast") || value + else + raw_value = value + end + + return if options[:allow_nil] and raw_value.nil? + + if options[:only_integer] + unless raw_value.to_s =~ /\A[+-]?\d+\Z/ + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message]) + return + end + raw_value = raw_value.to_i + else + begin + raw_value = Kernel.Float(raw_value) + rescue ArgumentError, TypeError + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message]) + return + end + end + + options.slice(*CHECKS.keys).each do |option, option_value| + case option + when :odd, :even + unless raw_value.to_i.method(CHECKS[option])[] + record.errors.add(attr_name, option, :value => raw_value, :default => options[:message]) + end + else + option_value = option_value.call(record) if option_value.is_a? Proc + option_value = record.method(option_value).call if option_value.is_a? Symbol + + unless raw_value.method(CHECKS[option])[option_value] + record.errors.add(attr_name, option, :default => options[:message], :value => raw_value, :count => option_value) + end + end + end + end + end module ClassMethods - ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', - :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', - :odd => 'odd?', :even => 'even?' }.freeze + # Validates whether the value of the specified attribute is numeric by trying to convert it to # a float with Kernel.Float (if only_integer is false) or applying it to the regular expression @@ -44,61 +90,18 @@ module ActiveModel # validates_numericality_of :width, :greater_than => :minimum_weight # end # - # - def validates_numericality_of(*attr_names) - configuration = { :only_integer => false, :allow_nil => false } - configuration.update(attr_names.extract_options!) + options = { :only_integer => false, :allow_nil => false } + options.update(attr_names.extract_options!) - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys + numericality_options = NumericalityValidator::CHECKS.keys & options.keys (numericality_options - [ :odd, :even ]).each do |option| - value = configuration[option] + value = options[option] raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) end - validates_each(attr_names,configuration) do |record, attr_name, value| - before_type_cast = "#{attr_name}_before_type_cast" - - if record.respond_to?(before_type_cast.to_sym) - raw_value = record.send("#{attr_name}_before_type_cast") || value - else - raw_value = value - end - - next if configuration[:allow_nil] and raw_value.nil? - - if configuration[:only_integer] - unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - next - end - raw_value = raw_value.to_i - else - begin - raw_value = Kernel.Float(raw_value) - rescue ArgumentError, TypeError - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - next - end - end - - numericality_options.each do |option| - case option - when :odd, :even - unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] - record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) - end - else - configuration[option] = configuration[option].call(record) if configuration[option].is_a? Proc - configuration[option] = record.method(configuration[option]).call if configuration[option].is_a? Symbol - - unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] - record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) - end - end - end - end + validates_with NumericalityValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 3ff677c137..a4c6f866a7 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -2,6 +2,12 @@ require 'active_support/core_ext/object/blank' module ActiveModel module Validations + class PresenceValidator < EachValidator + def validate(record) + record.errors.add_on_blank(attributes, options[:message]) + end + end + module ClassMethods # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: # @@ -28,13 +34,8 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - configuration = attr_names.extract_options! - - # can't use validates_each here, because it cannot cope with nonexistent attributes, - # while errors.add_on_empty can - validate configuration do |record| - record.errors.add_on_blank(attr_names, configuration[:message]) - end + options = attr_names.extract_options! + validates_with PresenceValidator, options.merge(:attributes => attr_names) end end end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 8313aded11..626e9d5731 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -50,10 +50,7 @@ module ActiveModel # def validates_with(*args) options = args.extract_options! - - args.each do |klass| - validate klass.new(options.except(:on, :if, :unless)), options - end + args.each { |klass| validate(klass.new(options), options) } end end end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 80a538fcb6..6885c6800e 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,5 +1,4 @@ module ActiveModel #:nodoc: - # A simple base class that can be used along with ActiveModel::Base.validates_with # # class Person < ActiveModel::Base @@ -61,7 +60,28 @@ module ActiveModel #:nodoc: end def validate(record) - raise "You must override this method" + raise NotImplementedError + end + end + + class EachValidator < Validator + attr_reader :attributes + + def initialize(options) + @attributes = options.delete(:attributes) + super + end + + def validate(record) + attributes.each do |attribute| + value = record.send(:read_attribute_for_validation, attribute) + next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) + validate_each(record, attribute, value) + end + end + + def validate_each(record) + raise NotImplementedError end end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 4350ad69ec..953ff8aa2d 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -98,10 +98,10 @@ class ValidatesWithTest < ActiveRecord::TestCase assert topic.errors[:base].include?(ERROR_MESSAGE) end - test "passes all non-standard configuration options to the validator class" do + test "passes all configuration options to the validator class" do topic = Topic.new validator = mock() - validator.expects(:new).with({:foo => :bar}).returns(validator) + validator.expects(:new).with(:foo => :bar, :if => "1 == 1").returns(validator) validator.expects(:validate).with(topic) Topic.validates_with(validator, :if => "1 == 1", :foo => :bar) -- cgit v1.2.3