Skip to content
This repository
Browse code

Add validates method as shortcut to setup validators for a given set …

…of attributes:

class Person < ActiveRecord::Base
  include MyValidators

  validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
  validates :email, :presence => true, :email => true
end

[#3058 status:resolved]

Signed-off-by: José Valim <jose.valim@gmail.com>
  • Loading branch information...
commit 0a79eb7889e7ac711ff171a453d65f3df57b9237 1 parent 2dcc53b
authored josevalim committed
67  activemodel/lib/active_model/validations.rb
@@ -15,21 +15,26 @@ module Validations
15 15
     module ClassMethods
16 16
       # Validates each attribute against a block.
17 17
       #
18  
-      #   class Person < ActiveRecord::Base
  18
+      #   class Person
  19
+      #     include ActiveModel::Validations
  20
+      # 
19 21
       #     validates_each :first_name, :last_name do |record, attr, value|
20 22
       #       record.errors.add attr, 'starts with z.' if value[0] == ?z
21 23
       #     end
22 24
       #   end
23 25
       #
24 26
       # Options:
25  
-      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
  27
+      # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>,
  28
+      #   other options <tt>:create</tt>, <tt>:update</tt>).
26 29
       # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
27 30
       # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
28 31
       # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
29  
-      #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
  32
+      #   occur (e.g. <tt>:if => :allow_validation</tt>, or
  33
+      #   <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).  The
30 34
       #   method, proc or string should return or evaluate to a true or false value.
31 35
       # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
32  
-      #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
  36
+      #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or
  37
+      #   <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
33 38
       #   method, proc or string should return or evaluate to a true or false value.
34 39
       def validates_each(*attr_names, &block)
35 40
         options = attr_names.extract_options!.symbolize_keys
@@ -42,7 +47,9 @@ def validates_each(*attr_names, &block)
42 47
       #
43 48
       # This can be done with a symbol pointing to a method:
44 49
       #
45  
-      #   class Comment < ActiveRecord::Base
  50
+      #   class Comment
  51
+      #     include ActiveModel::Validations
  52
+      # 
46 53
       #     validate :must_be_friends
47 54
       #
48 55
       #     def must_be_friends
@@ -52,7 +59,9 @@ def validates_each(*attr_names, &block)
52 59
       #
53 60
       # Or with a block which is passed the current record to be validated:
54 61
       #
55  
-      #   class Comment < ActiveRecord::Base
  62
+      #   class Comment
  63
+      #     include ActiveModel::Validations
  64
+      #
56 65
       #     validate do |comment|
57 66
       #       comment.must_be_friends
58 67
       #     end
@@ -71,6 +80,13 @@ def validate(*args, &block)
71 80
         end
72 81
         set_callback(:validate, *args, &block)
73 82
       end
  83
+    
  84
+      private
  85
+    
  86
+      def _merge_attributes(attr_names)
  87
+        options = attr_names.extract_options!
  88
+        options.merge(:attributes => attr_names)
  89
+      end
74 90
     end
75 91
 
76 92
     # Returns the Errors object that holds all information about attribute error messages.
@@ -90,27 +106,24 @@ def invalid?
90 106
       !valid?
91 107
     end
92 108
 
93  
-    protected
94  
-      # Hook method defining how an attribute value should be retieved. By default this is assumed
95  
-      # to be an instance named after the attribute. Override this method in subclasses should you
96  
-      # need to retrieve the value for a given attribute differently e.g.
97  
-      #   class MyClass
98  
-      #     include ActiveModel::Validations
99  
-      #
100  
-      #     def initialize(data = {})
101  
-      #       @data = data
102  
-      #     end
103  
-      #
104  
-      #     private
105  
-      #
106  
-      #     def read_attribute_for_validation(key)
107  
-      #       @data[key]
108  
-      #     end
109  
-      #   end
110  
-      #
111  
-      def read_attribute_for_validation(key)
112  
-        send(key)
113  
-      end
  109
+    # Hook method defining how an attribute value should be retieved. By default this is assumed
  110
+    # to be an instance named after the attribute. Override this method in subclasses should you
  111
+    # need to retrieve the value for a given attribute differently e.g.
  112
+    #   class MyClass
  113
+    #     include ActiveModel::Validations
  114
+    #
  115
+    #     def initialize(data = {})
  116
+    #       @data = data
  117
+    #     end
  118
+    #
  119
+    #     def read_attribute_for_validation(key)
  120
+    #       @data[key]
  121
+    #     end
  122
+    #   end
  123
+    #
  124
+    def read_attribute_for_validation(key)
  125
+      send(key)
  126
+    end
114 127
   end
115 128
 end
116 129
 
20  activemodel/lib/active_model/validations/acceptance.rb
@@ -10,6 +10,13 @@ def validate_each(record, attribute, value)
10 10
           record.errors.add(attribute, :accepted, :default => options[:message])
11 11
         end
12 12
       end
  13
+      
  14
+      def setup(klass)
  15
+        # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility
  16
+        # as instance_methods returns symbols unlike 1.8 which returns strings.
  17
+        new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") }
  18
+        klass.send(:attr_accessor, *new_attributes)        
  19
+      end
13 20
     end
14 21
 
15 22
     module ClassMethods
@@ -37,18 +44,7 @@ module ClassMethods
37 44
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
38 45
       #   method, proc or string should return or evaluate to a true or false value.
39 46
       def validates_acceptance_of(*attr_names)
40  
-        options = attr_names.extract_options!
41  
-
42  
-        db_cols = begin
43  
-          column_names
44  
-        rescue Exception # To ignore both statement and connection errors
45  
-          []
46  
-        end
47  
-
48  
-        names = attr_names.reject { |name| db_cols.include?(name.to_s) }
49  
-        attr_accessor(*names)
50  
-
51  
-        validates_with AcceptanceValidator, options.merge(:attributes => attr_names)
  47
+        validates_with AcceptanceValidator, _merge_attributes(attr_names)
52 48
       end
53 49
     end
54 50
   end
8  activemodel/lib/active_model/validations/confirmation.rb
@@ -6,6 +6,10 @@ def validate_each(record, attribute, value)
6 6
         return if confirmed.nil? || value == confirmed
7 7
         record.errors.add(attribute, :confirmation, :default => options[:message])
8 8
       end
  9
+      
  10
+      def setup(klass)
  11
+        klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" })        
  12
+      end
9 13
     end
10 14
 
11 15
     module ClassMethods
@@ -38,9 +42,7 @@ module ClassMethods
38 42
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
39 43
       #   method, proc or string should return or evaluate to a true or false value.
40 44
       def validates_confirmation_of(*attr_names)
41  
-        options = attr_names.extract_options!
42  
-        attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" }))
43  
-        validates_with ConfirmationValidator, options.merge(:attributes => attr_names)
  45
+        validates_with ConfirmationValidator, _merge_attributes(attr_names)
44 46
       end
45 47
     end
46 48
   end
5  activemodel/lib/active_model/validations/exclusion.rb
@@ -2,6 +2,7 @@ module ActiveModel
2 2
   module Validations
3 3
     class ExclusionValidator < EachValidator
4 4
       def check_validity!
  5
+        options[:in] ||= options.delete(:within)
5 6
         raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
6 7
                              ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
7 8
       end
@@ -33,9 +34,7 @@ module ClassMethods
33 34
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
34 35
       #   method, proc or string should return or evaluate to a true or false value.
35 36
       def validates_exclusion_of(*attr_names)
36  
-        options = attr_names.extract_options!
37  
-        options[:in] ||= options.delete(:within)
38  
-        validates_with ExclusionValidator, options.merge(:attributes => attr_names)
  37
+        validates_with ExclusionValidator, _merge_attributes(attr_names)
39 38
       end
40 39
     end
41 40
   end
30  activemodel/lib/active_model/validations/format.rb
@@ -8,6 +8,20 @@ def validate_each(record, attribute, value)
8 8
           record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
9 9
         end
10 10
       end
  11
+      
  12
+      def check_validity!
  13
+        unless options.include?(:with) ^ options.include?(:without)  # ^ == xor, or "exclusive or"
  14
+          raise ArgumentError, "Either :with or :without must be supplied (but not both)"
  15
+        end
  16
+
  17
+        if options[:with] && !options[:with].is_a?(Regexp)
  18
+          raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
  19
+        end
  20
+
  21
+        if options[:without] && !options[:without].is_a?(Regexp)
  22
+          raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
  23
+        end
  24
+      end
11 25
     end
12 26
 
13 27
     module ClassMethods
@@ -43,21 +57,7 @@ module ClassMethods
43 57
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
44 58
       #   method, proc or string should return or evaluate to a true or false value.
45 59
       def validates_format_of(*attr_names)
46  
-        options = attr_names.extract_options!
47  
-
48  
-        unless options.include?(:with) ^ options.include?(:without)  # ^ == xor, or "exclusive or"
49  
-          raise ArgumentError, "Either :with or :without must be supplied (but not both)"
50  
-        end
51  
-
52  
-        if options[:with] && !options[:with].is_a?(Regexp)
53  
-          raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
54  
-        end
55  
-
56  
-        if options[:without] && !options[:without].is_a?(Regexp)
57  
-          raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
58  
-        end
59  
-
60  
-        validates_with FormatValidator, options.merge(:attributes => attr_names)
  60
+        validates_with FormatValidator, _merge_attributes(attr_names)
61 61
       end
62 62
     end
63 63
   end
5  activemodel/lib/active_model/validations/inclusion.rb
@@ -2,6 +2,7 @@ module ActiveModel
2 2
   module Validations
3 3
     class InclusionValidator < EachValidator
4 4
       def check_validity!
  5
+          options[:in] ||= options.delete(:within)
5 6
          raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
6 7
                               ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
7 8
        end
@@ -33,9 +34,7 @@ module ClassMethods
33 34
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
34 35
       #   method, proc or string should return or evaluate to a true or false value.
35 36
       def validates_inclusion_of(*attr_names)
36  
-        options = attr_names.extract_options!
37  
-        options[:in] ||= options.delete(:within)
38  
-        validates_with InclusionValidator, options.merge(:attributes => attr_names)
  37
+        validates_with InclusionValidator, _merge_attributes(attr_names)
39 38
       end
40 39
     end
41 40
   end
3  activemodel/lib/active_model/validations/length.rb
@@ -107,8 +107,7 @@ module ClassMethods
107 107
       #   count words as in above example.)
108 108
       #   Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
109 109
       def validates_length_of(*attr_names)
110  
-        options = attr_names.extract_options!
111  
-        validates_with LengthValidator, options.merge(:attributes => attr_names)
  110
+        validates_with LengthValidator, _merge_attributes(attr_names)
112 111
       end
113 112
 
114 113
       alias_method :validates_size_of, :validates_length_of
3  activemodel/lib/active_model/validations/numericality.rb
@@ -103,8 +103,7 @@ module ClassMethods
103 103
       #   end
104 104
       #
105 105
       def validates_numericality_of(*attr_names)
106  
-        options = attr_names.extract_options!
107  
-        validates_with NumericalityValidator, options.merge(:attributes => attr_names)
  106
+        validates_with NumericalityValidator, _merge_attributes(attr_names)
108 107
       end
109 108
     end
110 109
   end
3  activemodel/lib/active_model/validations/presence.rb
@@ -34,8 +34,7 @@ module ClassMethods
34 34
       #   The method, proc or string should return or evaluate to a true or false value.
35 35
       #
36 36
       def validates_presence_of(*attr_names)
37  
-        options = attr_names.extract_options!
38  
-        validates_with PresenceValidator, options.merge(:attributes => attr_names)
  37
+        validates_with PresenceValidator, _merge_attributes(attr_names)
39 38
       end
40 39
     end
41 40
   end
74  activemodel/lib/active_model/validations/validates.rb
... ...
@@ -0,0 +1,74 @@
  1
+module ActiveModel
  2
+  module Validations
  3
+    module ClassMethods
  4
+      # This method is a shortcut to all default validators and any custom
  5
+      # validator classes ending in 'Validator'. Note that Rails default
  6
+      # validators can be overridden inside specific classes by creating
  7
+      # custom validator classes in their place such as PresenceValidator.
  8
+      # 
  9
+      # Examples of using the default rails validators:
  10
+      #   validates :terms, :acceptance => true
  11
+      #   validates :password, :confirmation => true
  12
+      #   validates :username, :exclusion => { :in => %w(admin superuser) }
  13
+      #   validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create }
  14
+      #   validates :age, :inclusion => { :in => 0..9 }
  15
+      #   validates :first_name, :length => { :maximum => 30 }
  16
+      #   validates :age, :numericality => true
  17
+      #   validates :username, :presence => true
  18
+      #   validates :username, :uniqueness => true
  19
+      # 
  20
+      # The power of the +validates+ method comes when using cusom validators
  21
+      # and default validators in one call for a given attribute e.g.
  22
+      #   class EmailValidator < ActiveModel::EachValidator
  23
+      #     def validate_each(record, attribute, value)
  24
+      #       record.errors[attribute] << (options[:message] || "is not an email") unless
  25
+      #         value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  26
+      #     end
  27
+      #   end
  28
+      # 
  29
+      #   class Person
  30
+      #     include ActiveModel::Validations
  31
+      #     attr_accessor :name, :email
  32
+      # 
  33
+      #     validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
  34
+      #     validates :email, :presence => true, :email => true
  35
+      #   end
  36
+      # 
  37
+      # Validator classes my also exist within the class being validated
  38
+      # allowing custom modules of validators to be included as needed e.g.
  39
+      # 
  40
+      #   module MyValidators
  41
+      #     class TitleValidator < ActiveModel::EachValidator
  42
+      #       def validate_each(record, attribute, value)
  43
+      #         record.errors[attribute] << "must start with 'the'" unless =~ /^the/i
  44
+      #       end
  45
+      #     end
  46
+      #   end
  47
+      # 
  48
+      #   class Film
  49
+      #     include ActiveModel::Validations
  50
+      #     include MyValidators
  51
+      # 
  52
+      #     validates :name, :title => true
  53
+      #   end 
  54
+      # 
  55
+      def validates(*attributes)
  56
+        validations = attributes.extract_options!
  57
+
  58
+        raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
  59
+        raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) }
  60
