aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@gmail.com>2009-12-23 12:14:00 +0100
committerJosé Valim <jose.valim@gmail.com>2009-12-23 12:14:00 +0100
commit44cd9e0e7132abe632664377f13f3edd1106685a (patch)
tree71373ffe546443361cb9e33010de8c73a835c073
parent279067639f319f3b4bbcaf90c26f286e96df2c77 (diff)
downloadrails-44cd9e0e7132abe632664377f13f3edd1106685a.tar.gz
rails-44cd9e0e7132abe632664377f13f3edd1106685a.tar.bz2
rails-44cd9e0e7132abe632664377f13f3edd1106685a.zip
ActiveRecord::Validations are now built on top of Validator as well.
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb7
-rw-r--r--activemodel/lib/active_model/validations/length.rb3
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb7
-rw-r--r--activerecord/lib/active_record/validations/associated.rb16
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb134
5 files changed, 96 insertions, 71 deletions
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index a5de58cd41..bd9463ed27 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -1,6 +1,10 @@
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])
@@ -33,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)
- options = { :allow_nil => true, :accept => "1" }
- options.update(attr_names.extract_options!)
+ options = attr_names.extract_options!
db_cols = begin
column_names
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index 04280b401b..6e90a75c17 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -9,9 +9,8 @@ module ActiveModel
attr_reader :type
def initialize(options)
- options[:tokenizer] ||= DEFAULT_TOKENIZER
@type = (OPTIONS & options.keys).first
- super
+ super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER))
end
def check_validity!
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 914a3133cf..f2aab8c5b8 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -5,6 +5,10 @@ module ActiveModel
: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)
@@ -99,8 +103,7 @@ module ActiveModel
# end
#
def validates_numericality_of(*attr_names)
- options = { :only_integer => false, :allow_nil => false }
- options.update(attr_names.extract_options!)
+ options = attr_names.extract_options!
validates_with NumericalityValidator, options.merge(:attributes => attr_names)
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 92f47d770f..6e6f4df415 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,5 +1,12 @@
module ActiveRecord
module Validations
+ class AssociatedValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if (value.is_a?(Array) ? value : [value]).compact.all?{ |r| r.valid? }
+ record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
+ end
+ end
+
module ClassMethods
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
#
@@ -33,13 +40,8 @@ module ActiveRecord
# 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_associated(*attr_names)
- configuration = attr_names.extract_options!
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
- end
- end
+ options = attr_names.extract_options!
+ validates_with AssociatedValidator, options.merge(:attributes => attr_names)
end
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 711086dc2c..b3a34501dc 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,5 +1,77 @@
module ActiveRecord
module Validations
+ class UniquenessValidator < ActiveModel::EachValidator
+ def initialize(options)
+ @klass = options.delete(:klass)
+ super(options.reverse_merge(:case_sensitive => true))
+ end
+
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table_name = record.class.quoted_table_name
+ sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
+
+ Array(options[:scope]).each do |scope_item|
+ scope_value = record.send(scope_item)
+ sql << " AND " << record.class.send(:attribute_condition, "#{table_name}.#{scope_item}", scope_value)
+ params << scope_value
+ end
+
+ unless record.new_record?
+ sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
+ params << record.send(:id)
+ end
+
+ finder_class.with_exclusive_scope do
+ if finder_class.exists?([sql, *params])
+ record.errors.add(attribute, :taken, :default => options[:message], :value => value)
+ end
+ end
+ end
+
+ protected
+
+ # The check for an existing value should be run from a class that
+ # isn't abstract. This means working down from the current class
+ # (self), to the first non-abstract class. Since classes don't know
+ # their subclasses, we have to build the hierarchy between self and
+ # the record's class.
+ def find_finder_class_for(record) #:nodoc:
+ class_hierarchy = [record.class]
+
+ while class_hierarchy.first != @klass
+ class_hierarchy.insert(0, class_hierarchy.first.superclass)
+ end
+
+ class_hierarchy.detect { |klass| !klass.abstract_class? }
+ end
+
+ def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
+ column = klass.columns_hash[attribute.to_s]
+
+ operator = if value.nil?
+ "IS ?"
+ elsif column.text?
+ value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
+ "#{klass.connection.case_sensitive_equality_operator} ?"
+ else
+ "= ?"
+ end
+
+ sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
+
+ if value.nil? || (options[:case_sensitive] || !column.text?)
+ sql = "#{sql_attribute} #{operator}"
+ params = [value]
+ else
+ sql = "LOWER(#{sql_attribute}) #{operator}"
+ params = [value.mb_chars.downcase]
+ end
+
+ [sql, params]
+ end
+ end
+
module ClassMethods
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
# can be named "davidhh".
@@ -69,6 +141,7 @@ module ActiveRecord
#
# This could even happen if you use transactions with the 'serializable'
# isolation level. There are several ways to get around this problem:
+ #
# - By locking the database table before validating, and unlocking it after
# saving. However, table locking is very expensive, and thus not
# recommended.
@@ -94,65 +167,10 @@ module ActiveRecord
# index constraint errors from other types of database errors, so you
# will have to parse the (database-specific) exception message to detect
# such a case.
+ #
def validates_uniqueness_of(*attr_names)
- configuration = { :case_sensitive => true }
- configuration.update(attr_names.extract_options!)
-
- validates_each(attr_names,configuration) do |record, attr_name, value|
- # The check for an existing value should be run from a class that
- # isn't abstract. This means working down from the current class
- # (self), to the first non-abstract class. Since classes don't know
- # their subclasses, we have to build the hierarchy between self and
- # the record's class.
- class_hierarchy = [record.class]
- while class_hierarchy.first != self
- class_hierarchy.insert(0, class_hierarchy.first.superclass)
- end
-
- # Now we can work our way down the tree to the first non-abstract
- # class (which has a database table to query from).
- finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
-
- column = finder_class.columns_hash[attr_name.to_s]
-
- if value.nil?
- comparison_operator = "IS ?"
- elsif column.text?
- comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
- else
- comparison_operator = "= ?"
- end
-
- sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
-
- if value.nil? || (configuration[:case_sensitive] || !column.text?)
- condition_sql = "#{sql_attribute} #{comparison_operator}"
- condition_params = [value]
- else
- condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
- condition_params = [value.mb_chars.downcase]
- end
-
- if scope = configuration[:scope]
- Array(scope).map do |scope_item|
- scope_value = record.send(scope_item)
- condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
- condition_params << scope_value
- end
- end
-
- unless record.new_record?
- condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
- condition_params << record.send(:id)
- end
-
- finder_class.with_exclusive_scope do
- if finder_class.exists?([condition_sql, *condition_params])
- record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
- end
- end
- end
+ options = attr_names.extract_options!
+ validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self)
end
end
end