Skip to content
This repository
Browse code

Move all the Active Record validations to Active Model

  • Loading branch information...
commit 8828b2ca674acfa028a3c1e086a1795d3bb893e1 1 parent 6ed42eb
Pratik authored March 19, 2009

Showing 22 changed files with 555 additions and 1,353 deletions. Show diff stats Hide diff stats

  1. 48  activemodel/lib/active_model.rb
  2. 23  activemodel/lib/active_model/deprecated_error_methods.rb
  3. 160  activemodel/lib/active_model/errors.rb
  4. 41  activemodel/lib/active_model/validations.rb
  5. 21  activemodel/lib/active_model/validations/acceptance.rb
  6. 11  activemodel/lib/active_model/validations/associated.rb
  7. 10  activemodel/lib/active_model/validations/confirmation.rb
  8. 16  activemodel/lib/active_model/validations/exclusion.rb
  9. 16  activemodel/lib/active_model/validations/format.rb
  10. 16  activemodel/lib/active_model/validations/inclusion.rb
  11. 80  activemodel/lib/active_model/validations/length.rb
  12. 23  activemodel/lib/active_model/validations/numericality.rb
  13. 32  activemodel/lib/active_model/validations/presence.rb
  14. 125  activemodel/lib/active_model/validations/uniqueness.rb
  15. 9  activerecord/lib/active_record.rb
  16. 2  activerecord/lib/active_record/autosave_association.rb
  17. 2  activerecord/lib/active_record/base.rb
  18. 1,003  activerecord/lib/active_record/validations.rb
  19. 4  activerecord/test/cases/autosave_association_test.rb
  20. 8  activerecord/test/cases/validations_i18n_test.rb
  21. 248  activerecord/test/cases/validations_test.rb
  22. 10  activerecord/test/models/reply.rb
48  activemodel/lib/active_model.rb
... ...
@@ -1,5 +1,43 @@
1  
-require 'active_model/observing'
2  
-# disabled until they're tested
3  
-# require 'active_model/callbacks'
4  
-# require 'active_model/validations'
5  
-require 'active_model/base'
  1
+#--
  2
+# Copyright (c) 2004-2009 David Heinemeier Hansson
  3
+#
  4
+# Permission is hereby granted, free of charge, to any person obtaining
  5
+# a copy of this software and associated documentation files (the
  6
+# "Software"), to deal in the Software without restriction, including
  7
+# without limitation the rights to use, copy, modify, merge, publish,
  8
+# distribute, sublicense, and/or sell copies of the Software, and to
  9
+# permit persons to whom the Software is furnished to do so, subject to
  10
+# the following conditions:
  11
+#
  12
+# The above copyright notice and this permission notice shall be
  13
+# included in all copies or substantial portions of the Software.
  14
+#
  15
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  19
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  20
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22
+#++
  23
+
  24
+begin
  25
+  require 'active_support'
  26
+rescue LoadError
  27
+  activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
  28
+  if File.directory?(activesupport_path)
  29
+    $:.unshift activesupport_path
  30
+    require 'active_support'
  31
+  end
  32
+end
  33
+
  34
+module ActiveModel
  35
+  def self.load_all!
  36
+    [Base]
  37
+  end
  38
+
  39
+  autoload :Base, 'active_model/base'
  40
+  autoload :Validations, 'active_model/validations'
  41
+  autoload :Errors, 'active_model/errors'
  42
+  autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
  43
+end
23  activemodel/lib/active_model/deprecated_error_methods.rb
... ...
@@ -1,37 +1,28 @@
1 1
 module ActiveModel
2 2
   module DeprecatedErrorMethods
3 3
     def on(attribute)
4  
-      ActiveSupport::Deprecation.warn "Errors#on have been deprecated, use Errors#[] instead"
5  
-      self[attribute]
  4
+      # ActiveSupport::Deprecation.warn "Errors#on have been deprecated, use Errors#[] instead"
  5
+      errors = self[attribute]
  6
+      errors.size < 2 ? errors.first : errors
6 7
     end
7 8
 
8 9
     def on_base
9  
-      ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead"
  10
+      # ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead"
10 11
       on(:base)
11 12
     end
12 13
 
13  
-    def add(attribute, msg = Errors.default_error_messages[:invalid])
14  
-      ActiveSupport::Deprecation.warn "Errors#add(attribute, msg) has been deprecated, use Errors#[attribute] << msg instead"
15  
-      self[attribute] << msg
16  
-    end
17  
-
18 14
     def add_to_base(msg)
19  
-      ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#[:base] << msg instead"
  15
+      # ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#[:base] << msg instead"
20 16
       self[:base] << msg
21 17
     end
22 18
   
23 19
     def invalid?(attribute)
24  
-      ActiveSupport::Deprecation.warn "Errors#invalid?(attribute) has been deprecated, use Errors#[attribute].any? instead"
  20
+      # ActiveSupport::Deprecation.warn "Errors#invalid?(attribute) has been deprecated, use Errors#[attribute].any? instead"
25 21
       self[attribute].any?