+        raise ArgumentError, "You need to supply at least one validation" if validations.empty?
  61
+        
  62
+        validations.each do |key, options|
  63
+          begin
  64
+            validator = const_get("#{key.to_s.camelize}Validator")
  65
+          rescue NameError
  66
+            raise ArgumentError, "Unknown validator: '#{key}'"
  67
+          end
  68
+
  69
+          validates_with(validator, (options == true ? {} : options).merge(:attributes => attributes))
  70
+        end
  71
+      end
  72
+    end
  73
+  end
  74
+end
47  activemodel/lib/active_model/validations/with.rb
@@ -2,14 +2,16 @@ module ActiveModel
2 2
   module Validations
3 3
     module ClassMethods
4 4
 
5  
-      # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions.
  5
+      # Passes the record off to the class or classes specified and allows them
  6
+      # to add errors based on more complex conditions.
6 7
       #
7  
-      #   class Person < ActiveRecord::Base
  8
+      #   class Person
  9
+      #     include ActiveModel::Validations
8 10
       #     validates_with MyValidator
9 11
       #   end
10 12
       #
11  
-      #   class MyValidator < ActiveRecord::Validator
12  
-      #     def validate
  13
+      #   class MyValidator < ActiveModel::Validator
  14
+      #     def validate(record)
13 15
       #       if some_complex_logic
