From 532b11690fa2c4fd2c127ddc7df246c4469ebbc4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 3 Jan 2010 22:02:10 -0500 Subject: Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 [DHH] --- activemodel/lib/active_model/serializers/json.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index ee6d48bfc6..794de7dc55 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,19 +10,17 @@ module ActiveModel included do extend ActiveModel::Naming - cattr_accessor :include_root_in_json, :instance_writer => false + cattr_accessor :include_root_in_json, :instance_writer => true end # Returns a JSON string representing the model. Some configuration is # available through +options+. # - # The option ActiveRecord::Base.include_root_in_json controls the - # top-level behavior of to_json. In a new Rails application, it is set to - # true in initializers/new_rails_defaults.rb. When it is true, + # The option ActiveModel::Base.include_root_in_json controls the + # top-level behavior of to_json. It is true by default. When it is true, # to_json will emit a single root node named after the object's type. For example: # # konata = User.find(1) - # ActiveRecord::Base.include_root_in_json = true # konata.to_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } -- cgit v1.2.3 From 2601a16ede92566c651c06942294250ea653bd85 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 4 Jan 2010 16:22:39 -0600 Subject: Autoload AS test case --- activemodel/lib/active_model/test_case.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb index 4cb5c9cbc0..6328807ad7 100644 --- a/activemodel/lib/active_model/test_case.rb +++ b/activemodel/lib/active_model/test_case.rb @@ -1,5 +1,3 @@ -require "active_support/test_case" - module ActiveModel #:nodoc: class TestCase < ActiveSupport::TestCase #:nodoc: def with_kcode(kcode) -- cgit v1.2.3 From 190ce3ab37966957997cd18772d1260bf121c2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 7 Jan 2010 01:04:25 +0100 Subject: Errors messages are now moved from :activerecord.errors to simply :errors on I18n yml files. --- activemodel/lib/active_model/errors.rb | 5 ++- activemodel/lib/active_model/locale/en.yml | 49 +++++++++++++++--------------- 2 files changed, 26 insertions(+), 28 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index abc084a74b..0b6c75c46e 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -105,8 +105,7 @@ module ActiveModel else attr_name = attribute.to_s.gsub('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - options = { :default => "{{attribute}} {{message}}", :attribute => attr_name, - :scope => @base.class.i18n_scope } + options = { :default => "{{attribute}} {{message}}", :attribute => attr_name } messages.each do |m| full_messages << I18n.t(:"errors.format", options.merge(:message => m)) @@ -153,7 +152,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value, - :scope => [@base.class.i18n_scope, :errors] + :scope => [:errors] }.merge(options) I18n.translate(key, options) diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 1cdb897f13..ea58021767 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -1,27 +1,26 @@ en: - activemodel: - errors: - # model.errors.full_messages format. - format: "{{attribute}} {{message}}" + errors: + # The default format use in full error messages. + format: "{{attribute}} {{message}}" - # The values :model, :attribute and :value are always available for interpolation - # The value :count is available when applicable. Can be used for pluralization. - messages: - inclusion: "is not included in the list" - exclusion: "is reserved" - invalid: "is invalid" - confirmation: "doesn't match confirmation" - accepted: "must be accepted" - empty: "can't be empty" - blank: "can't be blank" - too_long: "is too long (maximum is {{count}} characters)" - too_short: "is too short (minimum is {{count}} characters)" - wrong_length: "is the wrong length (should be {{count}} characters)" - not_a_number: "is not a number" - greater_than: "must be greater than {{count}}" - greater_than_or_equal_to: "must be greater than or equal to {{count}}" - equal_to: "must be equal to {{count}}" - less_than: "must be less than {{count}}" - less_than_or_equal_to: "must be less than or equal to {{count}}" - odd: "must be odd" - even: "must be even" + # The values :model, :attribute and :value are always available for interpolation + # The value :count is available when applicable. Can be used for pluralization. + messages: + inclusion: "is not included in the list" + exclusion: "is reserved" + invalid: "is invalid" + confirmation: "doesn't match confirmation" + accepted: "must be accepted" + empty: "can't be empty" + blank: "can't be blank" + too_long: "is too long (maximum is {{count}} characters)" + too_short: "is too short (minimum is {{count}} characters)" + wrong_length: "is the wrong length (should be {{count}} characters)" + not_a_number: "is not a number" + greater_than: "must be greater than {{count}}" + greater_than_or_equal_to: "must be greater than or equal to {{count}}" + equal_to: "must be equal to {{count}}" + less_than: "must be less than {{count}}" + less_than_or_equal_to: "must be less than or equal to {{count}}" + odd: "must be odd" + even: "must be even" -- cgit v1.2.3 From a25ef06956725f5c4c8833f86f61f1f12c4323e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 7 Jan 2010 02:45:50 +0100 Subject: Allow to specify default attributes names translation in I18n yml files. For example, you could easily specify :created_at and :updated_at translations as: en: attributes: created_at: "Created at" updated_at: "Updated at" This configuration is built on ActiveModel, so it means those translations are shared between different ORMs as well (but always as a fallback). --- activemodel/lib/active_model/translation.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index e5ef1e6114..2d2df269d0 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -25,13 +25,14 @@ module ActiveModel # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) defaults = lookup_ancestors.map do |klass| - :"#{klass.model_name.underscore}.#{attribute}" + :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}" end + defaults << :"attributes.#{attribute}" defaults << options.delete(:default) if options[:default] defaults << attribute.to_s.humanize - options.reverse_merge! :scope => [self.i18n_scope, :attributes], :count => 1, :default => defaults + options.reverse_merge! :count => 1, :default => defaults I18n.translate(defaults.shift, options) end -- cgit v1.2.3 From 0a79eb7889e7ac711ff171a453d65f3df57b9237 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 7 Jan 2010 18:44:35 +0100 Subject: Add validates method as shortcut to setup validators for a given set of attributes: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- activemodel/lib/active_model/validations.rb | 67 ++++++++++++-------- .../lib/active_model/validations/acceptance.rb | 20 +++--- .../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 | 74 ++++++++++++++++++++++ activemodel/lib/active_model/validations/with.rb | 47 ++++++++------ activemodel/lib/active_model/validator.rb | 67 +++++++++++++++++--- 12 files changed, 236 insertions(+), 96 deletions(-) create mode 100644 activemodel/lib/active_model/validations/validates.rb (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d5460a58bd..573c914e63 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 < ActiveRecord::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 < ActiveRecord::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 < ActiveRecord::Base + # class Comment + # include ActiveModel::Validations + # # validate do |comment| # comment.must_be_friends # end @@ -71,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. @@ -90,27 +106,24 @@ 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 + # + # 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/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index bd9463ed27..0423fcd17f 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -10,6 +10,13 @@ module ActiveModel record.errors.add(attribute, :accepted, :default => options[:message]) end end + + def setup(klass) + # 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 module ClassMethods @@ -37,18 +44,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_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 b06effdceb..8041d4b61f 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 setup(klass) + klass.send(:attr_accessor, *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_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 f8759f253b..0a44c6d54f 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -2,6 +2,7 @@ module ActiveModel module Validations class ExclusionValidator < EachValidator def check_validity! + options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end @@ -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_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 d5427c2b03..9a9e7eca4d 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_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 a122e9e737..25b8c7866d 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -2,6 +2,7 @@ module ActiveModel module Validations class InclusionValidator < EachValidator def check_validity! + options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end @@ -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_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 6e90a75c17..f41ce34328 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_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 f2aab8c5b8..9dfc5125cd 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_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 a4c6f866a7..4a71cf79b5 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_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 new file mode 100644 index 0000000000..e8935d3794 --- /dev/null +++ b/activemodel/lib/active_model/validations/validates.rb @@ -0,0 +1,74 @@ +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 + # include ActiveModel::Validations + # 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 Film + # include ActiveModel::Validations + # include MyValidators + # + # validates :name, :title => true + # end + # + def validates(*attributes) + validations = attributes.extract_options! + + raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? + raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) } + raise ArgumentError, "You need to supply at least one validation" if validations.empty? + + validations.each do |key, options| + begin + validator = const_get("#{key.to_s.camelize}Validator") + rescue NameError + raise ArgumentError, "Unknown validator: '#{key}'" + end + + validates_with(validator, (options == true ? {} : 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 8d521173c6..db563876af 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 < ActiveRecord::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator # end # - # class MyValidator < ActiveRecord::Validator - # def validate + # class MyValidator < ActiveModel::Validator + # def validate(record) # if some_complex_logic # record.errors[:base] << "This record is invalid" # end @@ -23,37 +25,46 @@ module ActiveModel # # You may also pass it multiple classes, like so: # - # class Person < ActiveRecord::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 < ActiveRecord::Base + # class Person + # include ActiveModel::Validations # validates_with MyValidator, :my_custom_key => "my custom value" # end # - # class MyValidator < ActiveRecord::Validator - # def validate + # class MyValidator < ActiveModel::Validator + # 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) + validator.setup(self) if validator.respond_to?(:setup) + 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 01695cb73a..382a4cc98d 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,13 +53,47 @@ 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 + # + # 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 + # 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 @@ -70,7 +106,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,18 +117,26 @@ 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) + value = record.read_attribute_for_validation(attribute) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) validate_each(record, attribute, value) 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 +150,8 @@ module ActiveModel #:nodoc: super end + private + def validate_each(record, attribute, value) @block.call(record, attribute, value) end -- cgit v1.2.3 From 47a5fd4c4ba447c997c0a029adfa80d8b790b25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 7 Jan 2010 19:22:32 +0100 Subject: Allow :if, :unless, :on, :allow_nil and :allow_blank as shared options in validates. --- activemodel/lib/active_model/validations.rb | 4 +--- .../lib/active_model/validations/validates.rb | 24 ++++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 573c914e63..276472ea46 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -121,9 +121,7 @@ module ActiveModel # end # end # - def read_attribute_for_validation(key) - send(key) - end + alias :read_attribute_for_validation :send end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e8935d3794..61caf32c06 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -31,7 +31,7 @@ module ActiveModel # attr_accessor :name, :email # # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } - # validates :email, :presence => true, :email => true + # validates :email, :presence => true, :format => { :with => /@/ } # end # # Validator classes my also exist within the class being validated @@ -44,21 +44,33 @@ module ActiveModel # end # end # end - # + # # class Film # include ActiveModel::Validations # include MyValidators # # validates :name, :title => true # end - # + # + # The options :if, :unless, :on, :allow_blank and :allow_nil can be given to one specific + # validator: + # + # validates :password, :presence => { :if => :password_required? }, :confirmation => true + # + # Or to all at the same time: + # + # validates :password, :presence => true, :confirmation => true, :if => :password_required? + # def validates(*attributes) - validations = attributes.extract_options! + defaults = attributes.extract_options! + validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil) raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) } raise ArgumentError, "You need to supply at least one validation" if validations.empty? - + + defaults.merge!(:attributes => attributes) + validations.each do |key, options| begin validator = const_get("#{key.to_s.camelize}Validator") @@ -66,7 +78,7 @@ module ActiveModel raise ArgumentError, "Unknown validator: '#{key}'" end - validates_with(validator, (options == true ? {} : options).merge(:attributes => attributes)) + validates_with(validator, defaults.merge(options == true ? {} : options)) end end end -- cgit v1.2.3 From fa14d6d51ed89acde66b49e8d3a6423396c3d553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 8 Jan 2010 08:34:44 +0100 Subject: Compile length validator options still at the class level, so whenever the validator is called, it just needs to check for :maximum, :minimum and :is values. --- .../lib/active_model/validations/exclusion.rb | 1 - .../lib/active_model/validations/inclusion.rb | 1 - activemodel/lib/active_model/validations/length.rb | 66 ++++++++-------------- 3 files changed, 24 insertions(+), 44 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 0a44c6d54f..7ee718cf3c 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -2,7 +2,6 @@ module ActiveModel module Validations class ExclusionValidator < EachValidator def check_validity! - options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 25b8c7866d..0c1334fe1b 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -2,7 +2,6 @@ module ActiveModel module Validations class InclusionValidator < EachValidator def check_validity! - options[:in] ||= options.delete(:within) raise ArgumentError, "An object with the method include? is required must be supplied as the " << ":in option of the configuration hash" unless options[:in].respond_to?(:include?) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index f41ce34328..871f589af9 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,36 +1,43 @@ module ActiveModel module Validations class LengthValidator < EachValidator - OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze DEFAULT_TOKENIZER = lambda { |value| value.split(//) } - attr_reader :type def initialize(options) - @type = (OPTIONS & options.keys).first + if range = (options.delete(:in) || options.delete(:within)) + raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range) + options[:minimum], options[:maximum] = range.begin, range.end + options[:maximum] -= 1 if range.exclude_end? + end + super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER)) end def check_validity! - ensure_one_range_option! - ensure_argument_types! + keys = CHECKS.keys & options.keys + + if keys.empty? + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + end + + keys.each do |key| + value = options[key] + + unless value.is_a?(Integer) && value >= 0 + raise ArgumentError, ":#{key} must be a nonnegative Integer" + end + end end def validate_each(record, attribute, value) - checks = options.slice(:minimum, :maximum, :is) - value = options[:tokenizer].call(value) if value.kind_of?(String) - - if [:within, :in].include?(type) - range = options[type] - checks[:minimum], checks[:maximum] = range.begin, range.end - checks[:maximum] -= 1 if range.exclude_end? - end + value = options[:tokenizer].call(value) if value.kind_of?(String) - checks.each do |key, check_value| + CHECKS.each do |key, validity_check| + next unless check_value = options[key] custom_message = options[:message] || options[MESSAGES[key]] - validity_check = CHECKS[key] valid_value = if key == :maximum value.nil? || value.size.send(validity_check, check_value) @@ -38,33 +45,8 @@ module ActiveModel value && value.size.send(validity_check, check_value) end - record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) unless valid_value - end - end - - protected - - def ensure_one_range_option! #:nodoc: - range_options = OPTIONS & options.keys - - case range_options.size - when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' - when 1 - # Valid number of options; do nothing. - else - raise ArgumentError, 'Too many range options specified. Choose only one.' - end - end - - def ensure_argument_types! #:nodoc: - value = options[type] - - case type - when :within, :in - raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range) - when :is, :minimum, :maximum - raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0 + next if valid_value + record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) end end end -- cgit v1.2.3 From 7045c4c279499eb7340fb420398d613497739eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 8 Jan 2010 08:37:58 +0100 Subject: Allow validates to map some types to specific options. So now you can do: validates :email, :presence => true, :format => /@/ validates :genre, :inclusion => %w(m f) validates :password, :length => 6..20 --- .../lib/active_model/validations/numericality.rb | 7 ++-- .../lib/active_model/validations/validates.rb | 44 ++++++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 9dfc5125cd..adf34bf454 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -10,9 +10,10 @@ module ActiveModel end def check_validity! - options.slice(*CHECKS.keys) do |option, value| - next if [:odd, :even].include?(option) - raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) + keys = CHECKS.keys - [:odd, :event] + options.slice(*keys) do |option, value| + next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) + raise ArgumentError, ":#{option} must be a number, a symbol or a proc" end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 61caf32c06..ea7303c70b 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -7,6 +7,7 @@ module ActiveModel # 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) } @@ -19,6 +20,7 @@ module ActiveModel # # 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 @@ -31,29 +33,32 @@ module ActiveModel # attr_accessor :name, :email # # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } - # validates :email, :presence => true, :format => { :with => /@/ } + # 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 Film + # include ActiveModel::Validations + # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i # end # end - # end # - # class Film - # include ActiveModel::Validations - # include MyValidators - # # validates :name, :title => true - # end + # end + # + # The validators hash can also handle regular expressions, ranges and arrays: + # + # validates :email, :format => /@/ + # validates :genre, :inclusion => %w(mail female) + # validates :password, :length => 6..20 # - # The options :if, :unless, :on, :allow_blank and :allow_nil can be given to one specific - # validator: + # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given + # to one specific validator: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true # @@ -78,7 +83,22 @@ module ActiveModel raise ArgumentError, "Unknown validator: '#{key}'" end - validates_with(validator, defaults.merge(options == true ? {} : options)) + validates_with(validator, defaults.merge(_parse_validates_options(options))) + end + end + + protected + + def _parse_validates_options(options) #:nodoc: + case options + when TrueClass + {} + when Hash + options + when Regexp + { :with => options } + when Range, Array + { :in => options } end end end -- cgit v1.2.3 From 017f5d5308098438da0b8c44163af4ecb422f1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Jan 2010 00:17:16 +0100 Subject: Fix typo by renaming :genre to :gender. --- activemodel/lib/active_model/validations/validates.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index ea7303c70b..80924f50d6 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -54,7 +54,7 @@ module ActiveModel # The validators hash can also handle regular expressions, ranges and arrays: # # validates :email, :format => /@/ - # validates :genre, :inclusion => %w(mail female) + # validates :gender, :inclusion => %w(mail female) # validates :password, :length => 6..20 # # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given -- cgit v1.2.3 From b078f7fd3910b0b174bc951cc8c0d27536b09c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 11 Jan 2010 23:37:48 +0100 Subject: Fix typos and add tests to ensure they will be caught the next time. --- activemodel/lib/active_model/validations/numericality.rb | 4 ++-- activemodel/lib/active_model/validations/validates.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index adf34bf454..c6d84c5312 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -10,8 +10,8 @@ module ActiveModel end def check_validity! - keys = CHECKS.keys - [:odd, :event] - options.slice(*keys) do |option, value| + keys = CHECKS.keys - [:odd, :even] + options.slice(*keys).each do |option, value| next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) raise ArgumentError, ":#{option} must be a number, a symbol or a proc" end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 80924f50d6..4c82a993ae 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -54,7 +54,7 @@ module ActiveModel # The validators hash can also handle regular expressions, ranges and arrays: # # validates :email, :format => /@/ - # validates :gender, :inclusion => %w(mail female) + # validates :gender, :inclusion => %w(male female) # validates :password, :length => 6..20 # # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given -- cgit v1.2.3