Skip to content
This repository
Browse code

Merge branch 'master' of github.com:rails/rails

  • Loading branch information...
commit 643862e3be1bbe004e2c1a00286b12c5bdc9849a 2 parents 9abbe9f + 078ea0d
Yehuda Katz authored

Showing 75 changed files with 1,652 additions and 1,126 deletions. Show diff stats Hide diff stats

  1. +1 0  Gemfile
  2. +8 1 Rakefile
  3. +4 3 activemodel/lib/active_model.rb
  4. +22 4 activemodel/lib/active_model/naming.rb
  5. +1 21 activemodel/lib/active_model/translation.rb
  6. +23 33 activemodel/lib/active_model/validations.rb
  7. +14 7 activemodel/lib/active_model/validations/acceptance.rb
  8. +11 9 activemodel/lib/active_model/validations/confirmation.rb
  9. +15 11 activemodel/lib/active_model/validations/exclusion.rb
  10. +15 13 activemodel/lib/active_model/validations/format.rb
  11. +15 11 activemodel/lib/active_model/validations/inclusion.rb
  12. +72 57 activemodel/lib/active_model/validations/length.rb
  13. +64 58 activemodel/lib/active_model/validations/numericality.rb
  14. +8 7 activemodel/lib/active_model/validations/presence.rb
  15. +3 8 activemodel/lib/active_model/validations/with.rb
  16. +48 7 activemodel/lib/active_model/validator.rb
  17. +2 1  activemodel/test/cases/naming_test.rb
  18. +15 21 activemodel/test/cases/translation_test.rb
  19. +12 21 activemodel/test/cases/validations/acceptance_validation_test.rb
  20. +3 2 activemodel/test/cases/validations/conditional_validation_test.rb
  21. +12 22 activemodel/test/cases/validations/confirmation_validation_test.rb
  22. +12 11 activemodel/test/cases/validations/exclusion_validation_test.rb
  23. +12 21 activemodel/test/cases/validations/format_validation_test.rb
  24. +0 1  activemodel/test/cases/validations/i18n_validation_test.rb
  25. +12 21 activemodel/test/cases/validations/inclusion_validation_test.rb
  26. +12 41 activemodel/test/cases/validations/length_validation_test.rb
  27. +12 29 activemodel/test/cases/validations/numericality_validation_test.rb
  28. +26 28 activemodel/test/cases/validations/presence_validation_test.rb
  29. +10 9 activemodel/test/cases/validations/with_validation_test.rb
  30. +3 2 activemodel/test/cases/validations_test.rb
  31. +5 1 activemodel/test/models/person.rb
  32. +4 0 activemodel/test/models/track_back.rb
  33. +23 0 activerecord/CHANGELOG
  34. +1 0  activerecord/lib/active_record.rb
  35. +11 10 activerecord/lib/active_record/associations.rb
  36. +2 2 activerecord/lib/active_record/associations/association_collection.rb
  37. +34 10 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
  38. +2 2 activerecord/lib/active_record/associations/has_one_association.rb
  39. +26 14 activerecord/lib/active_record/autosave_association.rb
  40. +10 27 activerecord/lib/active_record/base.rb
  41. +57 143 activerecord/lib/active_record/calculations.rb
  42. +0 12 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
  43. +0 6 activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
  44. +26 22 activerecord/lib/active_record/nested_attributes.rb
  45. +14 4 activerecord/lib/active_record/reflection.rb
  46. +42 10 activerecord/lib/active_record/relation.rb
  47. +169 0 activerecord/lib/active_record/relational_calculations.rb
  48. +9 7 activerecord/lib/active_record/validations/associated.rb
  49. +76 58 activerecord/lib/active_record/validations/uniqueness.rb
  50. +4 4 activerecord/test/cases/associations/eager_test.rb
  51. +15 15 activerecord/test/cases/associations/inner_join_association_test.rb
  52. +273 19 activerecord/test/cases/associations/inverse_associations_test.rb
  53. +29 0 activerecord/test/cases/autosave_association_test.rb
  54. +9 9 activerecord/test/cases/base_test.rb
  55. +9 11 activerecord/test/cases/calculations_test.rb
  56. +2 2 activerecord/test/cases/finder_test.rb
  57. +2 6 activerecord/test/cases/helper.rb
  58. +1 1  activerecord/test/cases/locking_test.rb
  59. +87 87 activerecord/test/cases/method_scoping_test.rb
  60. +51 0 activerecord/test/cases/nested_attributes_test.rb
  61. +21 20 activerecord/test/cases/readonly_test.rb
  62. +35 0 activerecord/test/cases/relations_test.rb
  63. +1 1  activerecord/test/cases/validations/uniqueness_validation_test.rb
  64. +1 1  {activemodel/lib/active_model → activerecord/test/cases}/validations_repair_helper.rb
  65. +3 3 activerecord/test/cases/validations_test.rb
  66. +4 0 activerecord/test/fixtures/faces.yml
  67. +5 1 activerecord/test/fixtures/interests.yml
  68. +3 1 activerecord/test/models/face.rb
  69. +1 0  activerecord/test/models/interest.rb
  70. +2 0  activerecord/test/models/man.rb
  71. +2 0  activerecord/test/models/pirate.rb
  72. +2 0  activerecord/test/models/ship.rb
  73. +4 0 activerecord/test/schema/schema.rb
  74. +9 40 railties/test/initializer/check_ruby_version_test.rb
  75. +99 97 railties/test/initializer/path_test.rb
