diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2010-01-04 03:24:39 +0530 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2010-01-04 03:24:39 +0530 |
commit | cda36a0731f14b33a920bf7e32255661e06f890a (patch) | |
tree | 79ccba37953f9fe3055503be42b1610faa6d64ad /activemodel/lib/active_model | |
parent | bd4a3cce4ecd8e648179a91e26506e3622ac2162 (diff) | |
parent | a115b5d79a850bb56cd3c9db9a05d6da35e3d7be (diff) | |
download | rails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.gz rails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.bz2 rails-cda36a0731f14b33a920bf7e32255661e06f890a.zip |
Merge remote branch 'mainstream/master'
Diffstat (limited to 'activemodel/lib/active_model')
19 files changed, 429 insertions, 286 deletions
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb new file mode 100644 index 0000000000..f66a1ddcaa --- /dev/null +++ b/activemodel/lib/active_model/callbacks.rb @@ -0,0 +1,91 @@ +require 'active_support/callbacks' + +module ActiveModel + module Callbacks + def self.extended(base) + base.class_eval do + include ActiveSupport::Callbacks + end + end + + # Define callbacks similar to ActiveRecord ones. It means: + # + # * The callback chain is aborted whenever the block given to + # _run_callbacks returns false. + # + # * If a class is given to the fallback, it will search for + # before_create, around_create and after_create methods. + # + # == Usage + # + # First you need to define which callbacks your model will have: + # + # class MyModel + # define_model_callbacks :create + # end + # + # This will define three class methods: before_create, around_create, + # and after_create. They accept a symbol, a string, an object or a block. + # + # After you create a callback, you need to tell when they are executed. + # For example, you could do: + # + # def create + # _run_create_callbacks do + # super + # end + # end + # + # == Options + # + # define_model_callbacks accepts all options define_callbacks does, in + # case you want to overwrite a default. Besides that, it also accepts + # an :only option, where you can choose if you want all types (before, + # around or after) or just some: + # + # define_model_callbacks :initializer, :only => :after + # + def define_model_callbacks(*callbacks) + options = callbacks.extract_options! + options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options) + + types = Array(options.delete(:only)) + types = [:before, :around, :after] if types.empty? + + callbacks.each do |callback| + define_callbacks(callback, options) + + types.each do |type| + send(:"_define_#{type}_model_callback", self, callback) + end + end + end + + def _define_before_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.before_#{callback}(*args, &block) + set_callback(:#{callback}, :before, *args, &block) + end + CALLBACK + end + + def _define_around_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.around_#{callback}(*args, &block) + set_callback(:#{callback}, :around, *args, &block) + end + CALLBACK + end + + def _define_after_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.after_#{callback}(*args, &block) + options = args.extract_options! + options[:prepend] = true + options[:if] = Array(options[:if]) << "!halted && value != false" + set_callback(:#{callback}, :after, *(args << options), &block) + end + CALLBACK + end + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index e8bb62953d..abc084a74b 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -97,18 +97,19 @@ module ActiveModel full_messages = [] each do |attribute, messages| - messages = Array.wrap(messages) + messages = Array(messages) next if messages.empty? if attribute == :base messages.each {|m| full_messages << m } else - attr_name = @base.class.human_attribute_name(attribute) - options = { :default => ' ', :scope => @base.class.i18n_scope } - prefix = attr_name + I18n.t(:"errors.format.separator", options) + attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + options = { :default => "{{attribute}} {{message}}", :attribute => attr_name, + :scope => @base.class.i18n_scope } messages.each do |m| - full_messages << "#{prefix}#{m}" + full_messages << I18n.t(:"errors.format", options.merge(:message => m)) end end end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 1c2347adbf..0be82aa180 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -41,6 +41,20 @@ module ActiveModel assert_boolean model.destroyed?, "destroyed?" end + # naming + # ------ + # + # Model.model_name must returns a string with some convenience methods as + # :human and :partial_path. Check ActiveModel::Naming for more information. + # + def test_model_naming + assert model.class.respond_to?(:model_name), "The model should respond to model_name" + model_name = model.class.model_name + assert_kind_of String, model_name + assert_kind_of String, model_name.human + assert_kind_of String, model_name.partial_path + end + # errors # ------ # diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 0c2cf9ea33..1cdb897f13 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -1,6 +1,9 @@ en: activemodel: errors: + # model.errors.full_messages format. + format: "{{attribute}} {{message}}" + # The values :model, :attribute and :value are always available for interpolation # The value :count is available when applicable. Can be used for pluralization. messages: diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 675d62b9a6..4cd68a0c89 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -2,11 +2,11 @@ require 'active_support/inflector' module ActiveModel class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, :human + attr_reader :singular, :plural, :element, :collection, :partial_path alias_method :cache_key, :collection - def initialize(klass, name) - super(name) + def initialize(klass) + super(klass.name) @klass = klass @singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze @plural = ActiveSupport::Inflector.pluralize(@singular).freeze @@ -15,13 +15,31 @@ module ActiveModel @collection = ActiveSupport::Inflector.tableize(self).freeze @partial_path = "#{@collection}/#{@element}".freeze end + + # Transform the model name into a more humane format, using I18n. By default, + # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post"). + # Specify +options+ with additional translating options. + def human(options={}) + return @human unless @klass.respond_to?(:lookup_ancestors) && + @klass.respond_to?(:i18n_scope) + + defaults = @klass.lookup_ancestors.map do |klass| + klass.model_name.underscore.to_sym + end + + defaults << options.delete(:default) if options[:default] + defaults << @human + + options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults + I18n.translate(defaults.shift, options) + end end module Naming # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information. def model_name - @_model_name ||= ActiveModel::Name.new(self, name) + @_model_name ||= ActiveModel::Name.new(self) end end end diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb new file mode 100644 index 0000000000..63ffe5db63 --- /dev/null +++ b/activemodel/lib/active_model/railtie.rb @@ -0,0 +1,2 @@ +require "active_model" +require "rails"
\ No newline at end of file diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 42ca463f82..e5ef1e6114 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -37,28 +37,8 @@ module ActiveModel # Model.human_name is deprecated. Use Model.model_name.human instead. def human_name(*args) - ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,1]) + ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5]) model_name.human(*args) end end - - class Name < String - # Transform the model name into a more humane format, using I18n. By default, - # it will underscore then humanize the class name (BlogPost.human_name #=> "Blog post"). - # Specify +options+ with additional translating options. - def human(options={}) - return @human unless @klass.respond_to?(:lookup_ancestors) && - @klass.respond_to?(:i18n_scope) - - defaults = @klass.lookup_ancestors.map do |klass| - klass.model_name.underscore.to_sym - end - - defaults << options.delete(:default) if options[:default] - defaults << @human - - options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults - I18n.translate(defaults.shift, options) - end - end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 064ec98f3a..d5460a58bd 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/keys' +require 'active_model/errors' module ActiveModel module Validations @@ -12,6 +13,29 @@ module ActiveModel end module ClassMethods + # Validates each attribute against a block. + # + # class Person < ActiveRecord::Base + # validates_each :first_name, :last_name do |record, attr, value| + # record.errors.add attr, 'starts with z.' if value[0] == ?z + # end + # end + # + # Options: + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. + def validates_each(*attr_names, &block) + options = attr_names.extract_options!.symbolize_keys + validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block + end + # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldly and # you're looking for more descriptive declaration of your validations. @@ -39,39 +63,6 @@ module ActiveModel # end # # This usage applies to +validate_on_create+ and +validate_on_update as well+. - - # Validates each attribute against a block. - # - # class Person < ActiveRecord::Base - # validates_each :first_name, :last_name do |record, attr, value| - # record.errors.add attr, 'starts with z.' if value[0] == ?z - # end - # end - # - # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_each(*attrs) - options = attrs.extract_options!.symbolize_keys - attrs = attrs.flatten - - # Declare the validation. - validate options do |record| - attrs.each do |attr| - value = record.send(:read_attribute_for_validation, attr) - next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) - yield record, attr, value - end - end - end - def validate(*args, &block) options = args.last if options.is_a?(Hash) && options.key?(:on) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index b65c9b933d..bd9463ed27 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -1,5 +1,17 @@ module ActiveModel module Validations + class AcceptanceValidator < EachValidator + def initialize(options) + super(options.reverse_merge(:allow_nil => true, :accept => "1")) + end + + 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 +37,7 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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 = attr_names.extract_options! db_cols = begin column_names @@ -37,11 +48,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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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..f8759f253b 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,5 +1,17 @@ module ActiveModel module Validations + class ExclusionValidator < EachValidator + def check_validity! + 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?) + end + + 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 +33,9 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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] - - 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?) - - 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 + options = attr_names.extract_options! + options[:in] ||= options.delete(:within) + 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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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..a122e9e737 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,5 +1,17 @@ module ActiveModel module Validations + class InclusionValidator < EachValidator + def check_validity! + 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?) + end + + 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 +33,9 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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] - - 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?) - - 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 + options = attr_names.extract_options! + options[:in] ||= options.delete(:within) + 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 6227239c9b..5db2060fe5 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,7 +1,75 @@ module ActiveModel module Validations + class LengthValidator < EachValidator + OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze + MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze + CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze + + DEFAULT_TOKENIZER = lambda { |value| value.split(//) } + attr_reader :type + + def initialize(options) + @type = (OPTIONS & options.keys).first + super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER)) + end + + def check_validity! + ensure_one_range_option! + ensure_argument_types! + 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 + + protected + + def ensure_one_range_option! #:nodoc: + range_options = OPTIONS & options.keys + + case range_options.size + when 0 + 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.' + end + end + + def ensure_argument_types! #:nodoc: + value = options[type] + + case type + when :within, :in + raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range) + when :is, :minimum, :maximum + raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0 + end + end + end + module ClassMethods - ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # @@ -38,62 +106,9 @@ module ActiveModel # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> 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) - - # 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.' - when 1 - # Valid number of options; do nothing. - else - 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] - - case option - 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 - 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 - end + def validates_length_of(*attr_names) + options = attr_names.extract_options! + validates_with LengthValidator, options.merge(:attributes => attr_names) 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..f2aab8c5b8 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,10 +1,68 @@ module ActiveModel module Validations - 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 + 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 initialize(options) + super(options.reverse_merge(:only_integer => false, :allow_nil => false)) + end + + def check_validity! + options.slice(*CHECKS.keys) do |option, value| + next if [:odd, :even].include?(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 + end + + def validate_each(record, attr_name, value) + before_type_cast = "#{attr_name}_before_type_cast" + + raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym) + raw_value ||= value + + return if options[:allow_nil] && raw_value.nil? + + unless value = parse_raw_value(raw_value, options) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message]) + return + end + + options.slice(*CHECKS.keys).each do |option, option_value| + case option + when :odd, :even + unless value.to_i.send(CHECKS[option]) + record.errors.add(attr_name, option, :value => value, :default => options[:message]) + end + else + option_value = option_value.call(record) if option_value.is_a?(Proc) + option_value = record.send(option_value) if option_value.is_a?(Symbol) + + unless value.send(CHECKS[option], option_value) + record.errors.add(attr_name, option, :default => options[:message], :value => value, :count => option_value) + end + end + end + end + + protected + + def parse_raw_value(raw_value, options) + if options[:only_integer] + raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ + else + begin + Kernel.Float(raw_value) + rescue ArgumentError, TypeError + nil + end + end + end + end + + module ClassMethods # Validates whether the value of the specified attribute is numeric by trying to convert it to # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). @@ -44,61 +102,9 @@ 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!) - - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys - - (numericality_options - [ :odd, :even ]).each do |option| - value = configuration[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 + options = attr_names.extract_options! + 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 edc2133ddc..8d521173c6 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -48,14 +48,9 @@ module ActiveModel # end # end # - def validates_with(*args) - configuration = args.extract_options! - - validate configuration do |record| - args.each do |klass| - klass.new(record, configuration.except(:on, :if, :unless)).validate - end - end + def validates_with(*args, &block) + options = args.extract_options! + args.each { |klass| validate(klass.new(options, &block), options) } end end end diff --git a/activemodel/lib/active_model/validations_repair_helper.rb b/activemodel/lib/active_model/validations_repair_helper.rb deleted file mode 100644 index 40741e6dbe..0000000000 --- a/activemodel/lib/active_model/validations_repair_helper.rb +++ /dev/null @@ -1,35 +0,0 @@ -module ActiveModel - module ValidationsRepairHelper - extend ActiveSupport::Concern - - module ClassMethods - def repair_validations(*model_classes) - setup do - @_stored_callbacks = {} - model_classes.each do |k| - @_stored_callbacks[k] = k._validate_callbacks.dup - end - end - teardown do - model_classes.each do |k| - k._validate_callbacks = @_stored_callbacks[k] - k.__update_callbacks(:validate) - end - end - end - end - - def repair_validations(*model_classes, &block) - @__stored_callbacks = {} - model_classes.each do |k| - @__stored_callbacks[k] = k._validate_callbacks.dup - end - return block.call - ensure - model_classes.each do |k| - k._validate_callbacks = @__stored_callbacks[k] - k.__update_callbacks(:validate) - end - end - end -end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 09de72b757..01695cb73a 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 @@ -52,17 +51,60 @@ module ActiveModel #:nodoc: # @my_custom_field = options[:field_name] || :first_name # end # end - # class Validator - attr_reader :record, :options + attr_reader :options - def initialize(record, options) - @record = record + def initialize(options) @options = options end - def validate - raise "You must override this method" + def validate(record) + raise NotImplementedError + end + end + + # EachValidator is a validator which iterates through the attributes given + # in the options hash invoking the validate_each method passing in the + # record, attribute and value. + # + # All ActiveModel validations are built on top of this Validator. + class EachValidator < Validator + attr_reader :attributes + + def initialize(options) + @attributes = Array(options.delete(:attributes)) + raise ":attributes cannot be blank" if @attributes.empty? + super + check_validity! + 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, attribute, value) + raise NotImplementedError + end + + def check_validity! + end + end + + # BlockValidator is a special EachValidator which receives a block on initialization + # and call this block for each attribute being validated. +validates_each+ uses this + # Validator. + class BlockValidator < EachValidator + def initialize(options, &block) + @block = block + super + end + + def validate_each(record, attribute, value) + @block.call(record, attribute, value) end end end |