26 22
     end
27 23
 
28  
-    def full_messages
29  
-      ActiveSupport::Deprecation.warn "Errors#full_messages has been deprecated, use Errors#to_a instead"
30  
-      to_a
31  
-    end
32  
-
33 24
     def each_full
34  
-      ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead"
  25
+      # ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead"
35 26
       to_a.each { |error| yield error }
36 27
     end
37 28
   end
160  activemodel/lib/active_model/errors.rb
... ...
@@ -1,40 +1,27 @@
1 1
 module ActiveModel
2 2
   class Errors < Hash
3 3
     include DeprecatedErrorMethods
4  
-    
5  
-    @@default_error_messages = {
6  
-      :inclusion                => "is not included in the list",
7  
-      :exclusion                => "is reserved",
8  
-      :invalid                  => "is invalid",
9  
-      :confirmation             => "doesn't match confirmation",
10  
-      :accepted                 => "must be accepted",
11  
-      :empty                    => "can't be empty",
12  
-      :blank                    => "can't be blank",
13  
-      :too_long                 => "is too long (maximum is %d characters)",
14  
-      :too_short                => "is too short (minimum is %d characters)",
15  
-      :wrong_length             => "is the wrong length (should be %d characters)",
16  
-      :taken                    => "has already been taken",
17  
-      :not_a_number             => "is not a number",
18  
-      :greater_than             => "must be greater than %d",
19  
-      :greater_than_or_equal_to => "must be greater than or equal to %d",
20  
-      :equal_to                 => "must be equal to %d",
21  
-      :less_than                => "must be less than %d",
22  
-      :less_than_or_equal_to    => "must be less than or equal to %d",
23  
-      :odd                      => "must be odd",
24  
-      :even                     => "must be even"
25  
-    }
26  
-  
27  
-    ##
28  
-    # :singleton-method:
29  
-    # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
30  
-    cattr_accessor :default_error_messages
  4
+
  5
+    class << self
  6
+      def default_error_messages
  7
+        message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')."
  8
+        ActiveSupport::Deprecation.warn(message)
  9
+
  10
+        I18n.translate 'activerecord.errors.messages'
  11
+      end
  12
+    end
  13
+
  14
+    def initialize(base)
  15
+      @base = base
  16
+      super()
  17
+    end
31 18
 
32 19
     alias_method :get, :[]
33 20
     alias_method :set, :[]=
34 21
 
35 22
     def [](attribute)
36 23
       if errors = get(attribute.to_sym)
37  
-        errors.size == 1 ? errors.first : errors
  24
+        errors
38 25
       else
39 26
         set(attribute.to_sym, [])
40 27
       end
@@ -55,17 +42,11 @@ def size
55 42
     end
56 43
 
57 44
     def to_a
58  
-      inject([]) do |errors_with_attributes, (attribute, errors)|
59  
-        if error.blank?
60  
-          errors_with_attributes
61  
-        else
62  
-          if attr == :base
63  
-            errors_with_attributes << error
64  
-          else
65  
-            errors_with_attributes << (attribute.to_s.humanize + " " + error)
66  
-          end
67  
-        end
68  
-      end
  45
+      full_messages
  46
+    end
  47
+
  48
+    def count
  49
+      to_a.size
69 50
     end
70 51
 
71 52
     def to_xml(options={})
@@ -78,5 +59,106 @@ def to_xml(options={})
78 59
         to_a.each { |error| e.error(error) }
79 60
       end
80 61
     end
  62
+
  63
+    # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
  64
+    # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
  65
+    # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
  66
+    # If no +messsage+ is supplied, :invalid is assumed.
  67
+    # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
  68
+    def add(attribute, message = nil, options = {})
  69
+      message ||= :invalid
  70
+      message = generate_message(attribute, message, options) if message.is_a?(Symbol)
  71
+      self[attribute] << message
  72
+    end
  73
+
  74
+    # Will add an error message to each of the attributes in +attributes+ that is empty.
  75
+    def add_on_empty(attributes, custom_message = nil)
  76
+      [attributes].flatten.each do |attribute|
  77
+        value = @base.respond_to?(attribute.to_s) ? @base.send(attribute.to_s) : @base[attribute.to_s]
  78
+        is_empty = value.respond_to?(:empty?) ? value.empty? : false
  79
+        add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty
  80
+      end
  81
+    end
  82
+
  83
+    # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
  84
+    def add_on_blank(attributes, custom_message = nil)
  85
+      [attributes].flatten.each do |attribute|
  86
+        value = @base.respond_to?(attribute.to_sym) ? @base.send(attribute.to_sym) : @base[attribute.to_sym]
  87
+        add(attribute, :blank, :default => custom_message) if value.blank?
  88
+      end
  89
+    end
  90
+
  91
+    # Returns all the full error messages in an array.
  92
+    #
  93
+    #   class Company < ActiveRecord::Base
  94
+    #     validates_presence_of :name, :address, :email
  95