1  Gemfile
... ... @@ -1,5 +1,6 @@
1 1 gem "rake", ">= 0.8.7"
2 2 gem "mocha", ">= 0.9.8"
  3 +gem "ruby-debug", ">= 0.10.3" if RUBY_VERSION < '1.9'
3 4
4 5 gem "rails", "3.0.pre", :path => "railties"
5 6 %w(activesupport activemodel actionpack actionmailer activerecord activeresource).each do |lib|
9 Rakefile
@@ -24,8 +24,15 @@ task :default => %w(test test:isolated)
24 24 end
25 25 end
26 26
27   -spec = eval(File.read('rails.gemspec'))
  27 +desc "Smoke-test all projects"
  28 +task :smoke do
  29 + (PROJECTS - %w(activerecord)).each do |project|
  30 + system %(cd #{project} && #{env} #{$0} test:isolated)
  31 + end
  32 + system %(cd activerecord && #{env} #{$0} sqlite3:isolated_test)
  33 +end
28 34
  35 +spec = eval(File.read('rails.gemspec'))
29 36 Rake::GemPackageTask.new(spec) do |pkg|
30 37 pkg.gem_spec = spec
31 38 end
7 activemodel/lib/active_model.rb
@@ -35,16 +35,17 @@ module ActiveModel
35 35 autoload :Dirty
36 36 autoload :Errors
37 37 autoload :Lint
38   - autoload :Name, 'active_model/naming'
  38 + autoload :Name, 'active_model/naming'
39 39 autoload :Naming
40   - autoload :Observer, 'active_model/observing'
  40 + autoload :Observer, 'active_model/observing'
41 41 autoload :Observing
42 42 autoload :Serialization
43 43 autoload :StateMachine
44 44 autoload :Translation
45 45 autoload :Validations
46   - autoload :ValidationsRepairHelper
47 46 autoload :Validator
  47 + autoload :EachValidator, 'active_model/validator'
  48 + autoload :BlockValidator, 'active_model/validator'
48 49 autoload :VERSION
49 50
50 51 module Serializers
26 activemodel/lib/active_model/naming.rb
@@ -2,11 +2,11 @@
2 2
3 3 module ActiveModel
4 4 class Name < String
5   - attr_reader :singular, :plural, :element, :collection, :partial_path, :human
  5 + attr_reader :singular, :plural, :element, :collection, :partial_path
6 6 alias_method :cache_key, :collection
7 7
8   - def initialize(klass, name)
9   - super(name)
  8 + def initialize(klass)
  9 + super(klass.name)
10 10 @klass = klass
11 11 @singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
12 12 @plural = ActiveSupport::Inflector.pluralize(@singular).freeze
@@ -15,13 +15,31 @@ def initialize(klass, name)
15 15 @collection = ActiveSupport::Inflector.tableize(self).freeze
16 16 @partial_path = "#{@collection}/#{@element}".freeze
17 17 end
  18 +
  19 + # Transform the model name into a more humane format, using I18n. By default,
  20 + # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").
  21 + # Specify +options+ with additional translating options.
  22 + def human(options={})
  23 + return @human unless @klass.respond_to?(:lookup_ancestors) &&
  24 + @klass.respond_to?(:i18n_scope)
  25 +
  26 + defaults = @klass.lookup_ancestors.map do |klass|
  27 + klass.model_name.underscore.to_sym
  28 + end
  29 +
  30 + defaults << options.delete(:default) if options[:default]
  31 + defaults << @human
  32 +
  33 + options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
  34 + I18n.translate(defaults.shift, options)
  35 + end
18 36 end
19 37
20 38 module Naming
21 39 # Returns an ActiveModel::Name object for module. It can be
22 40 # used to retrieve all kinds of naming-related information.
23 41 def model_name
24   - @_model_name ||= ActiveModel::Name.new(self, name)
  42 + @_model_name ||= ActiveModel::Name.new(self)
25 43 end
26 44 end
27 45 end
22 activemodel/lib/active_model/translation.rb
@@ -37,28 +37,8 @@ def human_attribute_name(attribute, options = {})
37 37
38 38 # Model.human_name is deprecated. Use Model.model_name.human instead.
39 39 def human_name(*args)
40   - ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,1])
  40 + ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5])
