From f83b91157232e9c97c013352983313eb69e00b69 Mon Sep 17 00:00:00 2001 From: jamie Date: Sun, 3 Jan 2010 03:33:09 +0000 Subject: [PATCH 1/6] Added validates method AKA sexy migrations along with doc fixes --- activemodel/lib/active_model/validations.rb | 47 ++++++------ .../lib/active_model/validations/validates.rb | 75 ++++++++++++++++++++ activemodel/lib/active_model/validations/with.rb | 10 ++-- .../test/cases/validations/validates_test.rb | 53 ++++++++++++++ activemodel/test/models/person_with_validator.rb | 11 +++ activemodel/test/validators/email_validator.rb | 6 ++ 6 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 activemodel/lib/active_model/validations/validates.rb create mode 100644 activemodel/test/cases/validations/validates_test.rb create mode 100644 activemodel/test/models/person_with_validator.rb create mode 100644 activemodel/test/validators/email_validator.rb diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d5460a5..efd3c58 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -15,7 +15,7 @@ module ActiveModel module ClassMethods # Validates each attribute against a block. # - # class Person < ActiveRecord::Base + # class Person < ActiveModel::Base # validates_each :first_name, :last_name do |record, attr, value| # record.errors.add attr, 'starts with z.' if value[0] == ?z # end @@ -42,7 +42,7 @@ module ActiveModel # # This can be done with a symbol pointing to a method: # - # class Comment < ActiveRecord::Base + # class Comment < ActiveModel::Base # validate :must_be_friends # # def must_be_friends @@ -52,7 +52,7 @@ module ActiveModel # # Or with a block which is passed the current record to be validated: # - # class Comment < ActiveRecord::Base + # class Comment < ActiveModel::Base # validate do |comment| # comment.must_be_friends # end @@ -90,27 +90,26 @@ module ActiveModel !valid? end - protected - # Hook method defining how an attribute value should be retieved. By default this is assumed - # to be an instance named after the attribute. Override this method in subclasses should you - # need to retrieve the value for a given attribute differently e.g. - # class MyClass - # include ActiveModel::Validations - # - # def initialize(data = {}) - # @data = data - # end - # - # private - # - # def read_attribute_for_validation(key) - # @data[key] - # end - # end - # - def read_attribute_for_validation(key) - send(key) - end + # Hook method defining how an attribute value should be retieved. By default this is assumed + # to be an instance named after the attribute. Override this method in subclasses should you + # need to retrieve the value for a given attribute differently e.g. + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # private + # + # def read_attribute_for_validation(key) + # @data[key] + # end + # end + # + def read_attribute_for_validation(key) + send(key) + end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb new file mode 100644 index 0000000..2dbb7e0 --- /dev/null +++ b/activemodel/lib/active_model/validations/validates.rb @@ -0,0 +1,75 @@ +module ActiveModel + module Validations + module ClassMethods + # This method is a shortcut to all default validators and any custom + # validator classes ending in 'Validator'. Note that Rails default + # validators can be overridden inside specific classes by creating + # custom validator classes in their place such as PresenceValidator. + # + # Examples of using the default rails validators: + # validates :terms, :acceptance => true + # validates :password, :confirmation => true + # validates :username, :exclusion => { :in => %w(admin superuser) } + # validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create } + # validates :age, :inclusion => { :in => 0..9 } + # validates :first_name, :length => { :maximum => 30 } + # validates :age, :numericality => true + # validates :username, :presence => true + # validates :username, :uniqueness => true + # + # The power of the +validates+ method comes when using cusom validators + # and default validators in one call for a given attribute e.g. + # class EmailValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << (options[:message] || "is not an email") unless + # value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + # end + # end + # + # class Person < ActiveModel::Base + # attr_accessor :name, :email + # + # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } + # validates :email, :presence => true, :email => true + # end + # + # Validator classes my also exist within the class being validated + # allowing custom modules of validators to be included as needed e.g. + # + # module MyValidators + # class TitleValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i + # end + # end + # end + # + # class Person < ActiveModel::Base + # include MyValidators + # + # validates :name, :title => true + # end + # + def validates(*attributes_and_validators) + attributes = attributes_and_validators.dup + validations = attributes.pop + unless attributes.size > 0 && attributes.all?{ |attribute| attribute.is_a?(Symbol) } + raise ArgumentError, 'Attribute names must be symbols' + end + raise ArgumentError, 'Validations must be supplied in a hash' unless validations.is_a?(Hash) + + validations.each do |key, options| + validator = begin + const_get("#{key.to_s.camelize}Validator") + rescue NameError + nil + end + + raise ArgumentError, "Unknown validator: '#{key}'" unless validator + options = {} if options == true + validates_with(validator, options.merge(:attributes => attributes)) + end + end + end + end +end \ No newline at end of file diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 8d52117..20cde76 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -4,11 +4,11 @@ module ActiveModel # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions. # - # class Person < ActiveRecord::Base + # class Person < ActiveModel::Base # validates_with MyValidator # end # - # class MyValidator < ActiveRecord::Validator + # class MyValidator < ActiveModel::Validator # def validate # if some_complex_logic # record.errors[:base] << "This record is invalid" @@ -23,7 +23,7 @@ module ActiveModel # # You may also pass it multiple classes, like so: # - # class Person < ActiveRecord::Base + # class Person < ActiveModel::Base # validates_with MyValidator, MyOtherValidator, :on => :create # end # @@ -38,11 +38,11 @@ module ActiveModel # # If you pass any additional configuration options, they will be passed to the class and available as options: # - # class Person < ActiveRecord::Base + # class Person < ActiveModel::Base # validates_with MyValidator, :my_custom_key => "my custom value" # end # - # class MyValidator < ActiveRecord::Validator + # class MyValidator < ActiveModel::Validator # def validate # options[:my_custom_key] # => "my custom value" # end diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb new file mode 100644 index 0000000..c0bf03e --- /dev/null +++ b/activemodel/test/cases/validations/validates_test.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 +require 'cases/helper' +require 'models/person' +require 'models/person_with_validator' +require 'validators/email_validator' + +class ValidatesTest < ActiveRecord::TestCase + def test_validates_with_built_in_validation + Person.validates :title, :numericality => true + person = Person.new + person.valid? + assert person.errors[:title].include?('is not a number') + end + + def test_validates_with_built_in_validation_and_options + Person.validates :title, :numericality => { :message => 'my custom message' } + person = Person.new + person.valid? + assert person.errors[:title].include?('my custom message') + end + + def test_validates_with_validator_class + Person.validates :karma, :email => true + person = Person.new + person.valid? + assert person.errors[:karma].include?('is not an email') + end + + def test_validates_with_validator_class_and_options + Person.validates :karma, :email => { :message => 'my custom message' } + person = Person.new + person.valid? + assert person.errors[:karma].include?('my custom message') + end + + def test_validates_with_unknown_validator + assert_raise(ArgumentError) { Person.validates :karma, :unknown => true } + end + + def test_validates_with_included_validator + PersonWithValidator.validates :title, :presence => true + person = PersonWithValidator.new + person.valid? + assert person.errors[:title].include?('Local validator') + end + + def test_validates_with_included_validator_and_options + PersonWithValidator.validates :title, :presence => { :custom => ' please' } + person = PersonWithValidator.new + person.valid? + assert person.errors[:title].include?('Local validator please') + end +end \ No newline at end of file diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb new file mode 100644 index 0000000..f9763ea --- /dev/null +++ b/activemodel/test/models/person_with_validator.rb @@ -0,0 +1,11 @@ +class PersonWithValidator + include ActiveModel::Validations + + class PresenceValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank? + end + end + + attr_accessor :title, :karma +end diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb new file mode 100644 index 0000000..cff47ac --- /dev/null +++ b/activemodel/test/validators/email_validator.rb @@ -0,0 +1,6 @@ +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << (options[:message] || "is not an email") unless + value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + end +end \ No newline at end of file -- 1.6.4.1 From 667515c8c78afddab934bab88b0f5a3c9fa01387 Mon Sep 17 00:00:00 2001 From: jamie Date: Tue, 5 Jan 2010 09:25:44 +0000 Subject: [PATCH 2/6] Made all validates_#{method}_of methods proxy to validates --- activemodel/lib/active_model/validations.rb | 21 +++++-- .../lib/active_model/validations/acceptance.rb | 17 ++---- .../lib/active_model/validations/confirmation.rb | 8 ++- .../lib/active_model/validations/exclusion.rb | 5 +- activemodel/lib/active_model/validations/format.rb | 30 +++++----- .../lib/active_model/validations/inclusion.rb | 5 +- activemodel/lib/active_model/validations/length.rb | 3 +- .../lib/active_model/validations/numericality.rb | 3 +- .../lib/active_model/validations/presence.rb | 3 +- .../lib/active_model/validations/validates.rb | 30 ++++++---- activemodel/lib/active_model/validations/with.rb | 52 ++++++++++++----- activemodel/lib/active_model/validator.rb | 59 +++++++++++++++++--- .../test/cases/validations/with_validation_test.rb | 22 +++++++ 13 files changed, 175 insertions(+), 83 deletions(-) diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index efd3c58..a31e766 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -15,21 +15,26 @@ module ActiveModel module ClassMethods # Validates each attribute against a block. # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations + # # validates_each :first_name, :last_name do |record, attr, value| # record.errors.add attr, 'starts with z.' if value[0] == ?z # end # end # # Options: - # * :on - Specifies when this validation is active (default is :save, other options :create, :update). + # * :on - Specifies when this validation is active (default is :save, + # other options :create, :update). # * :allow_nil - Skip validation if attribute is +nil+. # * :allow_blank - Skip validation if attribute is blank. # * :if - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The + # occur (e.g. :if => :allow_validation, or + # :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * :unless - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The + # not occur (e.g. :unless => :skip_validation, or + # :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_each(*attr_names, &block) options = attr_names.extract_options!.symbolize_keys @@ -42,7 +47,9 @@ module ActiveModel # # This can be done with a symbol pointing to a method: # - # class Comment < ActiveModel::Base + # class Comment + # include ActiveModel::Validations + # # validate :must_be_friends # # def must_be_friends @@ -52,7 +59,9 @@ module ActiveModel # # Or with a block which is passed the current record to be validated: # - # class Comment < ActiveModel::Base + # class Comment + # include ActiveModel::Validations + # # validate do |comment| # comment.must_be_friends # end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index bd9463e..e440855 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -10,6 +10,10 @@ module ActiveModel record.errors.add(attribute, :accepted, :default => options[:message]) end end + + def virtual_attributes + attributes # returning all attributes as they may be virtual or actual columns + end end module ClassMethods @@ -37,18 +41,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - options = attr_names.extract_options! - - db_cols = begin - column_names - rescue Exception # To ignore both statement and connection errors - [] - end - - names = attr_names.reject { |name| db_cols.include?(name.to_s) } - attr_accessor(*names) - - validates_with AcceptanceValidator, options.merge(:attributes => attr_names) + _validates :acceptance, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index b06effd..701da71 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -6,6 +6,10 @@ module ActiveModel return if confirmed.nil? || value == confirmed record.errors.add(attribute, :confirmation, :default => options[:message]) end + + def virtual_attributes + attributes.map { |attribute| :"#{attribute}_confirmation" } + end end module ClassMethods @@ -38,9 +42,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - options = attr_names.extract_options! - attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" })) - validates_with ConfirmationValidator, options.merge(:attributes => attr_names) + _validates :confirmation, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f8759f2..ec1d3ab 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -4,6 +4,7 @@ module ActiveModel def check_validity! raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + options[:in] ||= options.delete(:within) end def validate_each(record, attribute, value) @@ -33,9 +34,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - options = attr_names.extract_options! - options[:in] ||= options.delete(:within) - validates_with ExclusionValidator, options.merge(:attributes => attr_names) + _validates :exclusion, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index d5427c2..b155cec 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -8,6 +8,20 @@ module ActiveModel record.errors.add(attribute, :invalid, :default => options[:message], :value => value) end end + + def check_validity! + unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" + raise ArgumentError, "Either :with or :without must be supplied (but not both)" + end + + if options[:with] && !options[:with].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" + end + + if options[:without] && !options[:without].is_a?(Regexp) + raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" + end + end end module ClassMethods @@ -43,21 +57,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - options = attr_names.extract_options! - - unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" - raise ArgumentError, "Either :with or :without must be supplied (but not both)" - end - - if options[:with] && !options[:with].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" - end - - if options[:without] && !options[:without].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" - end - - validates_with FormatValidator, options.merge(:attributes => attr_names) + _validates :format, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index a122e9e..70f0616 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -4,6 +4,7 @@ module ActiveModel def check_validity! raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + options[:in] ||= options.delete(:within) end def validate_each(record, attribute, value) @@ -33,9 +34,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - options = attr_names.extract_options! - options[:in] ||= options.delete(:within) - validates_with InclusionValidator, options.merge(:attributes => attr_names) + _validates :inclusion, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 6e90a75..1120a0b 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -107,8 +107,7 @@ module ActiveModel # count words as in above example.) # Defaults to lambda{ |value| value.split(//) } which counts individual characters. def validates_length_of(*attr_names) - options = attr_names.extract_options! - validates_with LengthValidator, options.merge(:attributes => attr_names) + _validates :length, *attr_names end alias_method :validates_size_of, :validates_length_of diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index f2aab8c..66e7cfb 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -103,8 +103,7 @@ module ActiveModel # end # def validates_numericality_of(*attr_names) - options = attr_names.extract_options! - validates_with NumericalityValidator, options.merge(:attributes => attr_names) + _validates :numericality, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index a4c6f86..72b5fb0 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -34,8 +34,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - options = attr_names.extract_options! - validates_with PresenceValidator, options.merge(:attributes => attr_names) + _validates :presence, *attr_names end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 2dbb7e0..6f643c0 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -26,7 +26,8 @@ module ActiveModel # end # end # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # attr_accessor :name, :email # # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } @@ -44,7 +45,8 @@ module ActiveModel # end # end # - # class Person < ActiveModel::Base + # class Film + # include ActiveModel::Validations # include MyValidators # # validates :name, :title => true @@ -57,19 +59,25 @@ module ActiveModel raise ArgumentError, 'Attribute names must be symbols' end raise ArgumentError, 'Validations must be supplied in a hash' unless validations.is_a?(Hash) - + validations.each do |key, options| - validator = begin - const_get("#{key.to_s.camelize}Validator") + begin + validator = const_get("#{key.to_s.camelize}Validator") rescue NameError - nil - end - - raise ArgumentError, "Unknown validator: '#{key}'" unless validator - options = {} if options == true - validates_with(validator, options.merge(:attributes => attributes)) + raise ArgumentError, "Unknown validator: '#{key}'" + end + validates_with(validator, (options == true ? {} : options).merge(:attributes => attributes)) end end + + private + + # Used by validates_#{method}_of methods to make sure everything + # passes through +validates+. + def _validates(validator_name, *attr_names) + options = attr_names.extract_options! + validates *attr_names, validator_name => options + end end end end \ No newline at end of file diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 20cde76..c31a3d3 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -2,14 +2,16 @@ module ActiveModel module Validations module ClassMethods - # Passes the record off to the class or classes specified and allows them to add errors based on more complex conditions. + # Passes the record off to the class or classes specified and allows them + # to add errors based on more complex conditions. # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # if some_complex_logic # record.errors[:base] << "This record is invalid" # end @@ -23,37 +25,55 @@ module ActiveModel # # You may also pass it multiple classes, like so: # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator, MyOtherValidator, :on => :create # end # # Configuration options: - # * on - Specifies when this validation is active (:create or :update - # * if - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). + # * on - Specifies when this validation is active + # (:create or :update + # * if - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. :if => :allow_validation, + # or :if => Proc.new { |user| user.signup_step > 2 }). # The method, proc or string should return or evaluate to a true or false value. - # * unless - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). + # * unless - Specifies a method, proc or string to call to + # determine if the validation should not occur + # (e.g. :unless => :skip_validation, or + # :unless => Proc.new { |user| user.signup_step <= 2 }). # The method, proc or string should return or evaluate to a true or false value. # - # If you pass any additional configuration options, they will be passed to the class and available as options: + # If you pass any additional configuration options, they will be passed + # to the class and available as options: # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator, :my_custom_key => "my custom value" # end # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # options[:my_custom_key] # => "my custom value" # end # end # def validates_with(*args, &block) options = args.extract_options! - args.each { |klass| validate(klass.new(options, &block), options) } + args.each do |klass| + validator = klass.new(options, &block) + + # Create the virtual attributes excluding those that are actual + # columns, needed for validates_acceptence_of where they can be both + db_cols = begin + column_names + rescue Exception # To ignore both statement and connection errors + [] + end + attr_accessor(*validator.virtual_attributes.reject { |name| db_cols.include?(name.to_s) }) + + validate(validator, options) + end end end end -end - - +end \ No newline at end of file diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 01695cb..dcf49a0 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,12 +1,13 @@ module ActiveModel #:nodoc: - # A simple base class that can be used along with ActiveModel::Base.validates_with + # A simple base class that can be used along with ActiveModel::Validations::ClassMethods.validates_with # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # if some_complex_logic # record.errors[:base] = "This record is invalid" # end @@ -18,10 +19,11 @@ module ActiveModel #:nodoc: # end # end # - # Any class that inherits from ActiveModel::Validator will have access to record, - # which is an instance of the record being validated, and must implement a method called validate. + # Any class that inherits from ActiveModel::Validator must implement a method + # called validate which accepts a record. # - # class Person < ActiveModel::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # @@ -36,7 +38,7 @@ module ActiveModel #:nodoc: # from within the validators message # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # record.errors[:base] << "This is some custom error message" # record.errors[:first_name] << "This is some complex validation" # # etc... @@ -51,16 +53,44 @@ module ActiveModel #:nodoc: # @my_custom_field = options[:field_name] || :first_name # end # end + # + # The easiest way to add custom validators for validating individual attributes + # is with the convenient ActiveModel::EachValidator for example: + # + # class TitleValidator < ActiveModel::EachValidator + # def validate_each(record, attribute, value) + # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) + # end + # end + # + # This can now be used in combination with the +validates+ method + # (see ActiveModel::Validations::ClassMethods.validates for more on this) + # + # class Person + # include ActiveModel::Validations + # attr_accessor :title + # + # validates :title, :presence => true, :title => true + # end class Validator attr_reader :options + # Accepts options that will be made availible through the +options+ reader. def initialize(options) @options = options end + # Override this method in subclasses with validation logic, adding errors + # to the records +errors+ array where necessary. def validate(record) raise NotImplementedError end + + # An array of symbols denoting which virtual attributes are required to use + # this validator. + def virtual_attributes + [] + end end # EachValidator is a validator which iterates through the attributes given @@ -70,7 +100,10 @@ module ActiveModel #:nodoc: # All ActiveModel validations are built on top of this Validator. class EachValidator < Validator attr_reader :attributes - + + # Returns a new validator instance. All options will be available via the + # +options+ reader, however the :attributes option will be removed + # and instead be made available through the +attributes+ reader. def initialize(options) @attributes = Array(options.delete(:attributes)) raise ":attributes cannot be blank" if @attributes.empty? @@ -78,6 +111,9 @@ module ActiveModel #:nodoc: check_validity! end + # Performs validation on the supplied record. By default this will call + # +validates_each+ to determine validity therefore subclasses should + # override +validates_each+ with validation logic. def validate(record) attributes.each do |attribute| value = record.send(:read_attribute_for_validation, attribute) @@ -86,10 +122,15 @@ module ActiveModel #:nodoc: end end + # Override this method in subclasses with the validation logic, adding + # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) raise NotImplementedError end + # Hook method that gets called by the initializer allowing verification + # that the arguments supplied are valid. You could for example raise an + # ArgumentError when invalid options are supplied. def check_validity! end end @@ -103,6 +144,8 @@ module ActiveModel #:nodoc: super end + private + def validate_each(record, attribute, value) @block.call(record, attribute, value) end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 7540ccb..1c9f5f6 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -116,10 +116,32 @@ class ValidatesWithTest < ActiveRecord::TestCase validator = mock() validator.expects(:new).with(:foo => :bar, :if => "1 == 1").returns(validator) validator.expects(:validate).with(topic) + validator.stubs(:virtual_attributes).with().returns([]) Topic.validates_with(validator, :if => "1 == 1", :foo => :bar) assert topic.valid? end + + test "creates virtual attributes based on new validator instance" do + topic = Topic.new + validator = mock(:virtual_attributes => [:attribute_1, :attribute_2]) + validator.stubs(:new).returns(validator) + validator.stubs(:validate) + Topic.expects(:attr_accessor).with(:attribute_1, :attribute_2).once + Topic.validates_with(validator) + assert topic.valid? + end + + test "doesn't create virtual attributes based on new validator instance when they are already columns" do + topic = Topic.new + validator = mock(:virtual_attributes => [:column_1, :attribute_2]) + validator.stubs(:new).returns(validator) + validator.stubs(:validate) + Topic.stubs(:column_names).with().returns('column_1') + Topic.expects(:attr_accessor).with(:attribute_2).once + Topic.validates_with(validator) + assert topic.valid? + end test "validates_with with options" do Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name) -- 1.6.4.1 From c4b6b71e68972bdff9c3a9afb18ea72d295af8cf Mon Sep 17 00:00:00 2001 From: jamie Date: Tue, 5 Jan 2010 10:47:52 +0000 Subject: [PATCH 3/6] 1.8.7 compatability fix --- .../lib/active_model/validations/validates.rb | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 6f643c0..d9bd79e 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -76,7 +76,7 @@ module ActiveModel # passes through +validates+. def _validates(validator_name, *attr_names) options = attr_names.extract_options! - validates *attr_names, validator_name => options + validates(*(attr_names << { validator_name => options })) end end end -- 1.6.4.1 From 954059fdfef7a49244d62142405cdef76177ab61 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 6 Jan 2010 19:56:19 +0000 Subject: [PATCH 4/6] Remove dependency on ActiveRecord columns --- .../lib/active_model/validations/validates.rb | 2 +- activemodel/lib/active_model/validations/with.rb | 14 ++++++-------- .../test/cases/validations/with_validation_test.rb | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index d9bd79e..1d50c26 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -76,7 +76,7 @@ module ActiveModel # passes through +validates+. def _validates(validator_name, *attr_names) options = attr_names.extract_options! - validates(*(attr_names << { validator_name => options })) + validates(*(attr_names += [{ validator_name => options }])) end end end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index c31a3d3..fd2d4ca 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -62,14 +62,12 @@ module ActiveModel args.each do |klass| validator = klass.new(options, &block) - # Create the virtual attributes excluding those that are actual - # columns, needed for validates_acceptence_of where they can be both - db_cols = begin - column_names - rescue Exception # To ignore both statement and connection errors - [] - end - attr_accessor(*validator.virtual_attributes.reject { |name| db_cols.include?(name.to_s) }) + # Create the virtual attributes excluding those that already have + # setters, needed for validates_acceptence_of where column may or may + # not exist already. Note that the map(&:to_s) is important for 1.9 + # compatibility as 1.9 instance_methods returns symbols, not strings. + accessors = validator.virtual_attributes.reject { |name| instance_methods.map(&:to_s).include?("#{name}=") } + attr_accessor(*accessors) validate(validator, options) end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 1c9f5f6..ea46712 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -132,12 +132,12 @@ class ValidatesWithTest < ActiveRecord::TestCase assert topic.valid? end - test "doesn't create virtual attributes based on new validator instance when they are already columns" do + test "doesn't create virtual attributes based on new validator instance when setters already exist" do topic = Topic.new validator = mock(:virtual_attributes => [:column_1, :attribute_2]) validator.stubs(:new).returns(validator) validator.stubs(:validate) - Topic.stubs(:column_names).with().returns('column_1') + Topic.send :attr_accessor, :column_1 Topic.expects(:attr_accessor).with(:attribute_2).once Topic.validates_with(validator) assert topic.valid? -- 1.6.4.1 From 1705f3d4c31bb2f74c5395bbcaf2459517f5f2c2 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 7 Jan 2010 01:02:50 +0000 Subject: [PATCH 5/6] Add setup hook for Validator classes --- activemodel/lib/active_model/validations.rb | 7 +++++++ .../lib/active_model/validations/acceptance.rb | 6 +++--- .../lib/active_model/validations/confirmation.rb | 6 +++--- .../lib/active_model/validations/exclusion.rb | 2 +- activemodel/lib/active_model/validations/format.rb | 2 +- .../lib/active_model/validations/inclusion.rb | 2 +- activemodel/lib/active_model/validations/length.rb | 2 +- .../lib/active_model/validations/numericality.rb | 2 +- .../lib/active_model/validations/presence.rb | 2 +- .../lib/active_model/validations/validates.rb | 9 --------- activemodel/lib/active_model/validations/with.rb | 9 +-------- activemodel/lib/active_model/validator.rb | 18 ++++++++++++------ .../test/cases/validations/with_validation_test.rb | 19 ++++++++++--------- 13 files changed, 42 insertions(+), 44 deletions(-) diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index a31e766..f5d4b4e 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -80,6 +80,13 @@ module ActiveModel end set_callback(:validate, *args, &block) end + + private + + def _merge_attributes(attr_names) + options = attr_names.extract_options! + options.merge(:attributes => attr_names) + end end # Returns the Errors object that holds all information about attribute error messages. diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index e440855..760e424 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -11,8 +11,8 @@ module ActiveModel end end - def virtual_attributes - attributes # returning all attributes as they may be virtual or actual columns + def setup(klass) + klass.send(:attr_accessor, *attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") }) end end @@ -41,7 +41,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - _validates :acceptance, *attr_names + validates_with AcceptanceValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 701da71..8041d4b 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -7,8 +7,8 @@ module ActiveModel record.errors.add(attribute, :confirmation, :default => options[:message]) end - def virtual_attributes - attributes.map { |attribute| :"#{attribute}_confirmation" } + def setup(klass) + klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) end end @@ -42,7 +42,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - _validates :confirmation, *attr_names + validates_with ConfirmationValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index ec1d3ab..85b00c0 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -34,7 +34,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - _validates :exclusion, *attr_names + validates_with ExclusionValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index b155cec..9a9e7ec 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -57,7 +57,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - _validates :format, *attr_names + validates_with FormatValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 70f0616..1af23a2 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -34,7 +34,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - _validates :inclusion, *attr_names + validates_with InclusionValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 1120a0b..f41ce34 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -107,7 +107,7 @@ module ActiveModel # count words as in above example.) # Defaults to lambda{ |value| value.split(//) } which counts individual characters. def validates_length_of(*attr_names) - _validates :length, *attr_names + validates_with LengthValidator, _merge_attributes(attr_names) end alias_method :validates_size_of, :validates_length_of diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 66e7cfb..9dfc512 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -103,7 +103,7 @@ module ActiveModel # end # def validates_numericality_of(*attr_names) - _validates :numericality, *attr_names + validates_with NumericalityValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 72b5fb0..4a71cf7 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -34,7 +34,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - _validates :presence, *attr_names + validates_with PresenceValidator, _merge_attributes(attr_names) end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 1d50c26..e2995cf 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -69,15 +69,6 @@ module ActiveModel validates_with(validator, (options == true ? {} : options).merge(:attributes => attributes)) end end - - private - - # Used by validates_#{method}_of methods to make sure everything - # passes through +validates+. - def _validates(validator_name, *attr_names) - options = attr_names.extract_options! - validates(*(attr_names += [{ validator_name => options }])) - end end end end \ No newline at end of file diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index fd2d4ca..efb03b5 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -61,14 +61,7 @@ module ActiveModel options = args.extract_options! args.each do |klass| validator = klass.new(options, &block) - - # Create the virtual attributes excluding those that already have - # setters, needed for validates_acceptence_of where column may or may - # not exist already. Note that the map(&:to_s) is important for 1.9 - # compatibility as 1.9 instance_methods returns symbols, not strings. - accessors = validator.virtual_attributes.reject { |name| instance_methods.map(&:to_s).include?("#{name}=") } - attr_accessor(*accessors) - + validator.setup(self) if validator.respond_to?(:setup) validate(validator, options) end end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index dcf49a0..47e908d 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -72,6 +72,18 @@ module ActiveModel #:nodoc: # # validates :title, :presence => true, :title => true # end + # + # Validator may also define a +setup+ instance method which will get called + # with the class that using that validator as it's argument. This can be + # useful when there are prerequisites such as an attr_accessor being present + # for example: + # + # class MyValidator < ActiveModel::Validator + # def setup(klass) + # klass.send :attr_accessor, :custom_attribute + # end + # end + # class Validator attr_reader :options @@ -85,12 +97,6 @@ module ActiveModel #:nodoc: def validate(record) raise NotImplementedError end - - # An array of symbols denoting which virtual attributes are required to use - # this validator. - def virtual_attributes - [] - end end # EachValidator is a validator which iterates through the attributes given diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index ea46712..219fb8d 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -121,24 +121,25 @@ class ValidatesWithTest < ActiveRecord::TestCase Topic.validates_with(validator, :if => "1 == 1", :foo => :bar) assert topic.valid? end - - test "creates virtual attributes based on new validator instance" do + + test "calls setup method of validator passing in self when validator has setup method" do topic = Topic.new - validator = mock(:virtual_attributes => [:attribute_1, :attribute_2]) + validator = stub_everything validator.stubs(:new).returns(validator) validator.stubs(:validate) - Topic.expects(:attr_accessor).with(:attribute_1, :attribute_2).once + validator.stubs(:respond_to?).with(:setup).returns(true) + validator.expects(:setup).with(Topic).once Topic.validates_with(validator) assert topic.valid? end - - test "doesn't create virtual attributes based on new validator instance when setters already exist" do + + test "doesn't call setup method of validator when validator has no setup method" do topic = Topic.new - validator = mock(:virtual_attributes => [:column_1, :attribute_2]) + validator = stub_everything validator.stubs(:new).returns(validator) validator.stubs(:validate) - Topic.send :attr_accessor, :column_1 - Topic.expects(:attr_accessor).with(:attribute_2).once + validator.stubs(:respond_to?).with(:setup).returns(false) + validator.expects(:setup).with(Topic).never Topic.validates_with(validator) assert topic.valid? end -- 1.6.4.1 From 2872a938e5533d67a83aa056d4f117e873fe2d4f Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 7 Jan 2010 01:12:11 +0000 Subject: [PATCH 6/6] Add documentation about 1.9 compatibility --- .../lib/active_model/validations/acceptance.rb | 5 ++++- 1 files changed, 4 insertions(+), 1 deletions(-) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 760e424..0423fcd 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -12,7 +12,10 @@ module ActiveModel end def setup(klass) - klass.send(:attr_accessor, *attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") }) + # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility + # as instance_methods returns symbols unlike 1.8 which returns strings. + new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") } + klass.send(:attr_accessor, *new_attributes) end end -- 1.6.4.1