+    #     validates_length_of :name, :in => 5..30
  96
+    #   end
  97
+    #
  98
+    #   company = Company.create(:address => '123 First St.')
  99
+    #   company.errors.full_messages # =>
  100
+    #     ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
  101
+    def full_messages(options = {})
  102
+      full_messages = []
  103
+
  104
+      each do |attribute, messages|
  105
+        next if messages.empty?
  106
+
  107
+        if attribute == :base
  108
+          messages.each {|m| full_messages << m }
  109
+        else
  110
+          attr_name = @base.class.human_attribute_name(attribute.to_s)
  111
+          prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ')
  112
+          messages.each do |m|
  113
+            full_messages <<  "#{prefix}#{m}"
  114
+          end
  115
+        end
  116
+      end
  117
+
  118
+      full_messages
  119
+    end
  120
+
  121
+    # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
  122
+    # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, 
  123
+    # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the 
  124
+    # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name, 
  125
+    # translated attribute name and the value are available for interpolation.
  126
+    #
  127
+    # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
  128
+    # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
  129
+    # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
  130
+    # 
  131
+    # <ol>
  132
+    # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
  133
+    # <li><tt>activerecord.errors.models.admin.blank</tt></li>
  134
+    # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
  135
+    # <li><tt>activerecord.errors.models.user.blank</tt></li>
  136
+    # <li><tt>activerecord.errors.messages.blank</tt></li>
  137
+    # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
  138
+    # </ol>
  139
+    def generate_message(attribute, message = :invalid, options = {})
  140
+      message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
  141
+
  142
+      defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
  143
+        [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
  144
+          :"models.#{klass.name.underscore}.#{message}" ]
  145
+      end
  146
+      
  147
+      defaults << options.delete(:default)
  148
+      defaults = defaults.compact.flatten << :"messages.#{message}"
  149
+
  150
+      key = defaults.shift
  151
+      value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
  152
+
  153
+      options = { :default => defaults,
  154
+        :model => @base.class.human_name,
  155
+        :attribute => @base.class.human_attribute_name(attribute.to_s),
  156
+        :value => value,
  157
+        :scope => [:activerecord, :errors]
  158
+      }.merge(options)
  159
+
  160
+      I18n.translate(key, options)
  161
+    end
  162
+
81 163
   end
82 164
 end
41  activemodel/lib/active_model/validations.rb
@@ -38,7 +38,7 @@ module ClassMethods
38 38
       #   end
39 39
       #
40 40
       # This usage applies to +validate_on_create+ and +validate_on_update as well+.
41  
-      #
  41
+
42 42
       # Validates each attribute against a block.
43 43
       #
44 44
       #   class Person < ActiveRecord::Base
@@ -48,7 +48,7 @@ module ClassMethods
48 48
       #   end
49 49
       #
50 50
       # Options:
51  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
  51
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
52 52
       # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
53 53
       # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
54 54
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
@@ -83,7 +83,7 @@ def validation_method(on)
83 83
 
84 84
     # Returns the Errors object that holds all information about attribute error messages.
85 85
     def errors
86  
-      @errors ||= Errors.new
  86
+      @errors ||= Errors.new(self)
87 87
     end
88 88
 
89 89
     # Runs all the specified validations and returns true if no errors were added otherwise false.
@@ -92,35 +92,52 @@ def valid?
92 92
 
93 93
       run_callbacks(:validate)
94 94
       
95  
-      if responds_to?(:validate)
96  
-        ActiveSupport::Deprecations.warn "Base#validate has been deprecated, please use Base.validate :method instead"
  95
+      if respond_to?(:validate)
  96
+        # ActiveSupport::Deprecation.warn "Base#validate has been deprecated, please use Base.validate :method instead"
97 97
         validate
98 98
       end
99 99
 
100 100
       if new_record?
101 101
         run_callbacks(:validate_on_create)
102 102
 
103  
-        if responds_to?(:validate_on_create)
104  
-          ActiveSupport::Deprecations.warn(
105  
-            "Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead")
  103
+        if respond_to?(:validate_on_create)
  104
+          # ActiveSupport::Deprecation.warn "Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead"
106 105
           validate_on_create
107 106
         end
108 107
       else
109 108
         run_callbacks(:validate_on_update)
110 109
 
111  
-        if responds_to?(:validate_on_update)
112  
-          ActiveSupport::Deprecations.warn(
113  
-            "Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead")
  110
+        if respond_to?(:validate_on_update)
  111
+          # ActiveSupport::Deprecation.warn "Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead"
114 112
           validate_on_update
115 113
         end
116 114
       end
117 115
 
118 116
       errors.empty?
119 117
     end
  118
+
  119
+    # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
  120
+    def invalid?
  121
+      !valid?
  122
+    end
  123
+
  124
+    protected
  125
+    
  126
+    # Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
  127
+    def validate
  128
+    end
  129
+
  130
+    # Overwrite this method for validation checks used only on creation.
  131