41 41 model_name.human(*args)
42 42 end
43 43 end
44   -
45   - class Name < String
46   - # Transform the model name into a more humane format, using I18n. By default,
47   - # it will underscore then humanize the class name (BlogPost.human_name #=> "Blog post").
48   - # Specify +options+ with additional translating options.
49   - def human(options={})
50   - return @human unless @klass.respond_to?(:lookup_ancestors) &&
51   - @klass.respond_to?(:i18n_scope)
52   -
53   - defaults = @klass.lookup_ancestors.map do |klass|
54   - klass.model_name.underscore.to_sym
55   - end
56   -
57   - defaults << options.delete(:default) if options[:default]
58   - defaults << @human
59   -
60   - options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
61   - I18n.translate(defaults.shift, options)
62   - end
63   - end
64 44 end
56 activemodel/lib/active_model/validations.rb
@@ -13,6 +13,29 @@ module Validations
13 13 end
14 14
15 15 module ClassMethods
  16 + # Validates each attribute against a block.
  17 + #
  18 + # class Person < ActiveRecord::Base
  19 + # validates_each :first_name, :last_name do |record, attr, value|
  20 + # record.errors.add attr, 'starts with z.' if value[0] == ?z
  21 + # end
  22 + # end
  23 + #
  24 + # Options:
  25 + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
  26 + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
  27 + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
  28 + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
  29 + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
  30 + # method, proc or string should return or evaluate to a true or false value.
  31 + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
  32 + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
  33 + # method, proc or string should return or evaluate to a true or false value.
  34 + def validates_each(*attr_names, &block)
  35 + options = attr_names.extract_options!.symbolize_keys
  36 + validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block
  37 + end
  38 +
16 39 # Adds a validation method or block to the class. This is useful when
17 40 # overriding the +validate+ instance method becomes too unwieldly and
18 41 # you're looking for more descriptive declaration of your validations.
@@ -40,39 +63,6 @@ module ClassMethods
40 63 # end
41 64 #
42 65 # This usage applies to +validate_on_create+ and +validate_on_update as well+.
43   -
44   - # Validates each attribute against a block.
45   - #
46   - # class Person < ActiveRecord::Base
47   - # validates_each :first_name, :last_name do |record, attr, value|
48   - # record.errors.add attr, 'starts with z.' if value[0] == ?z
49   - # end
50   - # end
51   - #
52   - # Options:
53   - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
54   - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
55   - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
56   - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
57   - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
58   - # method, proc or string should return or evaluate to a true or false value.
59   - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
60   - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
61   - # method, proc or string should return or evaluate to a true or false value.
62   - def validates_each(*attrs)
63   - options = attrs.extract_options!.symbolize_keys
64   - attrs = attrs.flatten
65   -
66   - # Declare the validation.
67   - validate options do |record|
68   - attrs.each do |attr|
69   - value = record.send(:read_attribute_for_validation, attr)
70   - next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
71   - yield record, attr, value
72   - end
73   - end
74   - end
75   -
76 66 def validate(*args, &block)
77 67 options = args.last
78 68 if options.is_a?(Hash) && options.key?(:on)
21 activemodel/lib/active_model/validations/acceptance.rb
... ... @@ -1,5 +1,17 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class AcceptanceValidator < EachValidator
  4 + def initialize(options)
  5 + super(options.reverse_merge(:allow_nil => true, :accept => "1"))
  6 + end
  7 +
  8 + def validate_each(record, attribute, value)
  9 + unless value == options[:accept]
  10 + record.errors.add(attribute, :accepted, :default => options[:message])
  11 + end
  12 + end
  13 + end
  14 +
3 15 module ClassMethods
4 16 # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
5 17 #
@@ -25,8 +37,7 @@ module ClassMethods
25 37 # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
26 38 # method, proc or string should return or evaluate to a true or false value.
27 39 def validates_acceptance_of(*attr_names)
28   - configuration = { :allow_nil => true, :accept => "1" }
29   - configuration.update(attr_names.extract_options!)
  40 + options = attr_names.extract_options!
30 41
31 42 db_cols = begin
32 43 column_names
@@ -37,11 +48,7 @@ def validates_acceptance_of(*attr_names)
37 48 names = attr_names.reject { |name| db_cols.include?(name.to_s) }
38 49 attr_accessor(*names)
39 50
40   - validates_each(attr_names,configuration) do |record, attr_name, value|
41   - unless value == configuration[:accept]
42   - record.errors.add(attr_name, :accepted, :default => configuration[:message])
43   - end
44   - end
  51 + validates_with AcceptanceValidator, options.merge(:attributes => attr_names)
45 52 end
46 53 end
47 54 end
20 activemodel/lib/active_model/validations/confirmation.rb
... ... @@ -1,5 +1,13 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class ConfirmationValidator < EachValidator
  4 + def validate_each(record, attribute, value)
  5 + confirmed = record.send(:"#{attribute}_confirmation")
  6 + return if confirmed.nil? || value == confirmed
  7 + record.errors.add(attribute, :confirmation, :default => options[:message])
  8 + end
  9 + end
  10 +
3 11 module ClassMethods
4 12 # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
5 13 #
@@ -30,15 +38,9 @@ module ClassMethods
30 38 # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
31 39 # method, proc or string should return or evaluate to a true or false value.
32 40 def validates_confirmation_of(*attr_names)
33   - configuration = attr_names.extract_options!
34   -
35   - attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
36   -
37   - validates_each(attr_names, configuration) do |record, attr_name, value|
38   - unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
39   - record.errors.add(attr_name, :confirmation, :default => configuration[:message])
40   - end
41   - end
  41 + options = attr_names.extract_options!
  42 + attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" }))
  43 + validates_with ConfirmationValidator, options.merge(:attributes => attr_names)
