aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2010-01-04 03:24:39 +0530
committerPratik Naik <pratiknaik@gmail.com>2010-01-04 03:24:39 +0530
commitcda36a0731f14b33a920bf7e32255661e06f890a (patch)
tree79ccba37953f9fe3055503be42b1610faa6d64ad /activemodel/lib
parentbd4a3cce4ecd8e648179a91e26506e3622ac2162 (diff)
parenta115b5d79a850bb56cd3c9db9a05d6da35e3d7be (diff)
downloadrails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.gz
rails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.bz2
rails-cda36a0731f14b33a920bf7e32255661e06f890a.zip
Merge remote branch 'mainstream/master'
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb47
-rw-r--r--activemodel/lib/active_model/callbacks.rb91
-rw-r--r--activemodel/lib/active_model/errors.rb11
-rw-r--r--activemodel/lib/active_model/lint.rb14
-rw-r--r--activemodel/lib/active_model/locale/en.yml3
-rw-r--r--activemodel/lib/active_model/naming.rb26
-rw-r--r--activemodel/lib/active_model/railtie.rb2
-rw-r--r--activemodel/lib/active_model/translation.rb22
-rw-r--r--activemodel/lib/active_model/validations.rb57
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb21
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb20
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb26
-rw-r--r--activemodel/lib/active_model/validations/format.rb28
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb26
-rw-r--r--activemodel/lib/active_model/validations/length.rb129
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb122
-rw-r--r--activemodel/lib/active_model/validations/presence.rb15
-rw-r--r--activemodel/lib/active_model/validations/with.rb11
-rw-r--r--activemodel/lib/active_model/validations_repair_helper.rb35
-rw-r--r--activemodel/lib/active_model/validator.rb56
20 files changed, 456 insertions, 306 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 505e16c195..f14016027c 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -21,34 +21,41 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
-$:.unshift(activesupport_path) if File.directory?(activesupport_path)
+activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
+$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require 'active_support'
+
module ActiveModel
- autoload :AttributeMethods, 'active_model/attribute_methods'
- autoload :Conversion, 'active_model/conversion'
- autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
- autoload :Dirty, 'active_model/dirty'
- autoload :Errors, 'active_model/errors'
- autoload :Lint, 'active_model/lint'
+ extend ActiveSupport::Autoload
+
+ autoload :AttributeMethods
+ autoload :Callbacks
+ autoload :Conversion
+ autoload :DeprecatedErrorMethods
+ autoload :Dirty
+ autoload :Errors
+ autoload :Lint
autoload :Name, 'active_model/naming'
- autoload :Naming, 'active_model/naming'
+ autoload :Naming
autoload :Observer, 'active_model/observing'
- autoload :Observing, 'active_model/observing'
- autoload :Serialization, 'active_model/serialization'
- autoload :StateMachine, 'active_model/state_machine'
- autoload :TestCase, 'active_model/test_case'
- autoload :Translation, 'active_model/translation'
- autoload :Validations, 'active_model/validations'
- autoload :ValidationsRepairHelper, 'active_model/validations_repair_helper'
- autoload :Validator, 'active_model/validator'
- autoload :VERSION, 'active_model/version'
+ autoload :Observing
+ autoload :Serialization
+ autoload :StateMachine
+ autoload :Translation
+ autoload :Validations
+ autoload :Validator
+ autoload :EachValidator, 'active_model/validator'
+ autoload :BlockValidator, 'active_model/validator'
+ autoload :VERSION
module Serializers
- autoload :JSON, 'active_model/serializers/json'
- autoload :Xml, 'active_model/serializers/xml'
+ extend ActiveSupport::Autoload
+
+ autoload :JSON
+ autoload :Xml
end
end
+require 'active_support/i18n'
I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml'
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