+    def validate_on_create
  132
+    end
  133
+
  134
+    # Overwrite this method for validation checks used only on updates.
  135
+    def validate_on_update
  136
+    end
120 137
   end
121 138
 end
122 139
 
123 140
 Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
124 141
   filename = File.basename(path)
125 142
   require "active_model/validations/#{filename}"
126  
-end
  143
+end
21  activemodel/lib/active_model/validations/acceptance.rb
@@ -8,16 +8,16 @@ module ClassMethods
8 8
       #     validates_acceptance_of :eula, :message => "must be abided"
9 9
       #   end
10 10
       #
11  
-      # If the database column does not exist, the <tt>:terms_of_service</tt> attribute is entirely virtual. This check is
12  
-      # performed only if <tt>:terms_of_service</tt> is not +nil+ and by default on save.
  11
+      # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is
  12
+      # performed only if +terms_of_service+ is not +nil+ and by default on save.
13 13
       #
14 14
       # Configuration options:
15  
-      # * <tt>:message</tt> - A custom error message (default is: "must be accepted")
16  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
17  
-      # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. (default is +true+)
  15
+      # * <tt>:message</tt> - A custom error message (default is: "must be accepted").
  16
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
  17
+      # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true).
18 18
       # * <tt>:accept</tt> - Specifies value that is considered accepted.  The default value is a string "1", which
19 19
       #   makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database
20  
-      #   column, since the attribute is typecasted from "1" to +true+ before validation.
  20
+      #   column, since the attribute is typecast from "1" to +true+ before validation.
21 21
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
22 22
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
23 23
       #   method, proc or string should return or evaluate to a true or false value.
@@ -25,19 +25,22 @@ module ClassMethods
25 25
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
26 26
       #   method, proc or string should return or evaluate to a true or false value.
27 27
       def validates_acceptance_of(*attr_names)
28  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
  28
+        configuration = { :on => :save, :allow_nil => true, :accept => "1" }
29 29
         configuration.update(attr_names.extract_options!)
30 30
 
31 31
         db_cols = begin
32 32
           column_names
33  
-        rescue ActiveRecord::StatementInvalid
  33
+        rescue Exception # To ignore both statement and connection errors
34 34
           []
35 35
         end
  36
+
36 37
         names = attr_names.reject { |name| db_cols.include?(name.to_s) }
37 38
         attr_accessor(*names)
38 39
 
39 40
         validates_each(attr_names,configuration) do |record, attr_name, value|
40  
-          record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
  41
+          unless value == configuration[:accept]
  42
+            record.errors.add(attr_name, :accepted, :default => configuration[:message]) 
  43
+          end
41 44
         end
42 45
       end
43 46
     end
11  activemodel/lib/active_model/validations/associated.rb
@@ -18,14 +18,14 @@ module ClassMethods
18 18
       #     validates_associated :book
19 19
       #   end
20 20
       #
21  
-      # ...this would specify a circular dependency and cause infinite recursion.
  21
+      # this would specify a circular dependency and cause infinite recursion.
22 22
       #
23 23
       # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
24 24
       # is both present and guaranteed to be valid, you also need to use +validates_presence_of+.
25 25
       #
26 26
       # Configuration options:
27 27
       # * <tt>:message</tt> - A custom error message (default is: "is invalid")
28  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
  28
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
29 29
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
30 30
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
31 31
       #   method, proc or string should return or evaluate to a true or false value.
@@ -33,12 +33,13 @@ module ClassMethods
33 33
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
34 34
       #   method, proc or string should return or evaluate to a true or false value.
35 35
       def validates_associated(*attr_names)
36  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
  36
+        configuration = { :on => :save }
37 37
         configuration.update(attr_names.extract_options!)
38 38
 
39 39
         validates_each(attr_names, configuration) do |record, attr_name, value|