42 44 end
43 45 end
44 46 end
26 activemodel/lib/active_model/validations/exclusion.rb
... ... @@ -1,5 +1,17 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class ExclusionValidator < EachValidator
  4 + def check_validity!
  5 + raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
  6 + ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
  7 + end
  8 +
  9 + def validate_each(record, attribute, value)
  10 + return unless options[:in].include?(value)
  11 + record.errors.add(attribute, :exclusion, :default => options[:message], :value => value)
  12 + end
  13 + end
  14 +
3 15 module ClassMethods
4 16 # Validates that the value of the specified attribute is not in a particular enumerable object.
5 17 #
@@ -21,17 +33,9 @@ module ClassMethods
21 33 # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
22 34 # method, proc or string should return or evaluate to a true or false value.
23 35 def validates_exclusion_of(*attr_names)
24   - configuration = attr_names.extract_options!
25   -
26   - enum = configuration[:in] || configuration[:within]
27   -
28   - 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?)
29   -
30   - validates_each(attr_names, configuration) do |record, attr_name, value|
31   - if enum.include?(value)
32   - record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
33   - end
34   - end
  36 + options = attr_names.extract_options!
  37 + options[:in] ||= options.delete(:within)
  38 + validates_with ExclusionValidator, options.merge(:attributes => attr_names)
35 39 end
36 40 end
37 41 end
28 activemodel/lib/active_model/validations/format.rb
... ... @@ -1,5 +1,15 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class FormatValidator < EachValidator
  4 + def validate_each(record, attribute, value)
  5 + if options[:with] && value.to_s !~ options[:with]
  6 + record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
  7 + elsif options[:without] && value.to_s =~ options[:without]
  8 + record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
  9 + end
  10 + end
  11 + end
  12 +
3 13 module ClassMethods
4 14 # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
5 15 # You can require that the attribute matches the regular expression:
@@ -33,29 +43,21 @@ module ClassMethods
33 43 # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
34 44 # method, proc or string should return or evaluate to a true or false value.
35 45 def validates_format_of(*attr_names)
36   - configuration = attr_names.extract_options!
  46 + options = attr_names.extract_options!
37 47
38   - unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
  48 + unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
39 49 raise ArgumentError, "Either :with or :without must be supplied (but not both)"
40 50 end
41 51
42   - if configuration[:with] && !configuration[:with].is_a?(Regexp)
  52 + if options[:with] && !options[:with].is_a?(Regexp)
43 53 raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
44 54 end
45 55
46   - if configuration[:without] && !configuration[:without].is_a?(Regexp)
  56 + if options[:without] && !options[:without].is_a?(Regexp)
47 57 raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
48 58 end
49 59
50   - if configuration[:with]
51   - validates_each(attr_names, configuration) do |record, attr_name, value|
52   - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with]
53   - end
54   - elsif configuration[:without]
55   - validates_each(attr_names, configuration) do |record, attr_name, value|
56   - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without]
57   - end
58   - end
  60 + validates_with FormatValidator, options.merge(:attributes => attr_names)
59 61 end
60 62 end
61 63 end
26 activemodel/lib/active_model/validations/inclusion.rb
... ... @@ -1,5 +1,17 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class InclusionValidator < EachValidator
  4 + def check_validity!
  5 + raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
  6 + ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
  7 + end
  8 +
  9 + def validate_each(record, attribute, value)
  10 + return if options[:in].include?(value)
  11 + record.errors.add(attribute, :inclusion, :default => options[:message], :value => value)
  12 + end
  13 + end
  14 +
3 15 module ClassMethods
4 16 # Validates whether the value of the specified attribute is available in a particular enumerable object.
5 17 #
@@ -21,17 +33,9 @@ module ClassMethods
21 33 # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
22 34 # method, proc or string should return or evaluate to a true or false value.
23 35 def validates_inclusion_of(*attr_names)
24   - configuration = attr_names.extract_options!
25   -
26   - enum = configuration[:in] || configuration[:within]
27   -
28   - 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?)
29   -
30   - validates_each(attr_names, configuration) do |record, attr_name, value|
31   - unless enum.include?(value)
32   - record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
33   - end
34   - end
  36 + options = attr_names.extract_options!
  37 + options[:in] ||= options.delete(:within)
  38 + validates_with InclusionValidator, options.merge(:attributes => attr_names)