14 16
       #         record.errors[:base] << "This record is invalid"
15 17
       #       end
@@ -23,37 +25,46 @@ module ClassMethods
23 25
       #
24 26
       # You may also pass it multiple classes, like so:
25 27
       #
26  
-      #   class Person < ActiveRecord::Base
  28
+      #   class Person
  29
+      #     include ActiveModel::Validations
27 30
       #     validates_with MyValidator, MyOtherValidator, :on => :create
28 31
       #   end
29 32
       #
30 33
       # Configuration options:
31  
-      # * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <tt>:update</tt>
32  
-      # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
33  
-      #   occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
  34
+      # * <tt>on</tt> - Specifies when this validation is active
  35
+      #   (<tt>:create</tt> or <tt>:update</tt>
  36
+      # * <tt>if</tt> - Specifies a method, proc or string to call to determine
  37
+      #   if the validation should occur (e.g. <tt>:if => :allow_validation</tt>,
  38
+      #   or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
34 39
       #   The method, proc or string should return or evaluate to a true or false value.
35  
-      # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
36  
-      #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
  40
+      # * <tt>unless</tt> - Specifies a method, proc or string to call to
  41
+      #   determine if the validation should not occur
  42
+      #   (e.g. <tt>:unless => :skip_validation</tt>, or
  43