40  
-          record.errors.add(attr_name, configuration[:message]) unless
41  
-            (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
  40
+          unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
  41
+            record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
  42
+          end
42 43
         end
43 44
       end
44 45
     end
10  activemodel/lib/active_model/validations/confirmation.rb
@@ -21,8 +21,8 @@ module ClassMethods
21 21
       #   validates_presence_of :password_confirmation, :if => :password_changed?
22 22
       #
23 23
       # Configuration options:
24  
-      # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation")
25  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
  24
+      # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation").
  25
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
26 26
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
27 27
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
28 28
       #   method, proc or string should return or evaluate to a true or false value.
@@ -30,13 +30,15 @@ module ClassMethods
30 30
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
31 31
       #   method, proc or string should return or evaluate to a true or false value.
32 32
       def validates_confirmation_of(*attr_names)
33  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
  33
+        configuration = { :on => :save }
34 34
         configuration.update(attr_names.extract_options!)
35 35
 
36 36
         attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
37 37
 
38 38
         validates_each(attr_names, configuration) do |record, attr_name, value|
39  
-          record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
  39
+          unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
  40
+            record.errors.add(attr_name, :confirmation, :default => configuration[:message]) 
  41
+          end
40 42
         end
41 43
       end
42 44
     end
16  activemodel/lib/active_model/validations/exclusion.rb
@@ -6,14 +6,14 @@ module ClassMethods
6 6
       #   class Person < ActiveRecord::Base
7 7
       #     validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
8 8
       #     validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
9  
-      #     validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
  9
+      #     validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed"
10 10
       #   end
11 11
       #
12 12
       # Configuration options:
13  
-      # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of
14  
-      # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved")
15  
-      # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+)
16  
-      # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+)
  13
+      # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
  14
+      # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
  15
+      # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
  16
+      # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
17 17
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
18 18
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
19 19
       #   method, proc or string should return or evaluate to a true or false value.
@@ -21,7 +21,7 @@ module ClassMethods
21 21
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
22 22
       #   method, proc or string should return or evaluate to a true or false value.
23 23
       def validates_exclusion_of(*attr_names)
24  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
  24
+        configuration = { :on => :save }
25 25
         configuration.update(attr_names.extract_options!)
26 26
 
27 27
         enum = configuration[:in] || configuration[:within]
@@ -29,7 +29,9 @@ def validates_exclusion_of(*attr_names)
29 29
         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?)
30 30
 
31 31
         validates_each(attr_names, configuration) do |record, attr_name, value|
32  
-          record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
  32
+          if enum.include?(value)
  33
+            record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) 
  34
+          end
33 35
         end
34 36
       end
35 37
     end
16  activemodel/lib/active_model/validations/format.rb
@@ -13,11 +13,11 @@ module ClassMethods
13 13
       # A regular expression must be provided or else an exception will be raised.
14 14
       #
15 15
       # Configuration options:
16  
-      # * <tt>:message</tt> - A custom error message (default is: "is invalid")
17  
-      # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+)
18  
-      # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+)
19  
-      # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!)
20  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
  16
+      # * <tt>:message</tt> - A custom error message (default is: "is invalid").
  17
+      # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
  18
+      # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
  19
+      # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
  20
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
21 21
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
22 22
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
23 23
       #   method, proc or string should return or evaluate to a true or false value.
@@ -25,13 +25,15 @@ module ClassMethods
25 25
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
26 26
       #   method, proc or string should return or evaluate to a true or false value.
27 27
       def validates_format_of(*attr_names)
28  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
  28
+        configuration = { :on => :save, :with => nil }
29 29
         configuration.update(attr_names.extract_options!)
30 30
 
31 31
         raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
32 32
 
33 33
         validates_each(attr_names, configuration) do |record, attr_name, value|
34  
-          record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with]
  34
+          unless value.to_s =~ configuration[:with]
  35
+            record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) 
  36
+          end
35 37
         end
36 38
       end
37 39
     end
16  activemodel/lib/active_model/validations/inclusion.rb
@@ -6,14 +6,14 @@ module ClassMethods
6 6
       #   class Person < ActiveRecord::Base
7 7
       #     validates_inclusion_of :gender, :in => %w( m f )
8 8
       #     validates_inclusion_of :age, :in => 0..99
9  
-      #     validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
  9
+      #     validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
10 10
       #   end
11 11
       #
12 12
       # Configuration options:
13  
-      # * <tt>:in</tt> - An enumerable object of available items
14  
-      # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list")
15  
-      # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is null (default is: +false+)
16  
-      # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+)
  13
+      # * <tt>:in</tt> - An enumerable object of available items.
  14
+      # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
  15
+      # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
  16
+      # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
17 17
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
18 18
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
19 19
       #   method, proc or string should return or evaluate to a true or false value.
@@ -21,7 +21,7 @@ module ClassMethods
21 21
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
22 22
       #   method, proc or string should return or evaluate to a true or false value.
23 23
       def validates_inclusion_of(*attr_names)
24  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
  24
+        configuration = { :on => :save }
25 25
         configuration.update(attr_names.extract_options!)
26 26
 
27 27
         enum = configuration[:in] || configuration[:within]
@@ -29,7 +29,9 @@ def validates_inclusion_of(*attr_names)
29 29
         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?)
30 30
 
31 31
         validates_each(attr_names, configuration) do |record, attr_name, value|
32  
-          record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
  32
+          unless enum.include?(value)
  33
+            record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) 
  34
+          end
33 35
         end
34 36
       end
35 37
     end
80  activemodel/lib/active_model/validations/length.rb
@@ -2,44 +2,46 @@ module ActiveModel
2 2
   module Validations
3 3
     module ClassMethods
4 4
       ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
5  
-    
  5
+
6 6
       # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
7 7
       #
8 8
       #   class Person < ActiveRecord::Base
9  
-      #     validates_length_of :first_name, :maximum => 30
10  
-      #     validates_length_of :last_name, :maximum => 30, :message => "less than %d if you don't mind"
  9
+      #     validates_length_of :first_name, :maximum=>30
  10
+      #     validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
11 11
       #     validates_length_of :fax, :in => 7..32, :allow_nil => true
