aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG2
-rw-r--r--activemodel/lib/active_model.rb7
-rw-r--r--activemodel/lib/active_model/errors.rb5
-rw-r--r--activemodel/lib/active_model/locale/en.yml49
-rw-r--r--activemodel/lib/active_model/serializers/json.rb8
-rw-r--r--activemodel/lib/active_model/test_case.rb2
-rw-r--r--activemodel/lib/active_model/translation.rb5
-rw-r--r--activemodel/lib/active_model/validations.rb65
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb20
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb8
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/format.rb30
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/length.rb69
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb10
-rw-r--r--activemodel/lib/active_model/validations/presence.rb3
-rw-r--r--activemodel/lib/active_model/validations/validates.rb106
-rw-r--r--activemodel/lib/active_model/validations/with.rb47
-rw-r--r--activemodel/lib/active_model/validator.rb67
-rw-r--r--activemodel/test/cases/helper.rb1
-rw-r--r--activemodel/test/cases/tests_database.rb2
-rw-r--r--activemodel/test/cases/translation_test.rb5
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb36
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb90
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb8
-rw-r--r--activemodel/test/cases/validations/validates_test.rb114
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb22
-rw-r--r--activemodel/test/models/custom_reader.rb2
-rw-r--r--activemodel/test/models/person.rb6
-rw-r--r--activemodel/test/models/person_with_validator.rb11
-rw-r--r--activemodel/test/validators/email_validator.rb6
32 files changed, 548 insertions, 272 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG
index 26500568ee..7489c0daa5 100644
--- a/activemodel/CHANGELOG
+++ b/activemodel/CHANGELOG
@@ -1,5 +1,7 @@
*Edge*
+* Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 [DHH]
+
* Add validates_format_of :without => /regexp/ option. #430 [Elliot Winkler, Peer Allan]
Example :
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index f14016027c..6eab00c177 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -30,10 +30,12 @@ module ActiveModel
extend ActiveSupport::Autoload
autoload :AttributeMethods
+ autoload :BlockValidator, 'active_model/validator'
autoload :Callbacks
autoload :Conversion
autoload :DeprecatedErrorMethods
autoload :Dirty
+ autoload :EachValidator, 'active_model/validator'
autoload :Errors
autoload :Lint
autoload :Name, 'active_model/naming'
@@ -42,12 +44,11 @@ module ActiveModel
autoload :Observing
autoload :Serialization
autoload :StateMachine
+ autoload :TestCase
autoload :Translation
+ autoload :VERSION
autoload :Validations
autoload :Validator
- autoload :EachValidator, 'active_model/validator'
- autoload :BlockValidator, 'active_model/validator'
- autoload :VERSION
module Serializers
extend ActiveSupport::Autoload
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index abc084a74b..0b6c75c46e 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -105,8 +105,7 @@ module ActiveModel
else
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 }
+ options = { :default => "{{attribute}} {{message}}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
@@ -153,7 +152,7 @@ module ActiveModel
:model => @base.class.model_name.human,
:attribute => @base.class.human_attribute_name(attribute),
:value => value,
- :scope => [@base.class.i18n_scope, :errors]
+ :scope => [:errors]
}.merge(options)
I18n.translate(key, options)
diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml
index 1cdb897f13..ea58021767 100644
--- a/activemodel/lib/active_model/locale/en.yml
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -1,27 +1,26 @@
en:
- activemodel:
- errors:
- # model.errors.full_messages format.
- format: "{{attribute}} {{message}}"
+ errors:
+ # The default format use in full error messages.
+ 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:
- inclusion: "is not included in the list"
- exclusion: "is reserved"
- invalid: "is invalid"
- confirmation: "doesn't match confirmation"
- accepted: "must be accepted"
- empty: "can't be empty"
- blank: "can't be blank"
- too_long: "is too long (maximum is {{count}} characters)"
- too_short: "is too short (minimum is {{count}} characters)"
- wrong_length: "is the wrong length (should be {{count}} characters)"
- not_a_number: "is not a number"
- greater_than: "must be greater than {{count}}"
- greater_than_or_equal_to: "must be greater than or equal to {{count}}"
- equal_to: "must be equal to {{count}}"
- less_than: "must be less than {{count}}"
- less_than_or_equal_to: "must be less than or equal to {{count}}"
- odd: "must be odd"
- even: "must be even"
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match confirmation"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ too_long: "is too long (maximum is {{count}} characters)"
+ too_short: "is too short (minimum is {{count}} characters)"
+ wrong_length: "is the wrong length (should be {{count}} characters)"
+ not_a_number: "is not a number"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index ee6d48bfc6..794de7dc55 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -10,19 +10,17 @@ module ActiveModel
included do
extend ActiveModel::Naming
- cattr_accessor :include_root_in_json, :instance_writer => false
+ cattr_accessor :include_root_in_json, :instance_writer => true
end
# Returns a JSON string representing the model. Some configuration is
# available through +options+.
#
- # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
- # top-level behavior of to_json. In a new Rails application, it is set to
- # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
+ # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the
+ # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>,
# to_json will emit a single root node named after the object's type. For example:
#
# konata = User.find(1)
- # ActiveRecord::Base.include_root_in_json = true
# konata.to_json
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true} }
diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb
index 4cb5c9cbc0..6328807ad7 100644
--- a/activemodel/lib/active_model/test_case.rb
+++ b/activemodel/lib/active_model/test_case.rb
@@ -1,5 +1,3 @@
-require "active_support/test_case"
-
module ActiveModel #:nodoc:
class TestCase < ActiveSupport::TestCase #:nodoc:
def with_kcode(kcode)
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index e5ef1e6114..2d2df269d0 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -25,13 +25,14 @@ module ActiveModel
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
defaults = lookup_ancestors.map do |klass|
- :"#{klass.model_name.underscore}.#{attribute}"
+ :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}"
end
+ defaults << :"attributes.#{attribute}"
defaults << options.delete(:default) if options[:default]
defaults << attribute.to_s.humanize
- options.reverse_merge! :scope => [self.i18n_scope, :attributes], :count => 1, :default => defaults
+ options.reverse_merge! :count => 1, :default => defaults
I18n.translate(defaults.shift, options)
end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index d5460a58bd..276472ea46 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -15,21 +15,26 @@ module ActiveModel
module ClassMethods
# Validates each attribute against a block.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Validations
+ #
# 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>: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
+ # 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
+ # 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
@@ -42,7 +47,9 @@ module ActiveModel
#
# This can be done with a symbol pointing to a method:
#
- # class Comment < ActiveRecord::Base
+ # class Comment
+ # include ActiveModel::Validations
+ #
# validate :must_be_friends
#
# def must_be_friends
@@ -52,7 +59,9 @@ module ActiveModel
#
# Or with a block which is passed the current record to be validated:
#
- # class Comment < ActiveRecord::Base
+ # class Comment
+ # include ActiveModel::Validations
+ #
# validate do |comment|
# comment.must_be_friends
# end
@@ -71,6 +80,13 @@ module ActiveModel
end
set_callback(:validate, *args, &block)
end
+
+ private
+
+ def _merge_attributes(attr_names)
+ options = attr_names.extract_options!
+ options.merge(:attributes => attr_names)
+ end
end
# Returns the Errors object that holds all information about attribute error messages.
@@ -90,27 +106,22 @@ module ActiveModel
!valid?
end
- protected
- # Hook method defining how an attribute value should be retieved. By default this is assumed
- # to be an instance named after the attribute. Override this method in subclasses should you
- # need to retrieve the value for a given attribute differently e.g.
- # class MyClass
- # include ActiveModel::Validations
- #
- # def initialize(data = {})
- # @data = data
- # end
- #
- # private
- #
- # def read_attribute_for_validation(key)
- # @data[key]
- # end
- # end
- #
- def read_attribute_for_validation(key)
- send(key)
- end
+ # Hook method defining how an attribute value should be retieved. By default this is assumed
+ # to be an instance named after the attribute. Override this method in subclasses should you
+ # need to retrieve the value for a given attribute differently e.g.
+ # class MyClass
+ # include ActiveModel::Validations
+ #
+ # def initialize(data = {})
+ # @data = data
+ # end
+ #
+ # def read_attribute_for_validation(key)
+ # @data[key]
+ # end
+ # end
+ #
+ alias :read_attribute_for_validation :send
end
end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index bd9463ed27..0423fcd17f 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -10,6 +10,13 @@ module ActiveModel
record.errors.add(attribute, :accepted, :default => options[:message])
end
end
+
+ def setup(klass)
+ # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility
+ # as instance_methods returns symbols unlike 1.8 which returns strings.
+ new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") }
+ klass.send(:attr_accessor, *new_attributes)
+ end
end
module ClassMethods
@@ -37,18 +44,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)
- options = attr_names.extract_options!
-
- db_cols = begin
- column_names
- rescue Exception # To ignore both statement and connection errors
- []
- end
-
- names = attr_names.reject { |name| db_cols.include?(name.to_s) }
- attr_accessor(*names)
-
- validates_with AcceptanceValidator, options.merge(:attributes => attr_names)
+ validates_with AcceptanceValidator, _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 b06effdceb..8041d4b61f 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -6,6 +6,10 @@ module ActiveModel
return if confirmed.nil? || value == confirmed
record.errors.add(attribute, :confirmation, :default => options[:message])
end
+
+ def setup(klass)
+ klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" })
+ end
end
module ClassMethods
@@ -38,9 +42,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_confirmation_of(*attr_names)
- options = attr_names.extract_options!
- attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" }))
- validates_with ConfirmationValidator, options.merge(:attributes => attr_names)
+ validates_with ConfirmationValidator, _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 f8759f253b..7ee718cf3c 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -33,9 +33,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_exclusion_of(*attr_names)
- options = attr_names.extract_options!
- options[:in] ||= options.delete(:within)
- validates_with ExclusionValidator, options.merge(:attributes => attr_names)
+ validates_with ExclusionValidator, _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 d5427c2b03..9a9e7eca4d 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -8,6 +8,20 @@ module ActiveModel
record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
end
end
+
+ def check_validity!
+ 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 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 options[:without] && !options[:without].is_a?(Regexp)
+ raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
+ end
+ end
end
module ClassMethods
@@ -43,21 +57,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_format_of(*attr_names)
- options = attr_names.extract_options!
-
- 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 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 options[:without] && !options[:without].is_a?(Regexp)
- raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
- end
-
- validates_with FormatValidator, options.merge(:attributes => attr_names)
+ validates_with FormatValidator, _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 a122e9e737..0c1334fe1b 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -33,9 +33,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_inclusion_of(*attr_names)
- options = attr_names.extract_options!
- options[:in] ||= options.delete(:within)
- validates_with InclusionValidator, options.merge(:attributes => attr_names)
+ validates_with InclusionValidator, _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 5db2060fe5..9ceb75487f 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,36 +1,43 @@
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
+ if range = (options.delete(:in) || options.delete(:within))
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
+ options[:minimum], options[:maximum] = range.begin, range.end
+ options[:maximum] -= 1 if range.exclude_end?
+ end
+
super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER))
end
def check_validity!
- ensure_one_range_option!
- ensure_argument_types!
+ keys = CHECKS.keys & options.keys
+
+ if keys.empty?
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
+ end
+
+ keys.each do |key|
+ value = options[key]
+
+ unless value.is_a?(Integer) && value >= 0
+ raise ArgumentError, ":#{key} must be a nonnegative Integer"
+ end
+ end
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
+ value = options[:tokenizer].call(value) if value.kind_of?(String)
- checks.each do |key, check_value|
+ CHECKS.each do |key, validity_check|
+ next unless check_value = options[key]
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)
@@ -38,33 +45,8 @@ module ActiveModel
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
+ next if valid_value
+ record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value)
end
end
end
@@ -107,8 +89,7 @@ module ActiveModel
# count words as in above example.)
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
def validates_length_of(*attr_names)
- options = attr_names.extract_options!
- validates_with LengthValidator, options.merge(:attributes => attr_names)
+ validates_with LengthValidator, _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 f2aab8c5b8..c6d84c5312 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -10,9 +10,10 @@ module ActiveModel
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)
+ keys = CHECKS.keys - [:odd, :even]
+ options.slice(*keys).each do |option, value|
+ next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
end
end
@@ -103,8 +104,7 @@ module ActiveModel
# end
#
def validates_numericality_of(*attr_names)
- options = attr_names.extract_options!
- validates_with NumericalityValidator, options.merge(:attributes => attr_names)
+ validates_with NumericalityValidator, _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 a4c6f866a7..4a71cf79b5 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -34,8 +34,7 @@ module ActiveModel
# The method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
- options = attr_names.extract_options!
- validates_with PresenceValidator, options.merge(:attributes => attr_names)
+ validates_with PresenceValidator, _merge_attributes(attr_names)
end
end
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
new file mode 100644
index 0000000000..4c82a993ae
--- /dev/null
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -0,0 +1,106 @@
+module ActiveModel
+ module Validations
+ module ClassMethods
+ # This method is a shortcut to all default validators and any custom
+ # validator classes ending in 'Validator'. Note that Rails default
+ # validators can be overridden inside specific classes by creating
+ # custom validator classes in their place such as PresenceValidator.
+ #
+ # Examples of using the default rails validators:
+ #
+ # validates :terms, :acceptance => true
+ # validates :password, :confirmation => true
+ # validates :username, :exclusion => { :in => %w(admin superuser) }
+ # validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create }
+ # validates :age, :inclusion => { :in => 0..9 }
+ # validates :first_name, :length => { :maximum => 30 }
+ # validates :age, :numericality => true
+ # validates :username, :presence => true
+ # validates :username, :uniqueness => true
+ #
+ # The power of the +validates+ method comes when using cusom validators
+ # and default validators in one call for a given attribute e.g.
+ #
+ # class EmailValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # record.errors[attribute] << (options[:message] || "is not an email") unless
+ # value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
+ # end
+ # end
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # attr_accessor :name, :email
+ #
+ # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
+ # validates :email, :presence => true, :email => true
+ # end
+ #
+ # Validator classes my also exist within the class being validated
+ # allowing custom modules of validators to be included as needed e.g.
+ #
+ # class Film
+ # include ActiveModel::Validations
+ #
+ # class TitleValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i
+ # end
+ # end
+ #
+ # validates :name, :title => true
+ # end
+ #
+ # The validators hash can also handle regular expressions, ranges and arrays:
+ #
+ # validates :email, :format => /@/
+ # validates :gender, :inclusion => %w(male female)
+ # validates :password, :length => 6..20
+ #
+ # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given
+ # to one specific validator:
+ #
+ # validates :password, :presence => { :if => :password_required? }, :confirmation => true
+ #
+ # Or to all at the same time:
+ #
+ # validates :password, :presence => true, :confirmation => true, :if => :password_required?
+ #
+ def validates(*attributes)
+ defaults = attributes.extract_options!
+ validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil)
+
+ raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
+ raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) }
+ raise ArgumentError, "You need to supply at least one validation" if validations.empty?
+
+ defaults.merge!(:attributes => attributes)
+
+ validations.each do |key, options|
+ begin
+ validator = const_get("#{key.to_s.camelize}Validator")
+ rescue NameError
+ raise ArgumentError, "Unknown validator: '#{key}'"
+ end
+
+ validates_with(validator, defaults.merge(_parse_validates_options(options)))
+ end
+ end
+
+ protected
+
+ def _parse_validates_options(options) #:nodoc:
+ case options
+ when TrueClass
+ {}
+ when Hash
+ options
+ when Regexp
+ { :with => options }
+ when Range, Array
+ { :in => options }
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index 8d521173c6..db563876af 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -2,14 +2,16 @@ module ActiveModel
module Validations
module ClassMethods
- # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions.
+ # Passes the record off to the class or classes specified and allows them
+ # to add errors based on more complex conditions.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Validations
# validates_with MyValidator
# end
#
- # class MyValidator < ActiveRecord::Validator
- # def validate
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
# if some_complex_logic
# record.errors[:base] << "This record is invalid"
# end
@@ -23,37 +25,46 @@ module ActiveModel
#
# You may also pass it multiple classes, like so:
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Validations
# validates_with MyValidator, MyOtherValidator, :on => :create
# end
#
# Configuration options:
- # * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <tt>:update</tt>
- # * <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>).
+ # * <tt>on</tt> - Specifies when this validation is active
+ # (<tt>:create</tt> or <tt>:update</tt>
+ # * <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>).
+ # * <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.
#
- # If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>:
+ # If you pass any additional configuration options, they will be passed
+ # to the class and available as <tt>options</tt>:
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Validations
# validates_with MyValidator, :my_custom_key => "my custom value"
# end
#
- # class MyValidator < ActiveRecord::Validator
- # def validate
+ # class MyValidator < ActiveModel::Validator
+ # def validate(record)
# options[:my_custom_key] # => "my custom value"
# end
# end
#
def validates_with(*args, &block)
options = args.extract_options!
- args.each { |klass| validate(klass.new(options, &block), options) }
+ args.each do |klass|
+ validator = klass.new(options, &block)
+ validator.setup(self) if validator.respond_to?(:setup)
+ validate(validator, options)
+ end
end
end
end
-end
-
-
+end \ No newline at end of file
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 01695cb73a..382a4cc98d 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -1,12 +1,13 @@
module ActiveModel #:nodoc:
- # A simple base class that can be used along with ActiveModel::Base.validates_with
+ # A simple base class that can be used along with ActiveModel::Validations::ClassMethods.validates_with
#
- # class Person < ActiveModel::Base
+ # class Person
+ # include ActiveModel::Validations
# validates_with MyValidator
# end
#
# class MyValidator < ActiveModel::Validator
- # def validate
+ # def validate(record)
# if some_complex_logic
# record.errors[:base] = "This record is invalid"
# end
@@ -18,10 +19,11 @@ module ActiveModel #:nodoc:
# end
# end
#
- # Any class that inherits from ActiveModel::Validator will have access to <tt>record</tt>,
- # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>.
+ # Any class that inherits from ActiveModel::Validator must implement a method
+ # called <tt>validate</tt> which accepts a <tt>record</tt>.
#
- # class Person < ActiveModel::Base
+ # class Person
+ # include ActiveModel::Validations
# validates_with MyValidator
# end
#
@@ -36,7 +38,7 @@ module ActiveModel #:nodoc:
# from within the validators message
#
# class MyValidator < ActiveModel::Validator
- # def validate
+ # def validate(record)
# record.errors[:base] << "This is some custom error message"
# record.errors[:first_name] << "This is some complex validation"
# # etc...
@@ -51,13 +53,47 @@ module ActiveModel #:nodoc:
# @my_custom_field = options[:field_name] || :first_name
# end
# end
+ #
+ # The easiest way to add custom validators for validating individual attributes
+ # is with the convenient ActiveModel::EachValidator for example:
+ #
+ # class TitleValidator < ActiveModel::EachValidator
+ # def validate_each(record, attribute, value)
+ # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
+ # end
+ # end
+ #
+ # This can now be used in combination with the +validates+ method
+ # (see ActiveModel::Validations::ClassMethods.validates for more on this)
+ #
+ # class Person
+ # include ActiveModel::Validations
+ # attr_accessor :title
+ #
+ # validates :title, :presence => true, :title => true
+ # end
+ #
+ # Validator may also define a +setup+ instance method which will get called
+ # with the class that using that validator as it's argument. This can be
+ # useful when there are prerequisites such as an attr_accessor being present
+ # for example:
+ #
+ # class MyValidator < ActiveModel::Validator
+ # def setup(klass)
+ # klass.send :attr_accessor, :custom_attribute
+ # end
+ # end
+ #
class Validator
attr_reader :options
+ # Accepts options that will be made availible through the +options+ reader.
def initialize(options)
@options = options
end
+ # Override this method in subclasses with validation logic, adding errors
+ # to the records +errors+ array where necessary.
def validate(record)
raise NotImplementedError
end
@@ -70,7 +106,10 @@ module ActiveModel #:nodoc:
# All ActiveModel validations are built on top of this Validator.
class EachValidator < Validator
attr_reader :attributes
-
+
+ # Returns a new validator instance. All options will be available via the
+ # +options+ reader, however the <tt>:attributes</tt> option will be removed
+ # and instead be made available through the +attributes+ reader.
def initialize(options)
@attributes = Array(options.delete(:attributes))
raise ":attributes cannot be blank" if @attributes.empty?
@@ -78,18 +117,26 @@ module ActiveModel #:nodoc:
check_validity!
end
+ # Performs validation on the supplied record. By default this will call
+ # +validates_each+ to determine validity therefore subclasses should
+ # override +validates_each+ with validation logic.
def validate(record)
attributes.each do |attribute|
- value = record.send(:read_attribute_for_validation, attribute)
+ value = record.read_attribute_for_validation(attribute)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
validate_each(record, attribute, value)
end
end
+ # Override this method in subclasses with the validation logic, adding
+ # errors to the records +errors+ array where necessary.
def validate_each(record, attribute, value)
raise NotImplementedError
end
+ # Hook method that gets called by the initializer allowing verification
+ # that the arguments supplied are valid. You could for example raise an
+ # ArgumentError when invalid options are supplied.
def check_validity!
end
end
@@ -103,6 +150,8 @@ module ActiveModel #:nodoc:
super
end
+ private
+
def validate_each(record, attribute, value)
@block.call(record, attribute, value)
end
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 30193956ea..917bb720d0 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -8,7 +8,6 @@ $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
require 'config'
require 'active_model'
-require 'active_model/test_case'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
diff --git a/activemodel/test/cases/tests_database.rb b/activemodel/test/cases/tests_database.rb
index 79668dd941..8ca54d2678 100644
--- a/activemodel/test/cases/tests_database.rb
+++ b/activemodel/test/cases/tests_database.rb
@@ -2,8 +2,6 @@ require 'logger'
$:.unshift(File.dirname(__FILE__) + '/../../../activerecord/lib')
require 'active_record'
-require 'active_record/test_case'
-require 'active_record/fixtures'
module ActiveModel
module TestsDatabase
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index bfc1ca12e6..e25d308ca1 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -11,6 +11,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase
I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
assert_equal 'person name attribute', Person.human_attribute_name('name')
end
+
+ def test_translated_model_attributes_with_default
+ I18n.backend.store_translations 'en', :attributes => { :name => 'name default attribute' }
+ assert_equal 'name default attribute', Person.human_attribute_name('name')
+ end
def test_translated_model_attributes_with_symbols
I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index 54b2405c92..6116ef71d4 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -7,42 +7,6 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
def setup
Person.reset_callbacks(:validate)
@person = Person.new
-
- @old_load_path, @old_backend = I18n.load_path, I18n.backend
- I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
-
- I18n.backend.store_translations :'en', {
- :activemodel => {
- :errors => {
- :messages => {
- :inclusion => "is not included in the list",
- :exclusion => "is reserved",
- :invalid => "is invalid",
- :confirmation => "doesn't match confirmation",
- :accepted => "must be accepted",
- :empty => "can't be empty",
- :blank => "can't be blank",
- :too_long => "is too long (maximum is {{count}} characters)",
- :too_short => "is too short (minimum is {{count}} characters)",
- :wrong_length => "is the wrong length (should be {{count}} characters)",
- :not_a_number => "is not a number",
- :greater_than => "must be greater than {{count}}",
- :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
- :equal_to => "must be equal to {{count}}",
- :less_than => "must be less than {{count}}",
- :less_than_or_equal_to => "must be less than or equal to {{count}}",
- :odd => "must be odd",
- :even => "must be even"
- }
- }
- }
- }
- end
-
- def teardown
- I18n.load_path.replace @old_load_path
- I18n.backend = @old_backend
end
# validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index a7656fe219..7d33fcea98 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -9,10 +9,10 @@ class I18nValidationTest < ActiveModel::TestCase
Person.reset_callbacks(:validate)
@person = Person.new
- @old_load_path, @old_backend = I18n.load_path, I18n.backend
+ @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', :activemodel => {:errors => {:messages => {:custom => nil}}})
+ I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}})
end
def teardown
@@ -42,13 +42,13 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
- @person.errors.add('name', 'empty')
- I18n.expects(:translate).with(:"person.name", :default => ['Name', 'Name'], :scope => [:activemodel, :attributes], :count => 1).returns('Name')
- @person.errors.full_messages
+ @person.errors.add(:name, 'not found')
+ Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person's name")
+ assert_equal ["Person's name not found"], @person.errors.full_messages
end
def test_errors_full_messages_uses_format
- I18n.backend.store_translations('en', :activemodel => {:errors => {:format => "Field {{attribute}} {{message}}"}})
+ I18n.backend.store_translations('en', :errors => {:format => "Field {{attribute}} {{message}}"})
@person.errors.add('name', 'empty')
assert_equal ["Field Name empty"], @person.errors.full_messages
end
@@ -254,8 +254,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_confirmation_of w/o mocha
def test_validates_confirmation_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}}
Person.validates_confirmation_of :title
@person.title_confirmation = 'foo'
@@ -264,7 +264,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_confirmation_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}}
Person.validates_confirmation_of :title
@person.title_confirmation = 'foo'
@@ -275,8 +275,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_acceptance_of w/o mocha
def test_validates_acceptance_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}}
Person.validates_acceptance_of :title, :allow_nil => false
@person.valid?
@@ -284,7 +284,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_acceptance_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}}
Person.validates_acceptance_of :title, :allow_nil => false
@person.valid?
@@ -294,8 +294,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_presence_of w/o mocha
def test_validates_presence_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}}
Person.validates_presence_of :title
@person.valid?
@@ -303,7 +303,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_presence_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}}
Person.validates_presence_of :title
@person.valid?
@@ -313,8 +313,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_length_of :within w/o mocha
def test_validates_length_of_within_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}}
Person.validates_length_of :title, :within => 3..5
@person.valid?
@@ -322,7 +322,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_within_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}}
Person.validates_length_of :title, :within => 3..5
@person.valid?
@@ -332,8 +332,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_length_of :is w/o mocha
def test_validates_length_of_is_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}}
Person.validates_length_of :title, :is => 5
@person.valid?
@@ -341,7 +341,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_is_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}}
Person.validates_length_of :title, :is => 5
@person.valid?
@@ -351,8 +351,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_format_of w/o mocha
def test_validates_format_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}}
Person.validates_format_of :title, :with => /^[1-9][0-9]*$/
@person.valid?
@@ -360,7 +360,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}}
Person.validates_format_of :title, :with => /^[1-9][0-9]*$/
@person.valid?
@@ -370,8 +370,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_inclusion_of w/o mocha
def test_validates_inclusion_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}}
Person.validates_inclusion_of :title, :in => %w(a b c)
@person.valid?
@@ -379,7 +379,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}}
Person.validates_inclusion_of :title, :in => %w(a b c)
@person.valid?
@@ -389,8 +389,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_exclusion_of w/o mocha
def test_validates_exclusion_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}}
Person.validates_exclusion_of :title, :in => %w(a b c)
@person.title = 'a'
@@ -399,7 +399,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_exclusion_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}}
Person.validates_exclusion_of :title, :in => %w(a b c)
@person.title = 'a'
@@ -410,8 +410,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_numericality_of without :only_integer w/o mocha
def test_validates_numericality_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}}
Person.validates_numericality_of :title
@person.title = 'a'
@@ -420,7 +420,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true
@person.title = 'a'
@@ -431,8 +431,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_numericality_of with :only_integer w/o mocha
def test_validates_numericality_of_only_integer_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true
@person.title = 'a'
@@ -441,7 +441,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_only_integer_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true
@person.title = 'a'
@@ -452,8 +452,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_numericality_of :odd w/o mocha
def test_validates_numericality_of_odd_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true, :odd => true
@person.title = 0
@@ -462,7 +462,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_odd_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true, :odd => true
@person.title = 0
@@ -473,8 +473,8 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_numericality_of :less_than w/o mocha
def test_validates_numericality_of_less_than_finds_custom_model_key_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true, :less_than => 0
@person.title = 1
@@ -483,7 +483,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_less_than_finds_global_default_translation
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}}
Person.validates_numericality_of :title, :only_integer => true, :less_than => 0
@person.title = 1
@@ -494,7 +494,7 @@ class I18nValidationTest < ActiveModel::TestCase
# test with validates_with
def test_validations_with_message_symbol_must_translate
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:custom_error => "I am a custom error"}}}
+ I18n.backend.store_translations 'en', :errors => {:messages => {:custom_error => "I am a custom error"}}
Person.validates_presence_of :title, :message => :custom_error
@person.title = nil
@person.valid?
@@ -502,7 +502,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_with_message_symbol_must_translate_per_attribute
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}
Person.validates_presence_of :title, :message => :custom_error
@person.title = nil
@person.valid?
@@ -510,7 +510,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_validates_with_message_symbol_must_translate_per_model
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:custom_error => "I am a custom error"}}}}
+ I18n.backend.store_translations 'en', :errors => {:models => {:person => {:custom_error => "I am a custom error"}}}
Person.validates_presence_of :title, :message => :custom_error
@person.title = nil
@person.valid?
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index f3ef5e648a..99d0268b67 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -221,10 +221,8 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_nasty_params
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>-6) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6) }
assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") }
assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") }
assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") }
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 75cd654f98..38b3f87e93 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -154,6 +154,14 @@ class NumericalityValidationTest < ActiveModel::TestCase
Person.reset_callbacks(:validate)
end
+ def test_validates_numericality_with_invalid_args
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :greater_than_or_equal_to => "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :less_than_or_equal_to => "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :greater_than => "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :less_than => "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :equal_to => "foo" }
+ end
+
private
def invalid!(values, error = nil)
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
new file mode 100644
index 0000000000..d15fb4a524
--- /dev/null
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -0,0 +1,114 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/person'
+require 'models/person_with_validator'
+require 'validators/email_validator'
+
+class ValidatesTest < ActiveModel::TestCase
+ setup :reset_callbacks
+ teardown :reset_callbacks
+
+ def reset_callbacks
+ Person.reset_callbacks(:validate)
+ PersonWithValidator.reset_callbacks(:validate)
+ end
+
+ def test_validates_with_built_in_validation
+ Person.validates :title, :numericality => true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not a number'], person.errors[:title]
+ end
+
+ def test_validates_with_built_in_validation_and_options
+ Person.validates :salary, :numericality => { :message => 'my custom message' }
+ person = Person.new
+ person.valid?
+ assert_equal ['my custom message'], person.errors[:salary]
+ end
+
+ def test_validates_with_validator_class
+ Person.validates :karma, :email => true
+ person = Person.new
+ person.valid?
+ assert_equal ['is not an email'], person.errors[:karma]
+ end
+
+ def test_validates_with_if_as_local_conditions
+ Person.validates :karma, :presence => true, :email => { :unless => :condition_is_true }
+ person = Person.new
+ person.valid?
+ assert_equal ["can't be blank"], person.errors[:karma]
+ end
+
+ def test_validates_with_if_as_shared_conditions
+ Person.validates :karma, :presence => true, :email => true, :if => :condition_is_true
+ person = Person.new
+ person.valid?
+ assert ["can't be blank", "is not an email"], person.errors[:karma].sort
+ end
+
+ def test_validates_with_unless_shared_conditions
+ Person.validates :karma, :presence => true, :email => true, :unless => :condition_is_true
+ person = Person.new
+ assert person.valid?
+ end
+
+ def test_validates_with_allow_nil_shared_conditions
+ Person.validates :karma, :length => { :minimum => 20 }, :email => true, :allow_nil => true
+ person = Person.new
+ assert person.valid?
+ end
+
+ def test_validates_with_regexp
+ Person.validates :karma, :format => /positive|negative/
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is invalid'], person.errors[:karma]
+ person.karma = "positive"
+ assert person.valid?
+ end
+
+ def test_validates_with_array
+ Person.validates :gender, :inclusion => %w(m f)
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is not included in the list'], person.errors[:gender]
+ person.gender = "m"
+ assert person.valid?
+ end
+
+ def test_validates_with_range
+ Person.validates :karma, :length => 6..20
+ person = Person.new
+ assert person.invalid?
+ assert_equal ['is too short (minimum is 6 characters)'], person.errors[:karma]
+ person.karma = 'something'
+ assert person.valid?
+ end
+
+ def test_validates_with_validator_class_and_options
+ Person.validates :karma, :email => { :message => 'my custom message' }
+ person = Person.new
+ person.valid?
+ assert_equal ['my custom message'], person.errors[:karma]
+ end
+
+ def test_validates_with_unknown_validator
+ assert_raise(ArgumentError) { Person.validates :karma, :unknown => true }
+ end
+
+ def test_validates_with_included_validator
+ PersonWithValidator.validates :title, :presence => true
+ person = PersonWithValidator.new
+ person.valid?
+ assert_equal ['Local validator'], person.errors[:title]
+ end
+
+ def test_validates_with_included_validator_and_options
+ PersonWithValidator.validates :title, :presence => { :custom => ' please' }
+ person = PersonWithValidator.new
+ person.valid?
+ assert_equal ['Local validator please'], person.errors[:title]
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 7540ccb580..66b072ea38 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -120,6 +120,28 @@ class ValidatesWithTest < ActiveRecord::TestCase
Topic.validates_with(validator, :if => "1 == 1", :foo => :bar)
assert topic.valid?
end
+
+ test "calls setup method of validator passing in self when validator has setup method" do
+ topic = Topic.new
+ validator = stub_everything
+ validator.stubs(:new).returns(validator)
+ validator.stubs(:validate)
+ validator.stubs(:respond_to?).with(:setup).returns(true)
+ validator.expects(:setup).with(Topic).once
+ Topic.validates_with(validator)
+ assert topic.valid?
+ end
+
+ test "doesn't call setup method of validator when validator has no setup method" do
+ topic = Topic.new
+ validator = stub_everything
+ validator.stubs(:new).returns(validator)
+ validator.stubs(:validate)
+ validator.stubs(:respond_to?).with(:setup).returns(false)
+ validator.expects(:setup).with(Topic).never
+ Topic.validates_with(validator)
+ assert topic.valid?
+ end
test "validates_with with options" do
Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name)
diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb
index 7ac70e6167..14a8be9ebc 100644
--- a/activemodel/test/models/custom_reader.rb
+++ b/activemodel/test/models/custom_reader.rb
@@ -8,8 +8,6 @@ class CustomReader
def []=(key, value)
@data[key] = value
end
-
- private
def read_attribute_for_validation(key)
@data[key]
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
index c83d768379..cf16a38618 100644
--- a/activemodel/test/models/person.rb
+++ b/activemodel/test/models/person.rb
@@ -2,7 +2,11 @@ class Person
include ActiveModel::Validations
extend ActiveModel::Translation
- attr_accessor :title, :karma, :salary
+ attr_accessor :title, :karma, :salary, :gender
+
+ def condition_is_true
+ true
+ end
end
class Child < Person
diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb
new file mode 100644
index 0000000000..f9763ea853
--- /dev/null
+++ b/activemodel/test/models/person_with_validator.rb
@@ -0,0 +1,11 @@
+class PersonWithValidator
+ include ActiveModel::Validations
+
+ class PresenceValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
+ end
+ end
+
+ attr_accessor :title, :karma
+end
diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb
new file mode 100644
index 0000000000..cff47ac230
--- /dev/null
+++ b/activemodel/test/validators/email_validator.rb
@@ -0,0 +1,6 @@
+class EmailValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << (options[:message] || "is not an email") unless
+ value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
+ end
+end \ No newline at end of file