+      #   <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
37 44
       #   The method, proc or string should return or evaluate to a true or false value.
38 45
       #
39  
-      # If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>:
  46
+      # If you pass any additional configuration options, they will be passed
  47
+      # to the class and available as <tt>options</tt>:
40 48
       #
41  
-      #   class Person < ActiveRecord::Base
  49
+      #   class Person
  50
+      #     include ActiveModel::Validations
42 51
       #     validates_with MyValidator, :my_custom_key => "my custom value"
43 52
       #   end
44 53
       #
45  
-      #   class MyValidator < ActiveRecord::Validator
46  
-      #     def validate
  54
+      #   class MyValidator < ActiveModel::Validator
  55
+      #     def validate(record)
47 56
       #       options[:my_custom_key] # => "my custom value"
48 57
       #     end
49 58
       #   end
50 59
       #
51 60
       def validates_with(*args, &block)
52 61
         options = args.extract_options!
53  
-        args.each { |klass| validate(klass.new(options, &block), options) }
  62
+        args.each do |klass|
  63
+          validator = klass.new(options, &block)
  64
+          validator.setup(self) if validator.respond_to?(:setup)
  65
+          validate(validator, options)
  66
+        end
54 67
       end
55 68
     end
56 69
   end
57  
-end
58  
-
59  
-
  70