35 39 end
36 40 end
37 41 end
129 activemodel/lib/active_model/validations/length.rb
... ... @@ -1,7 +1,75 @@
1 1 module ActiveModel
2 2 module Validations
  3 + class LengthValidator < EachValidator
  4 + OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
  5 + MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
  6 + CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
  7 +
  8 + DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
  9 + attr_reader :type
  10 +
  11 + def initialize(options)
  12 + @type = (OPTIONS & options.keys).first
  13 + super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER))
  14 + end
  15 +
  16 + def check_validity!
  17 + ensure_one_range_option!
  18 + ensure_argument_types!
  19 + end
  20 +
  21 + def validate_each(record, attribute, value)
  22 + checks = options.slice(:minimum, :maximum, :is)
  23 + value = options[:tokenizer].call(value) if value.kind_of?(String)
  24 +
  25 + if [:within, :in].include?(type)
  26 + range = options[type]
  27 + checks[:minimum], checks[:maximum] = range.begin, range.end
  28 + checks[:maximum] -= 1 if range.exclude_end?
  29 + end
  30 +
  31 + checks.each do |key, check_value|
  32 + custom_message = options[:message] || options[MESSAGES[key]]
  33 + validity_check = CHECKS[key]
  34 +
  35 + valid_value = if key == :maximum
  36 + value.nil? || value.size.send(validity_check, check_value)
  37 + else
  38 + value && value.size.send(validity_check, check_value)
  39 + end
  40 +
  41 + record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) unless valid_value
  42 + end
  43 + end
  44 +
  45 + protected
  46 +
  47 + def ensure_one_range_option! #:nodoc:
  48 + range_options = OPTIONS & options.keys
  49 +
  50 + case range_options.size
  51 + when 0
  52 + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
  53 + when 1
  54 + # Valid number of options; do nothing.
  55 + else
  56 + raise ArgumentError, 'Too many range options specified. Choose only one.'
  57 + end
  58 + end
  59 +
  60 + def ensure_argument_types! #:nodoc:
  61 + value = options[type]
  62 +
  63 + case type
  64 + when :within, :in
  65 + raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range)
  66 + when :is, :minimum, :maximum
  67 + raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0
  68 + end
  69 + end
  70 + end
  71 +
3 72 module ClassMethods
4   - ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
5 73
6 74 # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
7 75 #
@@ -38,62 +106,9 @@ module ClassMethods
38 106 # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
39 107 # count words as in above example.)
40 108 # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
41   - def validates_length_of(*attrs)
42   - # Merge given options with defaults.
43   - options = { :tokenizer => lambda {|value| value.split(//)} }
44   - options.update(attrs.extract_options!.symbolize_keys)
45   -
46   - # Ensure that one and only one range option is specified.
47   - range_options = ALL_RANGE_OPTIONS & options.keys
48   - case range_options.size
49   - when 0
50   - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
51   - when 1
52   - # Valid number of options; do nothing.
53   - else
54   - raise ArgumentError, 'Too many range options specified. Choose only one.'
55   - end
56   -
57   - # Get range option and value.
58   - option = range_options.first
59   - option_value = options[range_options.first]
60   - key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
61   - custom_message = options[:message] || options[key]
62   -
63   - case option
64   - when :within, :in
65   - raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
66   -
67   - validates_each(attrs, options) do |record, attr, value|
68   - value = options[:tokenizer].call(value) if value.kind_of?(String)
69   -
70   - min, max = option_value.begin, option_value.end
71   - max = max - 1 if option_value.exclude_end?
72   -
73   - if value.nil? || value.size < min
74   - record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => min)
75   - elsif value.size > max
76   - record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => max)
77   - end
78   - end
79   - when :is, :minimum, :maximum
80   - raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
81   -
82   - # Declare different validations per option.
83   - validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
84   -
85   - validates_each(attrs, options) do |record, attr, value|
86   - value = options[:tokenizer].call(value) if value.kind_of?(String)
87   -
88   - valid_value = if option == :maximum
89   - value.nil? || value.size.send(validity_checks[option], option_value)
90   - else
91   - value && value.size.send(validity_checks[option], option_value)
92   - end
93   -
94   - record.errors.add(attr, key, :default => custom_message, :count => option_value) unless valid_value
95   - end
96   - end
  109 + def validates_length_of(*attr_names)
  110 + options = attr_names.extract_options!
  111 + validates_with LengthValidator, options.merge(:attributes => attr_names)
97 112 end
98 113
99 114 alias_method :validates_size_of, :validates_length_of
122 activemodel/lib/active_model/validations/numericality.rb
... ... @@ -1,10 +1,68 @@
1 1 module ActiveModel
2 2 module Validations
3   - module ClassMethods
4   - ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
5   - :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
6   - :odd => 'odd?', :even => 'even?' }.freeze
  3 + class NumericalityValidator < EachValidator
  4 + CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=,
  5 + :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=,
  6 + :odd => :odd?, :even => :even? }.freeze
  7 +
  8 + def initialize(options)
  9 + super(options.reverse_merge(:only_integer => false, :allow_nil => false))
  10 + end
  11 +
  12 + def check_validity!
  13 + options.slice(*CHECKS.keys) do |option, value|
  14 + next if [:odd, :even].include?(option)
  15 + 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)
  16 + end
  17 + end
  18 +
  19 + def validate_each(record, attr_name, value)
  20 + before_type_cast = "#{attr_name}_before_type_cast"
  21 +
  22 + raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym)
  23 + raw_value ||= value
  24 +
  25 + return if options[:allow_nil] && raw_value.nil?
  26 +
  27 + unless value = parse_raw_value(raw_value, options)
  28 + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message])
  29 + return
  30 + end
  31 +
  32 + options.slice(*CHECKS.keys).each do |option, option_value|
  33 + case option
  34 + when :odd, :even
  35 + unless value.to_i.send(CHECKS[option])
  36 + record.errors.add(attr_name, option, :value => value, :default => options[:message])
  37 + end
  38 + else
  39 + option_value = option_value.call(record) if option_value.is_a?(Proc)
  40 + option_value = record.send(option_value) if option_value.is_a?(Symbol)
  41 +
  42 + unless value.send(CHECKS[option], option_value)
  43 + record.errors.add(attr_name, option, :default => options[:message], :value => value, :count => option_value)
  44 + end
  45 + end
  46 + end
  47 + end
  48 +
  49 + protected
  50 +
  51 + def parse_raw_value(raw_value, options)
  52 + if options[:only_integer]
  53 + raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
  54 + else
  55 + begin
  56 + Kernel.Float(raw_value)
  57 + rescue ArgumentError, TypeError
  58 + nil
  59 + end
  60 + end
  61 + end