12 12
       #     validates_length_of :phone, :in => 7..32, :allow_blank => true
13 13
       #     validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
14  
-      #     validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
15  
-      #     validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
  14
+      #     validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
  15
+      #     validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
  16
+      #     validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
16 17
       #   end
17 18
       #
18 19
       # Configuration options:
19  
-      # * <tt>:minimum</tt> - The minimum size of the attribute
20  
-      # * <tt>:maximum</tt> - The maximum size of the attribute
21  
-      # * <tt>:is</tt> - The exact size of the attribute
22  
-      # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute
23  
-      # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>
  20
+      # * <tt>:minimum</tt> - The minimum size of the attribute.
  21
+      # * <tt>:maximum</tt> - The maximum size of the attribute.
  22
+      # * <tt>:is</tt> - The exact size of the attribute.
  23
+      # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute.
  24
+      # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
24 25
       # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
25 26
       # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
26  
-      # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)")
27  
-      # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
28  
-      # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
29  
-      # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation.  An alias of the appropriate <tt>:too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message
30  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
  27
+      # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)").
  28
+      # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)").
  29
+      # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)").
  30
+      # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation.  An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
  31
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
31 32
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
32 33
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
33 34
       #   method, proc or string should return or evaluate to a true or false value.
34 35
       # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
35 36
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
36 37
       #   method, proc or string should return or evaluate to a true or false value.
  38
+      # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
  39
+      #   count words as in above example.)
  40
+      #   Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
37 41
       def validates_length_of(*attrs)
38 42
         # Merge given options with defaults.
39 43
         options = {
40  
-          :too_long     => ActiveRecord::Errors.default_error_messages[:too_long],
41  
-          :too_short    => ActiveRecord::Errors.default_error_messages[:too_short],
42  
-          :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
  44
+          :tokenizer => lambda {|value| value.split(//)}
43 45
         }.merge(DEFAULT_VALIDATION_OPTIONS)
44 46
         options.update(attrs.extract_options!.symbolize_keys)
45 47
 
@@ -57,35 +59,33 @@ def validates_length_of(*attrs)
57 59
         # Get range option and value.
58 60
         option = range_options.first
59 61
         option_value = options[range_options.first]
  62
+        key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
  63
+        custom_message = options[:message] || options[key]
60 64
 
61 65
         case option
62  
-          when :within, :in
63  
-            raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
64  
-
65  
-            too_short = options[:too_short] % option_value.begin
66  
-            too_long  = options[:too_long]  % option_value.end
  66
+        when :within, :in
  67
+          raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
67 68
 
68  
-            validates_each(attrs, options) do |record, attr, value|
69  
-              value = value.split(//) if value.kind_of?(String)
70  
-              if value.nil? or value.size < option_value.begin
71  
-                record.errors.add(attr, too_short)
72  
-              elsif value.size > option_value.end
73  
-                record.errors.add(attr, too_long)
74  
-              end
  69
+          validates_each(attrs, options) do |record, attr, value|
  70
+            value = options[:tokenizer].call(value) if value.kind_of?(String)
  71
+            if value.nil? or value.size < option_value.begin
  72
+              record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin)
  73
+            elsif value.size > option_value.end
  74
+              record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end)
75 75
             end
76  
-          when :is, :minimum, :maximum
77  
-            raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
78  
-
79  
-            # Declare different validations per option.
80  
-            validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
81  
-            message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
  76
+          end
  77
+        when :is, :minimum, :maximum
  78
+          raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
82 79
 
83  
-            message = (options[:message] || options[message_options[option]]) % option_value
  80
+          # Declare different validations per option.
  81
+          validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
84 82
 
85  
-            validates_each(attrs, options) do |record, attr, value|
86  
-              value = value.split(//) if value.kind_of?(String)
87  
-              record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
  83
+          validates_each(attrs, options) do |record, attr, value|
  84
+            value = options[:tokenizer].call(value) if value.kind_of?(String)
  85
+            unless !value.nil? and value.size.method(validity_checks[option])[option_value]
  86
+              record.errors.add(attr, key, :default => custom_message, :count => option_value) 
88 87
             end
  88
+          end
89 89
         end
90 90
       end
91 91
 
23  activemodel/lib/active_model/validations/numericality.rb
@@ -5,10 +5,9 @@ module ClassMethods
5 5
                                   :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
6 6
                                   :odd => 'odd?', :even => 'even?' }.freeze
7 7
 
8  
-
9 8
       # Validates whether the value of the specified attribute is numeric by trying to convert it to
10  
-      # a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
11  
-      # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is true).
  9
+      # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
  10
+      # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
12 11
       #
13 12
       #   class Person < ActiveRecord::Base
14 13
       #     validates_numericality_of :value, :on => :create
@@ -50,15 +49,15 @@ def validates_numericality_of(*attr_names)
50 49
 
51 50
           if configuration[:only_integer]
52 51
             unless raw_value.to_s =~ /\A[+-]?\d+\Z/
53  
-              record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
  52
+              record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
54 53
               next
55 54
             end
56 55
             raw_value = raw_value.to_i
57 56
           else
58  
-           begin
59  
-              raw_value = Kernel.Float(raw_value.to_s)
  57
+            begin
  58
+              raw_value = Kernel.Float(raw_value)
60 59
             rescue ArgumentError, TypeError
61  
-              record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
  60
+              record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
62 61
               next
63 62
             end
64 63
           end
@@ -66,11 +65,13 @@ def validates_numericality_of(*attr_names)
66 65
           numericality_options.each do |option|
67 66
             case option
68 67
               when :odd, :even
69  
-                record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
  68
+                unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
  69
+                  record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) 
  70