+end
67  activemodel/lib/active_model/validator.rb
... ...
@@ -1,12 +1,13 @@
1 1
 module ActiveModel #:nodoc:
2  
-  # A simple base class that can be used along with ActiveModel::Base.validates_with
  2
+  # A simple base class that can be used along with ActiveModel::Validations::ClassMethods.validates_with
3 3
   #
4  
-  #   class Person < ActiveModel::Base
  4
+  #   class Person
  5
+  #     include ActiveModel::Validations
5 6
   #     validates_with MyValidator
6 7
   #   end
7 8
   #
8 9
   #   class MyValidator < ActiveModel::Validator
9  
-  #     def validate
  10
+  #     def validate(record)
10 11
   #       if some_complex_logic
11 12
   #         record.errors[:base] = "This record is invalid"
12 13
   #       end
@@ -18,10 +19,11 @@ module ActiveModel #:nodoc:
18 19
   #       end
19 20
   #   end
20 21
   #
21  
-  # Any class that inherits from ActiveModel::Validator will have access to <tt>record</tt>,
22  
-  # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>.
  22
+  # Any class that inherits from ActiveModel::Validator must implement a method
  23
+  # called <tt>validate</tt> which accepts a <tt>record</tt>.
23 24
   #
24  
-  #   class Person < ActiveModel::Base
  25
+  #   class Person
  26
+  #     include ActiveModel::Validations
25 27
   #     validates_with MyValidator
26 28
   #   end
27 29
   #
@@ -36,7 +38,7 @@ module ActiveModel #:nodoc:
36 38
   # from within the validators message
37 39
   #
38 40
   #   class MyValidator < ActiveModel::Validator
39  
-  #     def validate
  41
+  #     def validate(record)
40 42
   #       record.errors[:base] << "This is some custom error message"
41 43
   #       record.errors[:first_name] << "This is some complex validation"
42 44
   #       # etc...
@@ -51,13 +53,47 @@ module ActiveModel #:nodoc:
51 53
   #       @my_custom_field = options[:field_name] || :first_name
52 54
   #     end
53 55
   #   end
  56
+  # 
  57
+  # The easiest way to add custom validators for validating individual attributes
  58
+  # is with the convenient ActiveModel::EachValidator for example:
  59
+  # 
  60
+  #   class TitleValidator < ActiveModel::EachValidator
  61
+  #     def validate_each(record, attribute, value)
  62
+  #       record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
  63
+  #     end
  64
+  #   end
  65
+  # 
  66
+  # This can now be used in combination with the +validates+ method
  67
