提交 f1085f41 编写于 作者: J José Valim

Move validations in ActiveModel to validators, however all validatity checks...

Move validations in ActiveModel to validators, however all validatity checks are still in the class method.
上级 2476c531
......@@ -35,9 +35,9 @@ module ActiveModel
autoload :Dirty
autoload :Errors
autoload :Lint
autoload :Name, 'active_model/naming'
autoload :Name, 'active_model/naming'
autoload :Naming
autoload :Observer, 'active_model/observing'
autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :Serialization
autoload :StateMachine
......@@ -45,6 +45,7 @@ module ActiveModel
autoload :Validations
autoload :ValidationsRepairHelper
autoload :Validator
autoload :EachValidator, 'active_model/validator'
autoload :VERSION
module Serializers
......
module ActiveModel
module Validations
class AcceptanceValidator < EachValidator
def validate_each(record, attribute, value)
unless value == options[:accept]
record.errors.add(attribute, :accepted, :default => options[:message])
end
end
end
module ClassMethods
# Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
#
......@@ -25,8 +33,8 @@ module ClassMethods
# 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 = { :allow_nil => true, :accept => "1" }
options.update(attr_names.extract_options!)
db_cols = begin
column_names
......@@ -37,11 +45,7 @@ def validates_acceptance_of(*attr_names)
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
......
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 ClassMethods
# 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
......
module ActiveModel
module Validations
class ExclusionValidator < EachValidator
def validate_each(record, attribute, value)
return unless options[:in].include?(value)
record.errors.add(attribute, :exclusion, :default => options[:message], :value => value)
end
end
module ClassMethods
# Validates that the value of the specified attribute is not in a particular enumerable object.
#
......@@ -21,17 +28,13 @@ module ClassMethods
# 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]
options = attr_names.extract_options!
options[:in] ||= options.delete(:within)
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
":in option of the configuration hash" unless options[:in].respond_to?(:include?)
validates_each(attr_names, configuration) do |record, attr_name, value|
if enum.include?(value)
record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
end
end
validates_with ExclusionValidator, options.merge(:attributes => attr_names)
end
end
end
......
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 ClassMethods
# 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
......
module ActiveModel
module Validations
class InclusionValidator < EachValidator
def validate_each(record, attribute, value)
return if options[:in].include?(value)
record.errors.add(attribute, :inclusion, :default => options[:message], :value => value)
end
end
module ClassMethods
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
......@@ -21,17 +28,13 @@ module ClassMethods
# 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]
options = attr_names.extract_options!
options[:in] ||= options.delete(:within)
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
":in option of the configuration hash" unless options[:in].respond_to?(:include?)
validates_each(attr_names, configuration) do |record, attr_name, value|
unless enum.include?(value)
record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
end
end
validates_with InclusionValidator, options.merge(:attributes => attr_names)
end
end
end
......
module ActiveModel
module Validations
class LengthValidator < EachValidator
MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
attr_reader :type
def initialize(options)
@type = options.delete(:type)
super
end
def validate_each(record, attribute, value)
checks = options.slice(:minimum, :maximum, :is)
value = options[:tokenizer].call(value) if value.kind_of?(String)
if [:within, :in].include?(type)
range = options[type]
checks[:minimum], checks[:maximum] = range.begin, range.end
checks[:maximum] -= 1 if range.exclude_end?
end
checks.each do |key, check_value|
custom_message = options[:message] || options[MESSAGES[key]]
validity_check = CHECKS[key]
valid_value = if key == :maximum
value.nil? || value.size.send(validity_check, check_value)
else
value && value.size.send(validity_check, check_value)
end
record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) unless valid_value
end
end
end
module ClassMethods
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
#
......@@ -38,62 +75,32 @@ module ClassMethods
# * <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)
def validates_length_of(*attr_names)
options = { :tokenizer => DEFAULT_TOKENIZER }
options.update(attr_names.extract_options!)
# Ensure that one and only one range option is specified.
range_options = ALL_RANGE_OPTIONS & options.keys
case range_options.size
when 0
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
when 1
# Valid number of options; do nothing.
else
raise ArgumentError, 'Too many range options specified. Choose only one.'
raise ArgumentError, 'Too many range options specified. Choose only one.'
end
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
custom_message = options[:message] || options[key]
type = range_options.first
value = options[type]
case option
case type
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
min, max = option_value.begin, option_value.end
max = max - 1 if option_value.exclude_end?
if value.nil? || value.size < min
record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => min)
elsif value.size > max
record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => max)
end
end
raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range)
when :is, :minimum, :maximum
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
valid_value = if option == :maximum
value.nil? || value.size.send(validity_checks[option], option_value)
else
value && value.size.send(validity_checks[option], option_value)
end
record.errors.add(attr, key, :default => custom_message, :count => option_value) unless valid_value
end
raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0
end
validates_with LengthValidator, options.merge(:attributes => attr_names, :type => type)
end
alias_method :validates_size_of, :validates_length_of
......
module ActiveModel
module Validations
class NumericalityValidator < EachValidator
CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
:odd => 'odd?', :even => 'even?' }.freeze
def validate_each(record, attr_name, value)
before_type_cast = "#{attr_name}_before_type_cast"
if record.respond_to?(before_type_cast.to_sym)
raw_value = record.send("#{attr_name}_before_type_cast") || value
else
raw_value = value
end
return if options[:allow_nil] and raw_value.nil?
if options[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message])
return
end
raw_value = raw_value.to_i
else
begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message])
return
end
end
options.slice(*CHECKS.keys).each do |option, option_value|
case option
when :odd, :even
unless raw_value.to_i.method(CHECKS[option])[]
record.errors.add(attr_name, option, :value => raw_value, :default => options[:message])
end
else
option_value = option_value.call(record) if option_value.is_a? Proc
option_value = record.method(option_value).call if option_value.is_a? Symbol
unless raw_value.method(CHECKS[option])[option_value]
record.errors.add(attr_name, option, :default => options[:message], :value => raw_value, :count => option_value)
end
end
end
end
end
module ClassMethods
ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
:odd => 'odd?', :even => 'even?' }.freeze
# Validates whether the value of the specified attribute is numeric by trying to convert it to
# a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
......@@ -44,61 +90,18 @@ module ClassMethods
# validates_numericality_of :width, :greater_than => :minimum_weight
# end
#
#
def validates_numericality_of(*attr_names)
configuration = { :only_integer => false, :allow_nil => false }
configuration.update(attr_names.extract_options!)
options = { :only_integer => false, :allow_nil => false }
options.update(attr_names.extract_options!)
numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
numericality_options = NumericalityValidator::CHECKS.keys & options.keys
(numericality_options - [ :odd, :even ]).each do |option|
value = configuration[option]
value = options[option]
raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
end
validates_each(attr_names,configuration) do |record, attr_name, value|
before_type_cast = "#{attr_name}_before_type_cast"
if record.respond_to?(before_type_cast.to_sym)
raw_value = record.send("#{attr_name}_before_type_cast") || value
else
raw_value = value
end
next if configuration[:allow_nil] and raw_value.nil?
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
else
begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
numericality_options.each do |option|
case option
when :odd, :even
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
end
else
configuration[option] = configuration[option].call(record) if configuration[option].is_a? Proc
configuration[option] = record.method(configuration[option]).call if configuration[option].is_a? Symbol
unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
end
end
end
end
validates_with NumericalityValidator, options.merge(:attributes => attr_names)
end
end
end
......
......@@ -2,6 +2,12 @@
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 ClassMethods
# 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
......
......@@ -50,10 +50,7 @@ module ClassMethods
#
def validates_with(*args)
options = args.extract_options!
args.each do |klass|
validate klass.new(options.except(:on, :if, :unless)), options
end
args.each { |klass| validate(klass.new(options), options) }
end
end
end
......
module ActiveModel #:nodoc:
# A simple base class that can be used along with ActiveModel::Base.validates_with
#
# class Person < ActiveModel::Base
......@@ -61,7 +60,28 @@ def initialize(options)
end
def validate(record)
raise "You must override this method"
raise NotImplementedError
end
end
class EachValidator < Validator
attr_reader :attributes
def initialize(options)
@attributes = options.delete(:attributes)
super
end
def validate(record)
attributes.each do |attribute|
value = record.send(:read_attribute_for_validation, attribute)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
validate_each(record, attribute, value)
end
end
def validate_each(record)
raise NotImplementedError
end
end
end
......@@ -98,10 +98,10 @@ def validate(record)
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
test "passes all non-standard configuration options to the validator class" do
test "passes all configuration options to the validator class" do
topic = Topic.new
validator = mock()
validator.expects(:new).with({:foo => :bar}).returns(validator)
validator.expects(:new).with(:foo => :bar, :if => "1 == 1").returns(validator)
validator.expects(:validate).with(topic)
Topic.validates_with(validator, :if => "1 == 1", :foo => :bar)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册