+                end
70 71
               else
71  
-                message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
72  
-                message = message % configuration[option] if configuration[option]
73  
-                record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
  72
+                unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
  73
+                  record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
  74
+                end
74 75
             end
75 76
           end
76 77
         end
32  activemodel/lib/active_model/validations/presence.rb
@@ -7,28 +7,26 @@ module ClassMethods
7 7
       #     validates_presence_of :first_name
8 8
       #   end
9 9
       #
10  
-      # The +first_name+ attribute must be in the object and it cannot be blank.
  10
+      # The first_name attribute must be in the object and it cannot be blank.
11 11
       #
12  
-      # If you want to validate the presence of a boolean field (where the real values are +true+ and +false+),
13  
-      # you will want to use
  12
+      # If you want to validate the presence of a boolean field (where the real values are true and false),
  13
+      # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>.
14 14
       #
15  
-      #   validates_inclusion_of :field_name, :in => [true, false]
16  
-      #
17  
-      # This is due to the way Object#blank? handles boolean values:
18  
-      #
19  
-      #   false.blank? # => true
  15
+      # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>.
20 16
       #
21 17
       # Configuration options:
22  
-      # * <tt>:message</tt> - A custom error message (default is: "can't be blank")
23  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>)
24  
-      # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
25  
-      #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
26  
-      #   method, proc or string should return or evaluate to a true or false value.
27  
-      # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
28  
-      #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
29  
-      #   method, proc or string should return or evaluate to a true or false value.
  18
+      # * <tt>message</tt> - A custom error message (default is: "can't be blank").
  19
+      # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, 
  20
+      #   <tt>:update</tt>).
  21
+      # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
  22
+      #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
  23
+      #   The method, proc or string should return or evaluate to a true or false value.
  24
+      # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
  25
+      #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
  26
+      #   The method, proc or string should return or evaluate to a true or false value.
  27
+      #
30 28
       def validates_presence_of(*attr_names)
31  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
  29
+        configuration = { :on => :save }
32 30
         configuration.update(attr_names.extract_options!)
33 31
 
34 32
         # can't use validates_each here, because it cannot cope with nonexistent attributes,
125  activemodel/lib/active_model/validations/uniqueness.rb
@@ -18,24 +18,84 @@ module ClassMethods
18 18
       # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
19 19
       # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
20 20
       #
21  
-      # Because this check is performed outside the database there is still a chance that duplicate values
22  
-      # will be inserted in two parallel transactions.  To guarantee against this you should create a 
23  
-      # unique index on the field. See +add_index+ for more information.
24  
-      #
25 21
       # Configuration options:
26  
-      # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken")
  22
+      # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
27 23
       # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
28  
-      # * <tt>:case_sensitive</tt> - Looks for an exact match.  Ignored by non-text columns (+true+ by default).
29  
-      # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+)
30  
-      # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+)
  24
+      # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
  25
+      # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
  26
+      # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
31 27
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
32 28
       #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
33 29
       #   method, proc or string should return or evaluate to a true or false value.
34 30
       # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
35 31
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
36 32
       #   method, proc or string should return or evaluate to a true or false value.
  33
+      #
  34
+      # === Concurrency and integrity
  35
+      #
  36
+      # Using this validation method in conjunction with ActiveRecord::Base#save
  37
+      # does not guarantee the absence of duplicate record insertions, because
  38
+      # uniqueness checks on the application level are inherently prone to race
  39
+      # conditions. For example, suppose that two users try to post a Comment at
  40
+      # the same time, and a Comment's title must be unique. At the database-level,
  41
+      # the actions performed by these users could be interleaved in the following manner:
  42
+      #
  43
+      #               User 1                 |               User 2
  44
+      #  ------------------------------------+--------------------------------------
  45
+      #  # User 1 checks whether there's     |
  46
+      #  # already a comment with the title  |
  47
+      #  # 'My Post'. This is not the case.  |
  48
+      #  SELECT * FROM comments              |
  49
+      #  WHERE title = 'My Post'             |
  50
+      #                                      |
  51
+      #                                      | # User 2 does the same thing and also
  52
+      #                                      | # infers that his title is unique.
  53
+      #                                      | SELECT * FROM comments
  54