7 62
  63 + end
  64 +
  65 + module ClassMethods
8 66 # Validates whether the value of the specified attribute is numeric by trying to convert it to
9 67 # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
10 68 # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
@@ -44,61 +102,9 @@ module ClassMethods
44 102 # validates_numericality_of :width, :greater_than => :minimum_weight
45 103 # end
46 104 #
47   - #
48   -
49 105 def validates_numericality_of(*attr_names)
50   - configuration = { :only_integer => false, :allow_nil => false }
51   - configuration.update(attr_names.extract_options!)
52   -
53   - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
54   -
55   - (numericality_options - [ :odd, :even ]).each do |option|
56   - value = configuration[option]
57   - 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)
58   - end
59   -
60   - validates_each(attr_names,configuration) do |record, attr_name, value|
61   - before_type_cast = "#{attr_name}_before_type_cast"
62   -
63   - if record.respond_to?(before_type_cast.to_sym)
64   - raw_value = record.send("#{attr_name}_before_type_cast") || value
65   - else
66   - raw_value = value
67   - end
68   -
69   - next if configuration[:allow_nil] and raw_value.nil?
70   -
71   - if configuration[:only_integer]
72   - unless raw_value.to_s =~ /\A[+-]?\d+\Z/
73   - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
74   - next
75   - end
76   - raw_value = raw_value.to_i
77   - else
78   - begin
79   - raw_value = Kernel.Float(raw_value)
80   - rescue ArgumentError, TypeError
81   - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
82   - next
83   - end
84   - end
85   -
86   - numericality_options.each do |option|
87   - case option
88   - when :odd, :even
89   - unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
90   - record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
91   - end
92   - else
93   - configuration[option] = configuration[option].call(record) if configuration[option].is_a? Proc
94   - configuration[option] = record.method(configuration[option]).call if configuration[option].is_a? Symbol
95   -
96   - unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
97   - record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
98   - end
99   - end
100   - end
101   - end
  106 + options = attr_names.extract_options!
  107 + validates_with NumericalityValidator, options.merge(:attributes => attr_names)
102 108 end
103 109 end
104 110 end
15 activemodel/lib/active_model/validations/presence.rb
@@ -2,6 +2,12 @@
2 2
3 3 module ActiveModel
4 4 module Validations
  5 + class PresenceValidator < EachValidator
  6 + def validate(record)
  7 + record.errors.add_on_blank(attributes, options[:message])
  8 + end
  9 + end
  10 +
5 11 module ClassMethods
6 12 # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
7 13 #
@@ -28,13 +34,8 @@ module ClassMethods
28 34 # The method, proc or string should return or evaluate to a true or false value.
29 35 #
30 36 def validates_presence_of(*attr_names)
31   - configuration = attr_names.extract_options!
32   -
33   - # can't use validates_each here, because it cannot cope with nonexistent attributes,
34   - # while errors.add_on_empty can
35   - validate configuration do |record|
36   - record.errors.add_on_blank(attr_names, configuration[:message])
37   - end
  37 + options = attr_names.extract_options!
  38 + validates_with PresenceValidator, options.merge(:attributes => attr_names)
38 39 end
39 40 end
40 41 end
11 activemodel/lib/active_model/validations/with.rb
@@ -48,14 +48,9 @@ module ClassMethods
48 48 # end
49 49 # end
50 50 #
51   - def validates_with(*args)
52   - configuration = args.extract_options!
53   -
54   - validate configuration do |record|
55   - args.each do |klass|
56   - klass.new(record, configuration.except(:on, :if, :unless)).validate
57   - end
58   - end
  51 + def validates_with(*args, &block)
  52 + options = args.extract_options!
  53 + args.each { |klass| validate(klass.new(options, &block), options) }
