diff options
Diffstat (limited to 'activemodel/lib/active_model')
17 files changed, 324 insertions, 178 deletions
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" 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 <tt>ActiveRecord::Base.include_root_in_json</tt> controls the - # top-level behavior of to_json. In a new Rails application, it is set to - # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>, + # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the + # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>, # 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} } 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) 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 diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d5460a58bd..276472ea46 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: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, + # other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_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,22 @@ 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 + # + alias :read_attribute_for_validation :send 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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - options = 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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_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..7ee718cf3c 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -33,9 +33,7 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_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..0c1334fe1b 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -33,9 +33,7 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_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 5db2060fe5..9ceb75487f 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 @@ -107,8 +89,7 @@ module ActiveModel # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> 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..c6d84c5312 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, :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 end @@ -103,8 +104,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..4c82a993ae --- /dev/null +++ b/activemodel/lib/active_model/validations/validates.rb @@ -0,0 +1,106 @@ +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. + # + # 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 + # + # validates :name, :title => true + # end + # + # The validators hash can also handle regular expressions, ranges and arrays: + # + # validates :email, :format => /@/ + # 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 + # 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) + 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") + rescue NameError + raise ArgumentError, "Unknown validator: '#{key}'" + end + + 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 + 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: - # * <tt>on</tt> - Specifies when this validation is active (<tt>:create</tt> or <tt>:update</tt> - # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # * <tt>on</tt> - Specifies when this validation is active + # (<tt>:create</tt> or <tt>:update</tt> + # * <tt>if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). + # * <tt>unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur + # (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # - # If you pass any additional configuration options, they will be passed to the class and available as <tt>options</tt>: + # If you pass any additional configuration options, they will be passed + # to the class and available as <tt>options</tt>: # - # 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 <tt>record</tt>, - # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>. + # Any class that inherits from ActiveModel::Validator must implement a method + # called <tt>validate</tt> which accepts a <tt>record</tt>. # - # 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 <tt>:attributes</tt> 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 |