+      #                                      | WHERE title = 'My Post'
  55
+      #                                      |
  56
+      #  # User 1 inserts his comment.       |
  57
+      #  INSERT INTO comments                |
  58
+      #  (title, content) VALUES             |
  59
+      #  ('My Post', 'hi!')                  |
  60
+      #                                      |
  61
+      #                                      | # User 2 does the same thing.
  62
+      #                                      | INSERT INTO comments
  63
+      #                                      | (title, content) VALUES
  64
+      #                                      | ('My Post', 'hello!')
  65
+      #                                      |
  66
+      #                                      | # ^^^^^^
  67
+      #                                      | # Boom! We now have a duplicate
  68
+      #                                      | # title!
  69
+      #
  70
+      # This could even happen if you use transactions with the 'serializable'
  71
+      # isolation level. There are several ways to get around this problem:
  72
+      # - By locking the database table before validating, and unlocking it after
  73
+      #   saving. However, table locking is very expensive, and thus not
  74
+      #   recommended.
  75
+      # - By locking a lock file before validating, and unlocking it after saving.
  76
+      #   This does not work if you've scaled your Rails application across
  77
+      #   multiple web servers (because they cannot share lock files, or cannot
  78
+      #   do that efficiently), and thus not recommended.
  79
+      # - Creating a unique index on the field, by using
  80
+      #   ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
  81
+      #   rare case that a race condition occurs, the database will guarantee
  82
+      #   the field's uniqueness.
  83
+      #   
  84
+      #   When the database catches such a duplicate insertion,
  85
+      #   ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
  86
+      #   exception. You can either choose to let this error propagate (which
  87
+      #   will result in the default Rails exception page being shown), or you
  88
+      #   can catch it and restart the transaction (e.g. by telling the user
  89
+      #   that the title already exists, and asking him to re-enter the title).
  90
+      #   This technique is also known as optimistic concurrency control:
  91
+      #   http://en.wikipedia.org/wiki/Optimistic_concurrency_control
  92
+      #   
  93
+      #   Active Record currently provides no way to distinguish unique
  94
+      #   index constraint errors from other types of database errors, so you
  95
+      #   will have to parse the (database-specific) exception message to detect
  96
+      #   such a case.
37 97
       def validates_uniqueness_of(*attr_names)
38  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
  98
+        configuration = { :case_sensitive => true }
39 99
         configuration.update(attr_names.extract_options!)
40 100
 
41 101
         validates_each(attr_names,configuration) do |record, attr_name, value|
@@ -53,20 +113,31 @@ def validates_uniqueness_of(*attr_names)
53 113
           # class (which has a database table to query from).
54 114
           finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
55 115
 
56  
-          if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
57  
-            condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
  116
+          column = finder_class.columns_hash[attr_name.to_s]
  117
+
  118
+          if value.nil?
  119
+            comparison_operator = "IS ?"
  120
+          elsif column.text?
  121
+            comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
  122
+            value = column.limit ? value.to_s[0, column.limit] : value.to_s
  123
+          else
  124
+            comparison_operator = "= ?"
  125
+          end
  126
+
  127
+          sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
  128
+
  129
+          if value.nil? || (configuration[:case_sensitive] || !column.text?)
  130
+            condition_sql = "#{sql_attribute} #{comparison_operator}"
58 131
             condition_params = [value]
59 132
           else
60  
-            # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
61  
-            # Hence, this is needed only for sqlite.
62  
-            condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
63  
-            condition_params = [value.downcase]
  133
+            condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
  134
+            condition_params = [value.mb_chars.downcase]
64 135
           end
65 136
 
66 137
           if scope = configuration[:scope]
67 138
             Array(scope).map do |scope_item|
68 139
               scope_value = record.send(scope_item)
69  
-              condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}"
  140
+              condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
70 141
               condition_params << scope_value
71 142
             end
72 143
           end
@@ -76,26 +147,10 @@ def validates_uniqueness_of(*attr_names)
76 147
             condition_params << record.send(:id)
77 148
           end
78 149
 
79  
-          results = finder_class.with_exclusive_scope do
80  
-            connection.select_all(
81  
-              construct_finder_sql(
82  
-                :select     => attr_name,
83  
-                :from       => finder_class.quoted_table_name,
84  
-                :conditions => [condition_sql, *condition_params]
85  
-              )
86  
-            )
87  
-          end
88  
-
89  
-          unless results.length.zero?
90  
-            found = true
91  
-
92  
-            # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
93  
-            # column in ruby when case sensitive option
94  
-            if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
95  
-              found = results.any? { |a| a[attr_name.to_s] == value }
  150
+          finder_class.with_exclusive_scope do
  151
+            if finder_class.exists?([condition_sql, *condition_params])
  152
+              record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
96 153
             end
97  
-
98  
-            record.errors.add(attr_name, configuration[:message]) if found
99 154
           end
100 155
         end
101 156
       end
9  activerecord/lib/active_record.rb
@@ -31,6 +31,15 @@