59 54 end
60 55 end
61 56 end
55 activemodel/lib/active_model/validator.rb
... ... @@ -1,5 +1,4 @@
1 1 module ActiveModel #:nodoc:
2   -
3 2 # A simple base class that can be used along with ActiveModel::Base.validates_with
4 3 #
5 4 # class Person < ActiveModel::Base
@@ -52,17 +51,59 @@ module ActiveModel #:nodoc:
52 51 # @my_custom_field = options[:field_name] || :first_name
53 52 # end
54 53 # end
55   - #
56 54 class Validator
57   - attr_reader :record, :options
  55 + attr_reader :options
58 56
59   - def initialize(record, options)
60   - @record = record
  57 + def initialize(options)
61 58 @options = options
62 59 end
63 60
64   - def validate
65   - raise "You must override this method"
  61 + def validate(record)
  62 + raise NotImplementedError
  63 + end
  64 + end
  65 +
  66 + # EachValidator is a validator which iterates through the attributes given
  67 + # in the options hash invoking the validate_each method passing in the
  68 + # record, attribute and value.
  69 + #
  70 + # All ActiveModel validations are built on top of this Validator.
  71 + class EachValidator < Validator
  72 + attr_reader :attributes
  73 +
  74 + def initialize(options)
  75 + @attributes = options.delete(:attributes)
  76 + super
  77 + check_validity!
  78 + end
  79 +
  80 + def validate(record)
  81 + attributes.each do |attribute|
  82 + value = record.send(:read_attribute_for_validation, attribute)
  83 + next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
  84 + validate_each(record, attribute, value)
  85 + end
  86 + end
  87 +
  88 + def validate_each(record, attribute, value)
  89 + raise NotImplementedError
  90 + end
  91 +
  92 + def check_validity!
  93 + end
  94 + end
  95 +
  96 + # BlockValidator is a special EachValidator which receives a block on initialization
  97 + # and call this block for each attribute being validated. +validates_each+ uses this
  98 + # Validator.
  99 + class BlockValidator < EachValidator
  100 + def initialize(options, &block)
  101 + @block = block
  102 + super
  103 + end
  104 +
  105 + def validate_each(record, attribute, value)
  106 + @block.call(record, attribute, value)
66 107 end
67 108 end
68 109 end
3  activemodel/test/cases/naming_test.rb
... ... @@ -1,8 +1,9 @@
1 1 require 'cases/helper'
  2 +require 'models/track_back'
2 3
3 4 class NamingTest < ActiveModel::TestCase
4 5 def setup
5   - @model_name = ActiveModel::Name.new(self, 'Post::TrackBack')
  6 + @model_name = ActiveModel::Name.new(Post::TrackBack)
6 7 end
7 8
8 9 def test_singular
36 activemodel/test/cases/translation_test.rb
... ... @@ -1,11 +1,5 @@
1 1 require 'cases/helper'
2   -
3   -class SuperUser
4   - extend ActiveModel::Translation
5   -end
6   -
7   -class User < SuperUser
8   -end
  2 +require 'models/person'
9 3
10 4 class ActiveModelI18nTests < ActiveModel::TestCase
11 5
@@ -14,38 +8,38 @@ def setup
14 8 end
15 9
16 10 def test_translated_model_attributes
17   - I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
18   - assert_equal 'super_user name attribute', SuperUser.human_attribute_name('name')
  11 + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
  12 + assert_equal 'person name attribute', Person.human_attribute_name('name')
19 13 end
20 14
21 15 def test_translated_model_attributes_with_symbols
22   - I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
23   - assert_equal 'super_user name attribute', SuperUser.human_attribute_name(:name)
  16 + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
  17 + assert_equal 'person name attribute', Person.human_attribute_name(:name)
24 18 end
25 19
26 20 def test_translated_model_attributes_with_ancestor
27   - I18n.backend.store_translations 'en', :activemodel => {:attributes => {:user => {:name => 'user name attribute'} } }
28   - assert_equal 'user name attribute', User.human_attribute_name('name')
  21 + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:child => {:name => 'child name attribute'} } }
  22 + assert_equal 'child name attribute', Child.human_attribute_name('name')
29 23 end
30 24
31 25 def test_translated_model_attributes_with_ancestors_fallback
32   - I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
33   - assert_equal 'super_user name attribute', User.human_attribute_name('name')
  26 + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
  27 + assert_equal 'person name attribute', Child.human_attribute_name('name')
34 28 end
35 29
36 30 def test_translated_model_names
37   - I18n.backend.store_translations 'en', :activemodel => {:models => {:super_user => 'super_user model'} }
38   - assert_equal 'super_user model', SuperUser.model_name.human
  31 + I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
  32 + assert_equal 'person model', Person.model_name.human
39 33 end
40 34
41 35 def test_translated_model_names_with_sti
42   - I18n.backend.store_translations 'en', :activemodel => {:models => {:user => 'user model'} }
43   - assert_equal 'user model', User.model_name.human
  36 + I18n.backend.store_translations 'en', :activemodel => {:models => {:child => 'child model'} }
  37 + assert_equal 'child model', Child.model_name.human
44 38 end
45 39
46 40 def test_translated_model_names_with_ancestors_fallback
47   - I18n.backend.store_translations 'en', :activemodel => {:models => {:super_user => 'super_user model'} }
48   - assert_equal 'super_user model', User.model_name.human
  41 + I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
  42 + assert_equal 'person model', Child.model_name.human