+  # (see ActiveModel::Validations::ClassMethods.validates for more on this)
  68
+  # 
  69
+  #   class Person
  70
+  #     include ActiveModel::Validations
  71
+  #     attr_accessor :title
  72
+  # 
  73
+  #     validates :title, :presence => true, :title => true
  74
+  #   end
  75
+  # 
  76
+  # Validator may also define a +setup+ instance method which will get called
  77
+  # with the class that using that validator as it's argument. This can be
  78
+  # useful when there are prerequisites such as an attr_accessor being present
  79
+  # for example:
  80
+  # 
  81
+  #   class MyValidator < ActiveModel::Validator
  82
+  #     def setup(klass)
  83
+  #       klass.send :attr_accessor, :custom_attribute
  84
+  #     end
  85
+  #   end
  86
+  # 
54 87
   class Validator
55 88
     attr_reader :options
56 89
 
  90
+    # Accepts options that will be made availible through the +options+ reader.
57 91
     def initialize(options)
58 92
       @options = options
59 93
     end
60 94
 
  95
+    # Override this method in subclasses with validation logic, adding errors
  96
+    # to the records +errors+ array where necessary.
61 97
     def validate(record)
62 98
       raise NotImplementedError
63 99
     end
@@ -70,7 +106,10 @@ def validate(record)
70 106
   # All ActiveModel validations are built on top of this Validator.
71 107
   class EachValidator < Validator
72 108
     attr_reader :attributes
73  
-
  109
+    
  110
+    # Returns a new validator instance. All options will be available via the
  111
+    # +options+ reader, however the <tt>:attributes</tt> option will be removed
  112
+    # and instead be made available through the +attributes+ reader.
74 113
     def initialize(options)
75 114
       @attributes = Array(options.delete(:attributes))
76 115
       raise ":attributes cannot be blank" if @attributes.empty?
@@ -78,18 +117,26 @@ def initialize(options)
78 117
       check_validity!
79 118
     end
80 119
 
  120
+    # Performs validation on the supplied record. By default this will call
  121
+    # +validates_each+ to determine validity therefore subclasses should
  122
+    # override +validates_each+ with validation logic.
81 123
     def validate(record)
82 124
       attributes.each do |attribute|
83  
-        value = record.send(:read_attribute_for_validation, attribute)
  125
+        value = record.read_attribute_for_validation(attribute)
84 126
         next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
85 127
         validate_each(record, attribute, value)
86 128
       end
87 129
     end
88 130
 
  131
+    # Override this method in subclasses with the validation logic, adding
  132
+    # errors to the records +errors+ array where necessary.
89 133
     def validate_each(record, attribute, value)
90 134
       raise NotImplementedError
91 135
     end
92 136
 
  137
+    # Hook method that gets called by the initializer allowing verification
  138
+    # that the arguments supplied are valid. You could for example raise an
  139
+    # ArgumentError when invalid options are supplied.
93 140
     def check_validity!
94 141
     end
95 142
   end
@@ -103,6 +150,8 @@ def initialize(options, &block)
103 150
       super
104 151
     end
105 152
 
  153
+    private
  154
+
106 155
     def validate_each(record, attribute, value)
107 156
       @block.call(record, attribute, value)
108 157
     end
53  activemodel/test/cases/validations/validates_test.rb
... ...
@@ -0,0 +1,53 @@
  1
+# encoding: utf-8
  2
+require 'cases/helper'
  3
+require 'models/person'
  4
+require 'models/person_with_validator'
  5
+require 'validators/email_validator'
  6
+
  7
+class ValidatesTest < ActiveRecord::TestCase  
  8
+  def test_validates_with_built_in_validation
  9
+    Person.validates :title, :numericality => true
  10
+    person = Person.new
  11
+    person.valid?
  12
+    assert person.errors[:title].include?('is not a number')
  13
+  end
  14
+  
  15
+  def test_validates_with_built_in_validation_and_options
  16
+    Person.validates :title, :numericality => { :message => 'my custom message' }
  17
+    person = Person.new
  18
+    person.valid?
  19
+    assert person.errors[:title].include?('my custom message')
  20
+  end
  21
+  
  22
+  def test_validates_with_validator_class
  23
