Ticket #10876: extract_validations.patch
| File extract_validations.patch, 49.1 kB (added by lifofifo, 2 years ago) |
|---|
-
a/activerecord/lib/active_record/validations.rb
old new 14 14 end 15 15 end 16 16 17 # Active Record validation is reported to and from this object, which is used by Base#save to18 # determine whether the object is in a valid state to be saved. See usage example in Validations.19 class Errors20 include Enumerable21 22 def initialize(base) # :nodoc:23 @base, @errors = base, {}24 end25 26 @@default_error_messages = {27 :inclusion => "is not included in the list",28 :exclusion => "is reserved",29 :invalid => "is invalid",30 :confirmation => "doesn't match confirmation",31 :accepted => "must be accepted",32 :empty => "can't be empty",33 :blank => "can't be blank",34 :too_long => "is too long (maximum is %d characters)",35 :too_short => "is too short (minimum is %d characters)",36 :wrong_length => "is the wrong length (should be %d characters)",37 :taken => "has already been taken",38 :not_a_number => "is not a number",39 :greater_than => "must be greater than %d",40 :greater_than_or_equal_to => "must be greater than or equal to %d",41 :equal_to => "must be equal to %d",42 :less_than => "must be less than %d",43 :less_than_or_equal_to => "must be less than or equal to %d",44 :odd => "must be odd",45 :even => "must be even"46 }47 48 # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.49 cattr_accessor :default_error_messages50 51 52 # Adds an error to the base object instead of any particular attribute. This is used53 # to report errors that don't tie to any specific attribute, but rather to the object54 # as a whole. These error messages don't get prepended with any field name when iterating55 # with each_full, so they should be complete sentences.56 def add_to_base(msg)57 add(:base, msg)58 end59 60 # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>61 # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one62 # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.63 # If no +msg+ is supplied, "invalid" is assumed.64 def add(attribute, msg = @@default_error_messages[:invalid])65 @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?66 @errors[attribute.to_s] << msg67 end68 69 # Will add an error message to each of the attributes in +attributes+ that is empty.70 def add_on_empty(attributes, msg = @@default_error_messages[:empty])71 for attr in [attributes].flatten72 value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]73 is_empty = value.respond_to?("empty?") ? value.empty? : false74 add(attr, msg) unless !value.nil? && !is_empty75 end76 end77 78 # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).79 def add_on_blank(attributes, msg = @@default_error_messages[:blank])80 for attr in [attributes].flatten81 value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]82 add(attr, msg) if value.blank?83 end84 end85 86 # Returns true if the specified +attribute+ has errors associated with it.87 #88 # class Company < ActiveRecord::Base89 # validates_presence_of :name, :address, :email90 # validates_length_of :name, :in => 5..3091 # end92 #93 # company = Company.create(:address => '123 First St.')94 # company.errors.invalid?(:name) # => true95 # company.errors.invalid?(:address) # => false96 def invalid?(attribute)97 !@errors[attribute.to_s].nil?98 end99 100 # Returns nil, if no errors are associated with the specified +attribute+.101 # Returns the error message, if one error is associated with the specified +attribute+.102 # Returns an array of error messages, if more than one error is associated with the specified +attribute+.103 #104 # class Company < ActiveRecord::Base105 # validates_presence_of :name, :address, :email106 # validates_length_of :name, :in => 5..30107 # end108 #109 # company = Company.create(:address => '123 First St.')110 # company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"]111 # company.errors.on(:email) # => "can't be blank"112 # company.errors.on(:address) # => nil113 def on(attribute)114 errors = @errors[attribute.to_s]115 return nil if errors.nil?116 errors.size == 1 ? errors.first : errors117 end118 119 alias :[] :on120 121 # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).122 def on_base123 on(:base)124 end125 126 # Yields each attribute and associated message per error added.127 #128 # class Company < ActiveRecord::Base129 # validates_presence_of :name, :address, :email130 # validates_length_of :name, :in => 5..30131 # end132 #133 # company = Company.create(:address => '123 First St.')134 # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>135 # name - is too short (minimum is 5 characters)136 # name - can't be blank137 # address - can't be blank138 def each139 @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }140 end141 142 # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned143 # through iteration as "First name can't be empty".144 #145 # class Company < ActiveRecord::Base146 # validates_presence_of :name, :address, :email147 # validates_length_of :name, :in => 5..30148 # end149 #150 # company = Company.create(:address => '123 First St.')151 # company.errors.each_full{|msg| puts msg } # =>152 # Name is too short (minimum is 5 characters)153 # Name can't be blank154 # Address can't be blank155 def each_full156 full_messages.each { |msg| yield msg }157 end158 159 # Returns all the full error messages in an array.160 #161 # class Company < ActiveRecord::Base162 # validates_presence_of :name, :address, :email163 # validates_length_of :name, :in => 5..30164 # end165 #166 # company = Company.create(:address => '123 First St.')167 # company.errors.full_messages # =>168 # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]169 def full_messages170 full_messages = []171 172 @errors.each_key do |attr|173 @errors[attr].each do |msg|174 next if msg.nil?175 176 if attr == "base"177 full_messages << msg178 else179 full_messages << @base.class.human_attribute_name(attr) + " " + msg180 end181 end182 end183 full_messages184 end185 186 # Returns true if no errors have been added.187 def empty?188 @errors.empty?189 end190 191 # Removes all errors that have been added.192 def clear193 @errors = {}194 end195 196 # Returns the total number of errors added. Two errors added to the same attribute will be counted as such.197 def size198 @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }199 end200 201 alias_method :count, :size202 alias_method :length, :size203 204 # Return an XML representation of this error object.205 #206 # class Company < ActiveRecord::Base207 # validates_presence_of :name, :address, :email208 # validates_length_of :name, :in => 5..30209 # end210 #211 # company = Company.create(:address => '123 First St.')212 # company.errors.to_xml # =>213 # <?xml version="1.0" encoding="UTF-8"?>214 # <errors>215 # <error>Name is too short (minimum is 5 characters)</error>216 # <error>Name can't be blank</error>217 # <error>Address can't be blank</error>218 # </errors>219 def to_xml(options={})220 options[:root] ||= "errors"221 options[:indent] ||= 2222 options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])223 224 options[:builder].instruct! unless options.delete(:skip_instruct)225 options[:builder].errors do |e|226 full_messages.each { |msg| e.error(msg) }227 end228 end229 end230 231 232 17 # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and 233 18 # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring 234 19 # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). … … 270 55 # 271 56 # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. 272 57 module Validations 273 VALIDATIONS = %w( validate validate_on_create validate_on_update ) 274 58 275 59 def self.included(base) # :nodoc: 60 base.send :include, ActiveSupport::Validations 61 base.default_validation_options[:on] = :save 62 276 63 base.extend ClassMethods 64 277 65 base.class_eval do 278 66 alias_method_chain :save, :validation 279 67 alias_method_chain :save!, :validation 280 68 alias_method_chain :update_attribute, :validation_skipping 281 69 end 282 70 283 base.send :include, ActiveSupport::Callbacks284 285 71 # TODO: Use helper ActiveSupport::Callbacks#define_callbacks instead 286 %w( validate validate_on_create validate_on_update ).each do |validation_method|72 %w( validate_on_create validate_on_update ).each do |validation_method| 287 73 base.class_eval <<-"end_eval" 288 74 def self.#{validation_method}(*methods, &block) 289 75 options = methods.extract_options! … … 304 90 # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use 305 91 # these over the low-level calls to validate and validate_on_create when possible. 306 92 module ClassMethods 307 DEFAULT_VALIDATION_OPTIONS = {308 :on => :save,309 :allow_nil => false,310 :allow_blank => false,311 :message => nil312 }.freeze313 314 ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze315 ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',316 :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',317 :odd => 'odd?', :even => 'even?' }.freeze318 319 93 # Adds a validation method or block to the class. This is useful when 320 94 # overriding the #validate instance method becomes too unwieldly and 321 95 # you're looking for more descriptive declaration of your validations. … … 363 137 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 364 138 # method, proc or string should return or evaluate to a true or false value. 365 139 def validates_each(*attrs) 366 options = attrs.extract_options!.symbolize_keys 367 attrs = attrs.flatten 368 369 # Declare the validation. 370 send(validation_method(options[:on] || :save), options) do |record| 371 attrs.each do |attr| 372 value = record.send(attr) 373 next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 374 yield record, attr, value 375 end 376 end 140 super(*attrs) 377 141 end 378 142 379 143 # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: … … 405 169 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 406 170 # method, proc or string should return or evaluate to a true or false value. 407 171 def validates_confirmation_of(*attr_names) 408 configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } 409 configuration.update(attr_names.extract_options!) 410 411 attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) 412 413 validates_each(attr_names, configuration) do |record, attr_name, value| 414 record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") 415 end 172 super(*attr_names) 416 173 end 417 174 418 175 # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: … … 439 196 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 440 197 # method, proc or string should return or evaluate to a true or false value. 441 198 def validates_acceptance_of(*attr_names) 442 configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } 199 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:accepted], 200 :on => :save, :allow_nil => true, :accept => "1" } 443 201 configuration.update(attr_names.extract_options!) 444 202 445 203 db_cols = begin 446 204 column_names 447 205 rescue ActiveRecord::StatementInvalid … … 450 208 names = attr_names.reject { |name| db_cols.include?(name.to_s) } 451 209 attr_accessor(*names) 452 210 453 validates_each(attr_names, configuration) do |record, attr_name, value|211 validates_each(attr_names, configuration) do |record, attr_name, value| 454 212 record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] 455 213 end 456 214 end … … 489 247 # failures on saves when both the parent object and the child object are 490 248 # new. 491 249 def validates_presence_of(*attr_names) 492 configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } 493 configuration.update(attr_names.extract_options!) 494 495 # can't use validates_each here, because it cannot cope with nonexistent attributes, 496 # while errors.add_on_empty can 497 send(validation_method(configuration[:on]), configuration) do |record| 498 record.errors.add_on_blank(attr_names, configuration[:message]) 499 end 250 super(*attr_names) 500 251 end 501 252 502 253 # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: … … 532 283 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 533 284 # method, proc or string should return or evaluate to a true or false value. 534 285 def validates_length_of(*attrs) 535 # Merge given options with defaults. 536 options = { 537 :too_long => ActiveRecord::Errors.default_error_messages[:too_long], 538 :too_short => ActiveRecord::Errors.default_error_messages[:too_short], 539 :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] 540 }.merge(DEFAULT_VALIDATION_OPTIONS) 541 options.update(attrs.extract_options!.symbolize_keys) 542 543 # Ensure that one and only one range option is specified. 544 range_options = ALL_RANGE_OPTIONS & options.keys 545 case range_options.size 546 when 0 547 raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' 548 when 1 549 # Valid number of options; do nothing. 550 else 551 raise ArgumentError, 'Too many range options specified. Choose only one.' 552 end 553 554 # Get range option and value. 555 option = range_options.first 556 option_value = options[range_options.first] 557 558 case option 559 when :within, :in 560 raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) 561 562 too_short = options[:too_short] % option_value.begin 563 too_long = options[:too_long] % option_value.end 564 565 validates_each(attrs, options) do |record, attr, value| 566 if value.nil? or value.split(//).size < option_value.begin 567 record.errors.add(attr, too_short) 568 elsif value.split(//).size > option_value.end 569 record.errors.add(attr, too_long) 570 end 571 end 572 when :is, :minimum, :maximum 573 raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 574 575 # Declare different validations per option. 576 validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } 577 message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } 578 579 message = (options[:message] || options[message_options[option]]) % option_value 580 581 validates_each(attrs, options) do |record, attr, value| 582 if value.kind_of?(String) 583 record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] 584 else 585 record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] 586 end 587 end 588 end 286 super(*attrs) 589 287 end 590 591 alias_method :validates_size_of, :validates_length_of 592 593 288 289 def validates_size_of(*attrs) 290 super(*attrs) 291 end 292 594 293 # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user 595 294 # can be named "davidhh". 596 295 # … … 625 324 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 626 325 # method, proc or string should return or evaluate to a true or false value. 627 326 def validates_uniqueness_of(*attr_names) 628 configuration = { :message => Active Record::Errors.default_error_messages[:taken], :case_sensitive => true }327 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:taken], :case_sensitive => true } 629 328 configuration.update(attr_names.extract_options!) 630 329 631 validates_each(attr_names, configuration) do |record, attr_name, value|330 validates_each(attr_names, configuration) do |record, attr_name, value| 632 331 if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?) 633 332 condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" 634 333 condition_params = [value] … … 693 392 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 694 393 # method, proc or string should return or evaluate to a true or false value. 695 394 def validates_format_of(*attr_names) 696 configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } 697 configuration.update(attr_names.extract_options!) 698 699 raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) 700 701 validates_each(attr_names, configuration) do |record, attr_name, value| 702 record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] 703 end 395 super(*attr_names) 704 396 end 705 397 706 398 # Validates whether the value of the specified attribute is available in a particular enumerable object. … … 723 415 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 724 416 # method, proc or string should return or evaluate to a true or false value. 725 417 def validates_inclusion_of(*attr_names) 726 configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } 727 configuration.update(attr_names.extract_options!) 728 729 enum = configuration[:in] || configuration[:within] 730 731 raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 732 733 validates_each(attr_names, configuration) do |record, attr_name, value| 734 record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) 735 end 418 super(*attr_names) 736 419 end 737 420 738 421 # Validates that the value of the specified attribute is not in a particular enumerable object. … … 755 438 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 756 439 # method, proc or string should return or evaluate to a true or false value. 757 440 def validates_exclusion_of(*attr_names) 758 configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } 759 configuration.update(attr_names.extract_options!) 760 761 enum = configuration[:in] || configuration[:within] 762 763 raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 764 765 validates_each(attr_names, configuration) do |record, attr_name, value| 766 record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) 767 end 441 super(*attr_names) 768 442 end 769 443 770 444 # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. … … 799 473 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 800 474 # method, proc or string should return or evaluate to a true or false value. 801 475 def validates_associated(*attr_names) 802 configuration = { :message => Active Record::Errors.default_error_messages[:invalid], :on => :save }476 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:invalid], :on => :save } 803 477 configuration.update(attr_names.extract_options!) 804 478 805 479 validates_each(attr_names, configuration) do |record, attr_name, value| 806 480 record.errors.add(attr_name, configuration[:message]) unless 807 481 (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } … … 835 509 # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The 836 510 # method, proc or string should return or evaluate to a true or false value. 837 511 def validates_numericality_of(*attr_names) 838 configuration = { :on => :save, :only_integer => false, :allow_nil => false } 839 configuration.update(attr_names.extract_options!) 840 841 842 numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys 843 844 (numericality_options - [ :odd, :even ]).each do |option| 845 raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) 846 end 847 848 validates_each(attr_names,configuration) do |record, attr_name, value| 849 raw_value = record.send("#{attr_name}_before_type_cast") || value 850 851 next if configuration[:allow_nil] and raw_value.nil? 852 853 if configuration[:only_integer] 854 unless raw_value.to_s =~ /\A[+-]?\d+\Z/ 855 record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) 856 next 857 end 858 raw_value = raw_value.to_i 859 else 860 begin 861 raw_value = Kernel.Float(raw_value.to_s) 862 rescue ArgumentError, TypeError 863 record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) 864 next 865 end 866 end 867 868 numericality_options.each do |option| 869 case option 870 when :odd, :even 871 record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] 872 else 873 record.errors.add(attr_name, configuration[:message] || (ActiveRecord::Errors.default_error_messages[option] % configuration[option])) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] 874 end 875 end 876 end 512 super(*attr_names) 877 513 end 878 514 879 515 # Creates an object just like Base.create but calls save! instead of save … … 889 525 end 890 526 891 527 private 892 def validation_method(o n)893 case o n528 def validation_method(options) 529 case options[:on] || :save 894 530 when :save then :validate 895 531 when :create then :validate_on_create 896 532 when :update then :validate_on_update … … 929 565 # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false. 930 566 def valid? 931 567 errors.clear 932 933 568 run_callbacks(:validate) 934 569 validate 935 570 … … 944 579 errors.empty? 945 580 end 946 581 947 # Returns the Errors object that holds all information about attribute error messages.948 def errors949 @errors ||= Errors.new(self)950 end951 952 582 protected 953 # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. 954 def validate #:doc: 955 end 583 584 # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. 585 def validate #:doc: 586 end 956 587 957 # Overwrite this method for validation checks used only on creation.958 def validate_on_create #:doc:959 end588 # Overwrite this method for validation checks used only on creation. 589 def validate_on_create #:doc: 590 end 960 591 961 # Overwrite this method for validation checks used only on updates.962 def validate_on_update # :doc:963 end592 # Overwrite this method for validation checks used only on updates. 593 def validate_on_update # :doc: 594 end 964 595 end 965 596 end -
a/activerecord/test/cases/validations_test.rb
old new 781 781 end 782 782 783 783 def test_validates_length_with_globally_modified_error_message 784 Active Record::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'784 ActiveSupport::Validations::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' 785 785 Topic.validates_length_of :title, :minimum => 10 786 786 t = Topic.create(:title => 'too short') 787 787 assert !t.valid? -
a/activesupport/lib/active_support.rb
old new 50 50 require 'active_support/multibyte' 51 51 52 52 require 'active_support/base64' 53 require 'active_support/validations' -
/dev/null
old new 1 module ActiveSupport 2 module Validations 3 class Errors 4 include Enumerable 5 6 def initialize(base) # :nodoc: 7 @base, @errors = base, {} 8 end 9 10 @@default_error_messages = { 11 :inclusion => "is not included in the list", 12 :exclusion => "is reserved", 13 :invalid => "is invalid", 14 :confirmation => "doesn't match confirmation", 15 :accepted => "must be accepted", 16 :empty => "can't be empty", 17 :blank => "can't be blank", 18 :too_long => "is too long (maximum is %d characters)", 19 :too_short => "is too short (minimum is %d characters)", 20 :wrong_length => "is the wrong length (should be %d characters)", 21 :taken => "has already been taken", 22 :not_a_number => "is not a number", 23 :greater_than => "must be greater than %d", 24 :greater_than_or_equal_to => "must be greater than or equal to %d", 25 :equal_to => "must be equal to %d", 26 :less_than => "must be less than %d", 27 :less_than_or_equal_to => "must be less than or equal to %d", 28 :odd => "must be odd", 29 :even => "must be even" 30 } 31 32 # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. 33 cattr_accessor :default_error_messages 34 35 # Adds an error to the base object instead of any particular attribute. This is used 36 # to report errors that don't tie to any specific attribute, but rather to the object 37 # as a whole. These error messages don't get prepended with any field name when iterating 38 # with each_full, so they should be complete sentences. 39 def add_to_base(msg) 40 add(:base, msg) 41 end 42 43 # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> 44 # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one 45 # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. 46 # If no +msg+ is supplied, "invalid" is assumed. 47 def add(attribute, msg = @@default_error_messages[:invalid]) 48 @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? 49 @errors[attribute.to_s] << msg 50 end 51 52 # Will add an error message to each of the attributes in +attributes+ that is empty. 53 def add_on_empty(attributes, msg = @@default_error_messages[:empty]) 54 for attr in [attributes].flatten 55 value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 56 is_empty = value.respond_to?("empty?") ? value.empty? : false 57 add(attr, msg) unless !value.nil? && !is_empty 58 end 59 end 60 61 # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). 62 def add_on_blank(attributes, msg = @@default_error_messages[:blank]) 63 for attr in [attributes].flatten 64 value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 65 add(attr, msg) if value.blank? 66 end 67 end 68 69 # Returns true if the specified +attribute+ has errors associated with it. 70 # 71 # class Company < ActiveRecord::Base 72 # validates_presence_of :name, :address, :email 73 # validates_length_of :name, :in => 5..30 74 # end 75 # 76 # company = Company.create(:address => '123 First St.') 77 # company.errors.invalid?(:name) # => true 78 # company.errors.invalid?(:address) # => false 79 def invalid?(attribute) 80 !@errors[attribute.to_s].nil? 81 end 82 83 # Returns nil, if no errors are associated with the specified +attribute+. 84 # Returns the error message, if one error is associated with the specified +attribute+. 85 # Returns an array of error messages, if more than one error is associated with the specified +attribute+. 86 # 87 # class Company < ActiveRecord::Base 88 # validates_presence_of :name, :address, :email 89 # validates_length_of :name, :in => 5..30 90 # end 91 # 92 # company = Company.create(:address => '123 First St.') 93 # company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"] 94 # company.errors.on(:email) # => "can't be blank" 95 # company.errors.on(:address) # => nil 96 def on(attribute) 97 errors = @errors[attribute.to_s] 98 return nil if errors.nil? 99 errors.size == 1 ? errors.first : errors 100 end 101 102 alias :[] :on 103 104 # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute). 105 def on_base 106 on(:base) 107 end 108 109 # Yields each attribute and associated message per error added. 110 # 111 # class Company < ActiveRecord::Base 112 # validates_presence_of :name, :address, :email 113 # validates_length_of :name, :in => 5..30 114 # end 115 # 116 # company = Company.create(:address => '123 First St.') 117 # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # => 118 # name - is too short (minimum is 5 characters) 119 # name - can't be blank 120 # address - can't be blank 121 def each 122 @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } 123 end 124 125 # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned 126 # through iteration as "First name can't be empty". 127 # 128 # class Company < ActiveRecord::Base 129 # validates_presence_of :name, :address, :email 130 # validates_length_of :name, :in => 5..30 131 # end 132 # 133 # company = Company.create(:address => '123 First St.') 134 # company.errors.each_full{|msg| puts msg } # => 135 # Name is too short (minimum is 5 characters) 136 # Name can't be blank 137 # Address can't be blank 138 def each_full 139 full_messages.each { |msg| yield msg } 140 end 141 142 # Returns all the full error messages in an array. 143 # 144 # class Company < ActiveRecord::Base 145 # validates_presence_of :name, :address, :email 146 # validates_length_of :name, :in => 5..30 147 # end 148 # 149 # company = Company.create(:address => '123 First St.') 150 # company.errors.full_messages # => 151 # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] 152 def full_messages 153 full_messages = [] 154 155 @errors.each_key do |attr| 156 @errors[attr].each do |msg| 157 next if msg.nil? 158 159 if attr == "base" 160 full_messages << msg 161 else 162 if @base.class.respond_to?(:human_attribute_name) 163 full_messages << @base.class.human_attribute_name(attr) + " " + msg 164 else 165 full_messages << attr.humanize + " " + msg 166 end 167 end 168 end 169 end 170 full_messages 171 end 172 173 # Returns true if no errors have been added. 174 def empty? 175 @errors.empty? 176 end 177 178 # Removes all errors that have been added. 179 def clear 180 @errors = {} 181 end 182 183 # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. 184 def size 185 @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } 186 end 187 188 alias_method :count, :size 189 alias_method :length, :size 190 191 # Return an XML representation of this error object. 192 # 193 # class Company < ActiveRecord::Base 194 # validates_presence_of :name, :address, :email 195 # validates_length_of :name, :in => 5..30 196 # end 197 # 198 # company = Company.create(:address => '123 First St.') 199 # company.errors.to_xml # => 200 # <?xml version="1.0" encoding="UTF-8"?> 201 # <errors> 202 # <error>Name is too short (minimum is 5 characters)</error> 203 # <error>Name can't be blank</error> 204 # <error>Address can't be blank</error> 205 # </errors> 206 def to_xml(options={}) 207 options[:root] ||= "errors" 208 options[:indent] ||= 2 209 options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) 210 211 options[:builder].instruct! unless options.delete(:skip_instruct) 212 options[:builder].errors do |e| 213 full_messages.each { |msg| e.error(msg) } 214 end 215 end 216 end 217 218 def self.included(base) 219 base.send :include, ActiveSupport::Callbacks 220 base.cattr_accessor :default_validation_options 221 base.default_validation_options = {} 222 223 base.extend ClassMethods 224 end 225 226 # All of the following validations are defined in the class scope of the model that you're interested in validating. 227 # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use 228 # these over the low-level calls to validate and validate_on_create when possible. 229 module ClassMethods 230 ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze 231 ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', 232 :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', 233 :odd => 'odd?', :even => 'even?' }.freeze 234 235 def validate(*methods, &block) 236 options = methods.extract_options! 237 methods << block if block_given? 238 methods.map! { |method| ActiveSupport::Callbacks::Callback.new(:validate, method, options) } 239 existing_methods = read_inheritable_attribute(:validate) || [] 240 write_inheritable_attribute(:validate, existing_methods | methods) 241 end 242 243 def validate_callback_chain 244 read_inheritable_attribute(:validate) || [] 245 end 246 247 def validates_each(*attrs) 248 options = attrs.extract_options!.symbolize_keys.reverse_merge(self.default_validation_options) 249 attrs = attrs.flatten 250 251 # Declare the validation. 252 send(validation_method(options), options) do |record| 253 attrs.each do |attr| 254 value = record.send(attr) 255 next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 256 yield record, attr, value 257 end 258 end 259 end 260 261 def validates_confirmation_of(*attr_names) 262 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:confirmation] } 263 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 264 265 attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) 266 267 validates_each(attr_names, configuration) do |record, attr_name, value| 268 record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") 269 end 270 end 271 272 def validates_acceptance_of(*attr_names) 273 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:accepted], :allow_nil => true, :accept => "1" } 274 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 275 276 attr_accessor(*attr_names) 277 278 validates_each(attr_names, configuration) do |record, attr_name, value| 279 record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] 280 end 281 end 282 283 def validates_presence_of(*attr_names) 284 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:blank] } 285 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 286 287 # can't use validates_each here, because it cannot cope with nonexistent attributes, 288 # while errors.add_on_empty can 289 send(validation_method(configuration), configuration) do |record| 290 record.errors.add_on_blank(attr_names, configuration[:message]) 291 end 292 end 293 294 def validates_length_of(*attrs) 295 # Merge given options with defaults. 296 options = { 297 :too_long => ActiveSupport::Validations::Errors.default_error_messages[:too_long], 298 :too_short => ActiveSupport::Validations::Errors.default_error_messages[:too_short], 299 :wrong_length => ActiveSupport::Validations::Errors.default_error_messages[:wrong_length] 300 }.merge({ :allow_nil => false, :allow_blank => false, :message => nil }) 301 302 options.update(attrs.extract_options!.symbolize_keys).reverse_merge!(self.default_validation_options) 303 304 # Ensure that one and only one range option is specified. 305 range_options = ALL_RANGE_OPTIONS & options.keys 306 case range_options.size 307 when 0 308 raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' 309 when 1 310 # Valid number of options; do nothing. 311 else 312 raise ArgumentError, 'Too many range options specified. Choose only one.' 313 end 314 315 # Get range option and value. 316 option = range_options.first 317 option_value = options[range_options.first] 318 319 case option 320 when :within, :in 321 raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) 322 323 too_short = options[:too_short] % option_value.begin 324 too_long = options[:too_long] % option_value.end 325 326 validates_each(attrs, options) do |record, attr, value| 327 if value.nil? or value.split(//).size < option_value.begin 328 record.errors.add(attr, too_short) 329 elsif value.split(//).size > option_value.end 330 record.errors.add(attr, too_long) 331 end 332 end 333 when :is, :minimum, :maximum 334 raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 335 336 # Declare different validations per option. 337 validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } 338 message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } 339 340 message = (options[:message] || options[message_options[option]]) % option_value 341 342 validates_each(attrs, options) do |record, attr, value| 343 if value.kind_of?(String) 344 record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] 345 else 346 record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] 347 end 348 end 349 end 350 end 351 352 def validates_size_of(*attr_names) 353 validates_length_of(*attr_names) 354 end 355 356 def validates_format_of(*attr_names) 357 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:invalid], :with => nil } 358 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 359 360 unless configuration[:with].is_a?(Regexp) 361 raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") 362 end 363 364 validates_each(attr_names, configuration) do |record, attr_name, value| 365 record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] 366 end 367 end 368 369 def validates_inclusion_of(*attr_names) 370 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:inclusion] } 371 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 372 373 enum = configuration[:in] || configuration[:within] 374 375 raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 376 377 validates_each(attr_names, configuration) do |record, attr_name, value| 378 record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) 379 end 380 end 381 382 def validates_exclusion_of(*attr_names) 383 configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:exclusion] } 384 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 385 386 enum = configuration[:in] || configuration[:within] 387 388 unless enum.respond_to?("include?") 389 raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") 390 end 391 392 validates_each(attr_names, configuration) do |record, attr_name, value| 393 record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) 394 end 395 end 396 397 def validates_numericality_of(*attr_names) 398 configuration = { :only_integer => false, :allow_nil => false } 399 configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 400 401 numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys 402 403 (numericality_options - [ :odd, :even ]).each do |option| 404 raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) 405 end 406 407 validates_each(attr_names, configuration) do |record, attr_name, value| 408 raw_value = nil 409 if record.respond_to?("#{attr_name}_before_type_cast") 410 raw_value = record.send("#{attr_name}_before_type_cast") 411 end 412 raw_value ||= value 413 414 next if configuration[:allow_nil] and raw_value.nil? 415 416 if configuration[:only_integer] 417 unless raw_value.to_s =~ /\A[+-]?\d+\Z/ 418 record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[:not_a_number]) 419 next 420 end 421 raw_value = raw_value.to_i 422 else 423 begin 424 raw_value = Kernel.Float(raw_value.to_s) 425 rescue ArgumentError, TypeError 426 record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[:not_a_number]) 427 next 428 end 429 end 430 431 numericality_options.each do |option| 432 case option 433 when :odd, :even 434 unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] 435 record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[option]) 436 end 437 else 438 unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] 439 record.errors.add(attr_name, configuration[:message] || (ActiveSupport::Validations::Errors.default_error_messages[option] % configuration[option])) 440 end 441 end 442 end 443 end 444 end 445 446 private 447 def validation_method(configuration) 448 :validate 449 end 450 end 451 452 # Runs validate and returns true if no errors were added otherwise false. 453 def is_valid? 454 errors.clear 455 run_callbacks(:validate) 456 validate 457 errors.empty? 458 end 459 460 # Returns the Errors object that holds all information about attribute error messages. 461 def errors 462 @errors ||= ActiveSupport::Validations::Errors.new(self) 463 end 464 465 protected 466 467 def validate 468 end 469 470 end 471 end -
/dev/null
old new 1 require 'abstract_unit' 2 3 class Car 4 include ActiveSupport::Validations 5 attr_accessor :name, :age 6 7 validates_presence_of :name 8 end 9 10 class Ferrari < Car 11 attr_accessor :speed 12 13 validates_numericality_of :speed 14 validates_presence_of :name, :message => "missing name" 15 end 16 17 class ValidationsTest < Test::Unit::TestCase 18 19 def setup 20 @car = Car.new 21 @ferrari = Ferrari.new 22 end 23 24 def test_validates_presence_of 25 assert !@car.is_valid? 26 assert_equal "can't be blank", @car.errors[:name] 27 28 @car.name = "Zen" 29 assert @car.is_valid? 30 assert @car.errors.empty? 31 end 32 33 def test_validates_numericality_of 34 assert !@ferrari.is_valid? 35 36 assert_equal "is not a number", @ferrari.errors[:speed] 37 assert_equal ["can't be blank", "missing name"], @ferrari.errors[:name] 38 39 @ferrari.name = "Vrummmm" 40 @ferrari.speed = 200 41 42 assert @ferrari.is_valid? 43 end 44 45 end