49 43 end
50 44 end
51 45
33 activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -9,9 +9,10 @@
9 9
10 10 class AcceptanceValidationTest < ActiveModel::TestCase
11 11 include ActiveModel::TestsDatabase
12   - include ActiveModel::ValidationsRepairHelper
13 12
14   - repair_validations(Topic)
  13 + def teardown
  14 + Topic.reset_callbacks(:validate)
  15 + end
15 16
16 17 def test_terms_of_service_agreement_no_acceptance
17 18 Topic.validates_acceptance_of(:terms_of_service, :on => :create)
@@ -53,28 +54,18 @@ def test_terms_of_service_agreement_with_accept_value
53 54 assert t.save
54 55 end
55 56
56   - def test_validates_acceptance_of_with_custom_error_using_quotes
57   - repair_validations(Developer) do
58   - Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
59   - d = Developer.new
60   - d.salary = "0"
61   - assert !d.valid?
62   - assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last
63   - end
64   - end
65   -
66 57 def test_validates_acceptance_of_for_ruby_class
67   - repair_validations(Person) do
68   - Person.validates_acceptance_of :karma
  58 + Person.validates_acceptance_of :karma
69 59
70   - p = Person.new
71   - p.karma = ""
  60 + p = Person.new
  61 + p.karma = ""
72 62
73   - assert p.invalid?
74   - assert_equal ["must be accepted"], p.errors[:karma]
  63 + assert p.invalid?
  64 + assert_equal ["must be accepted"], p.errors[:karma]
75 65
76   - p.karma = "1"
77   - assert p.valid?
78   - end
  66 + p.karma = "1"
  67 + assert p.valid?
  68 + ensure
  69 + Person.reset_callbacks(:validate)
79 70 end
80 71 end
5 activemodel/test/cases/validations/conditional_validation_test.rb
@@ -6,9 +6,10 @@
6 6
7 7 class ConditionalValidationTest < ActiveModel::TestCase
8 8 include ActiveModel::TestsDatabase
9   - include ActiveModel::ValidationsRepairHelper
10 9
11   - repair_validations(Topic)
  10 + def teardown
  11 + Topic.reset_callbacks(:validate)
  12 + end
12 13
13 14 def test_if_validation_using_method_true
14 15 # When the method returns true
34 activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -8,9 +8,10 @@
8 8
9 9 class ConfirmationValidationTest < ActiveModel::TestCase
10 10 include ActiveModel::TestsDatabase
11   - include ActiveModel::ValidationsRepairHelper
12 11
13   - repair_validations(Topic)
  12 + def teardown
  13 + Topic.reset_callbacks(:validate)
  14 + end
14 15
15 16 def test_no_title_confirmation
16 17 Topic.validates_confirmation_of(:title)
@@ -39,30 +40,19 @@ def test_title_confirmation
39 40 assert t.save
40 41 end
41 42
42   - def test_validates_confirmation_of_with_custom_error_using_quotes
43   - repair_validations(Developer) do
44   - Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
45   - d = Developer.new
46   - d.name = "John"
47   - d.name_confirmation = "Johnny"
48   - assert !d.valid?
49   - assert_equal ["confirm 'single' and \"double\" quotes"], d.errors[:name]
50   - end
51   - end
52   -
53 43 def test_validates_confirmation_of_for_ruby_class
54   - repair_validations(Person) do
55   - Person.validates_confirmation_of :karma
  44 + Person.validates_confirmation_of :karma
56 45
57   - p = Person.new
58   - p.karma_confirmation = "None"
59   - assert p.invalid?
  46 + p = Person.new
  47 + p.karma_confirmation = "None"
  48 + assert p.invalid?
60 49
61   - assert_equal ["doesn't match confirmation"], p.errors[:karma]
  50 + assert_equal ["doesn't match confirmation"], p.errors[:karma]
62 51
63   - p.karma = "None"
64   - assert p.valid?
65   - end
  52 + p.karma = "None"
  53 + assert p.valid?
  54 + ensure
  55 + Person.reset_callbacks(:validate)
66 56 end
67 57
68 58 end
23 activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -7,9 +7,10 @@
7 7
8 8 class ExclusionValidationTest < ActiveModel::TestCase
9 9 include ActiveModel::TestsDatabase
10   - include ActiveModel::ValidationsRepairHelper
11 10
12   - repair_validations(Topic)
  11 + def teardown
  12 + Topic.reset_callbacks(:validate)
  13 + end
13 14
14 15 def test_validates_exclusion_of
15 16 Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) )
@@ -30,17 +31,17 @@ def test_validates_exclusion_of_with_formatted_message
30 31 end
31 32
32 33 def test_validates_exclusion_of_for_ruby_class
33   - repair_validations(Person) do
34   - Person.validates_exclusion_of :karma, :in => %w( abe monkey )
  34 + Person.validates_exclusion_of :karma, :in => %w( abe monkey )
35 35
36   - p = Person.new
37   - p.karma = "abe"
38