+    Person.validates :karma, :email => true
  24
+    person = Person.new
  25
+    person.valid?
  26
+    assert person.errors[:karma].include?('is not an email')
  27
+  end
  28
+  
  29
+  def test_validates_with_validator_class_and_options
  30
+    Person.validates :karma, :email => { :message => 'my custom message' }
  31
+    person = Person.new
  32
+    person.valid?
  33
+    assert person.errors[:karma].include?('my custom message')
  34
+  end
  35
+  
  36
+  def test_validates_with_unknown_validator
  37
+    assert_raise(ArgumentError) { Person.validates :karma, :unknown => true }
  38
+  end
  39
+  
  40
+  def test_validates_with_included_validator
  41
+    PersonWithValidator.validates :title, :presence => true
  42
+    person = PersonWithValidator.new
  43
+    person.valid?
  44
+    assert person.errors[:title].include?('Local validator')
  45
+  end
  46
+  
  47
+  def test_validates_with_included_validator_and_options
  48
+    PersonWithValidator.validates :title, :presence => { :custom => ' please' }
  49
+    person = PersonWithValidator.new
  50
+    person.valid?
  51
+    assert person.errors[:title].include?('Local validator please')
  52
+  end
  53
+end
22  activemodel/test/cases/validations/with_validation_test.rb
@@ -120,6 +120,28 @@ def check_validity!
120 120
     Topic.validates_with(validator, :if => "1 == 1", :foo => :bar)
121 121
     assert topic.valid?
122 122
   end
  123
+ 
  124
+  test "calls setup method of validator passing in self when validator has setup method" do
  125
+    topic = Topic.new
  126
+    validator = stub_everything
  127
+    validator.stubs(:new).returns(validator)
  128
+    validator.stubs(:validate)
  129
+    validator.stubs(:respond_to?).with(:setup).returns(true)
  130
+    validator.expects(:setup).with(Topic).once
  131
+    Topic.validates_with(validator)
  132
+    assert topic.valid?
  133
+  end
  134
+  
  135
+  test "doesn't call setup method of validator when validator has no setup method" do
  136
+    topic = Topic.new
  137
+    validator = stub_everything
  138
+    validator.stubs(:new).returns(validator)
  139
+    validator.stubs(:validate)
  140
+    validator.stubs(:respond_to?).with(:setup).returns(false)
  141
+    validator.expects(:setup).with(Topic).never
  142
+    Topic.validates_with(validator)
  143
+    assert topic.valid?
  144
+  end
123 145
 
124 146
   test "validates_with with options" do
125 147
     Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name)
2  activemodel/test/models/custom_reader.rb
@@ -8,8 +8,6 @@ def initialize(data = {})
8 8
   def []=(key, value)
9 9
     @data[key] = value
10 10
   end
11  
-
12  
-  private
13 11
   
14 12
   def read_attribute_for_validation(key)
15 13
     @data[key]
11  activemodel/test/models/person_with_validator.rb
... ...
@@ -0,0 +1,11 @@
  1
+class PersonWithValidator
  2
+  include ActiveModel::Validations
  3
+  
  4
+  class PresenceValidator < ActiveModel::EachValidator
  5
+    def validate_each(record, attribute, value)
  6
+      record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
  7
+    end
  8
+  end
  9
+
  10
+  attr_accessor :title, :karma
  11
+end
6  activemodel/test/validators/email_validator.rb
... ...
@@ -0,0 +1,6 @@
  1
+class EmailValidator < ActiveModel::EachValidator
  2
+  def validate_each(record, attribute, value)
  3
+    record.errors[attribute] << (options[:message] || "is not an email") unless
  4
+      value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  5
+  end
  6
+end

12 notes on commit 0a79eb7

Mislav Marohnić

Maximum awesome

Jonathan Goldman

Nifty!

James Miller

Very sweet.

Luke van der Hoeven

I like.

Trey Bean

nice!

неплохая телега

Bob Martens

Wow, I like this a lot.

Jinzhu

really nice feature.

Yaroslav Markin

Great stuff!

Igor Bozato

Very crazy feature!!!!

Xavier Defrang

This is hot!

Philip Ingram

great job. thanks!

Please sign in to comment.
Something went wrong with that request. Please try again.