activemodel 3.0.pre → 3.0.0.rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. data/CHANGELOG +44 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +184 -0
  4. data/lib/active_model.rb +29 -19
  5. data/lib/active_model/attribute_methods.rb +167 -46
  6. data/lib/active_model/callbacks.rb +134 -0
  7. data/lib/active_model/conversion.rb +41 -1
  8. data/lib/active_model/deprecated_error_methods.rb +1 -1
  9. data/lib/active_model/dirty.rb +56 -12
  10. data/lib/active_model/errors.rb +205 -46
  11. data/lib/active_model/lint.rb +53 -17
  12. data/lib/active_model/locale/en.yml +26 -23
  13. data/lib/active_model/mass_assignment_security.rb +160 -0
  14. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  15. data/lib/active_model/mass_assignment_security/sanitizer.rb +23 -0
  16. data/lib/active_model/naming.rb +70 -5
  17. data/lib/active_model/observing.rb +40 -16
  18. data/lib/active_model/railtie.rb +2 -0
  19. data/lib/active_model/serialization.rb +59 -0
  20. data/lib/active_model/serializers/json.rb +17 -11
  21. data/lib/active_model/serializers/xml.rb +66 -123
  22. data/lib/active_model/test_case.rb +0 -2
  23. data/lib/active_model/translation.rb +64 -0
  24. data/lib/active_model/validations.rb +150 -68
  25. data/lib/active_model/validations/acceptance.rb +53 -33
  26. data/lib/active_model/validations/callbacks.rb +57 -0
  27. data/lib/active_model/validations/confirmation.rb +41 -23
  28. data/lib/active_model/validations/exclusion.rb +18 -13
  29. data/lib/active_model/validations/format.rb +28 -24
  30. data/lib/active_model/validations/inclusion.rb +18 -13
  31. data/lib/active_model/validations/length.rb +67 -65
  32. data/lib/active_model/validations/numericality.rb +83 -58
  33. data/lib/active_model/validations/presence.rb +10 -8
  34. data/lib/active_model/validations/validates.rb +110 -0
  35. data/lib/active_model/validations/with.rb +90 -23
  36. data/lib/active_model/validator.rb +186 -0
  37. data/lib/active_model/version.rb +3 -2
  38. metadata +79 -20
  39. data/README +0 -21
  40. data/lib/active_model/state_machine.rb +0 -70
  41. data/lib/active_model/state_machine/event.rb +0 -62
  42. data/lib/active_model/state_machine/machine.rb +0 -75
  43. data/lib/active_model/state_machine/state.rb +0 -47
  44. data/lib/active_model/state_machine/state_transition.rb +0 -40
  45. data/lib/active_model/validations_repair_helper.rb +0 -35
@@ -1,44 +1,62 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Confirmation Validator
2
4
  module Validations
3
- module ClassMethods
4
- # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
5
+ class ConfirmationValidator < EachValidator
6
+ def validate_each(record, attribute, value)
7
+ confirmed = record.send(:"#{attribute}_confirmation")
8
+ return if confirmed.nil? || value == confirmed
9
+ record.errors.add(attribute, :confirmation, options)
10
+ end
11
+
12
+ def setup(klass)
13
+ klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" })
14
+ end
15
+ end
16
+
17
+ module HelperMethods
18
+ # Encapsulates the pattern of wanting to validate a password or email
19
+ # address field with a confirmation. For example:
5
20
  #
6
21
  # Model:
7
22
  # class Person < ActiveRecord::Base
8
23
  # validates_confirmation_of :user_name, :password
9
- # validates_confirmation_of :email_address, :message => "should match confirmation"
24
+ # validates_confirmation_of :email_address,
25
+ # :message => "should match confirmation"
10
26
  # end
11
27
  #
12
28
  # View:
13
29
  # <%= password_field "person", "password" %>
14
30
  # <%= password_field "person", "password_confirmation" %>
15
31
  #
16
- # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password.
17
- # To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed
18
- # only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence
19
- # check for the confirmation attribute:
32
+ # The added +password_confirmation+ attribute is virtual; it exists only
33
+ # as an in-memory attribute for validating the password. To achieve this,
34
+ # the validation adds accessors to the model for the confirmation
35
+ # attribute.
36
+ #
37
+ # NOTE: This check is performed only if +password_confirmation+ is not
38
+ # +nil+, and by default only on save. To require confirmation, make sure
39
+ # to add a presence check for the confirmation attribute:
20
40
  #
21
41
  # validates_presence_of :password_confirmation, :if => :password_changed?
22
42
  #
23
43
  # Configuration options:
24
- # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation").
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>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
27
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
28
- # method, proc or string should return or evaluate to a true or false value.
29
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
30
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
44
+ # * <tt>:message</tt> - A custom error message (default is: "doesn't match
45
+ # confirmation").
46
+ # * <tt>:on</tt> - Specifies when this validation is active (default is
47
+ # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
48
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
49
+ # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>,
50
+ # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
51
+ # method, proc or string should return or evaluate to a true or false
52
+ # value.
53
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
54
+ # determine if the validation should not occur (e.g.
55
+ # <tt>:unless => :skip_validation</tt>, or
56
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
31
57
  # method, proc or string should return or evaluate to a true or false value.
32
58
  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
59
+ validates_with ConfirmationValidator, _merge_attributes(attr_names)
42
60
  end
43
61
  end
44
62
  end
@@ -1,12 +1,27 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Exclusion Validator
2
4
  module Validations
3
- module ClassMethods
5
+ class ExclusionValidator < EachValidator
6
+ def check_validity!
7
+ raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
8
+ ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
9
+ end
10
+
11
+ def validate_each(record, attribute, value)
12
+ if options[:in].include?(value)
13
+ record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value))
14
+ end
15
+ end
16
+ end
17
+
18
+ module HelperMethods
4
19
  # Validates that the value of the specified attribute is not in a particular enumerable object.
5
20
  #
6
21
  # class Person < ActiveRecord::Base
7
22
  # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
8
23
  # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
9
- # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed"
24
+ # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed"
10
25
  # end
11
26
  #
12
27
  # Configuration options:
@@ -21,17 +36,7 @@ module ActiveModel
21
36
  # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
22
37
  # method, proc or string should return or evaluate to a true or false value.
23
38
  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
39
+ validates_with ExclusionValidator, _merge_attributes(attr_names)
35
40
  end
36
41
  end
37
42
  end
@@ -1,6 +1,32 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Format Validator
2
4
  module Validations
3
- module ClassMethods
5
+ class FormatValidator < EachValidator
6
+ def validate_each(record, attribute, value)
7
+ if options[:with] && value.to_s !~ options[:with]
8
+ record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value))
9
+ elsif options[:without] && value.to_s =~ options[:without]
10
+ record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value))
11
+ end
12
+ end
13
+
14
+ def check_validity!
15
+ unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
16
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)"
17
+ end
18
+
19
+ if options[:with] && !options[:with].is_a?(Regexp)
20
+ raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
21
+ end
22
+
23
+ if options[:without] && !options[:without].is_a?(Regexp)
24
+ raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
25
+ end
26
+ end
27
+ end
28
+
29
+ module HelperMethods
4
30
  # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
5
31
  # You can require that the attribute matches the regular expression:
6
32
  #
@@ -33,29 +59,7 @@ module ActiveModel
33
59
  # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
34
60
  # method, proc or string should return or evaluate to a true or false value.
35
61
  def validates_format_of(*attr_names)
36
- configuration = attr_names.extract_options!
37
-
38
- unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
39
- raise ArgumentError, "Either :with or :without must be supplied (but not both)"
40
- end
41
-
42
- if configuration[:with] && !configuration[:with].is_a?(Regexp)
43
- raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
44
- end
45
-
46
- if configuration[:without] && !configuration[:without].is_a?(Regexp)
47
- raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
48
- end
49
-
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
62
+ validates_with FormatValidator, _merge_attributes(attr_names)
59
63
  end
60
64
  end
61
65
  end
@@ -1,12 +1,27 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Inclusion Validator
2
4
  module Validations
3
- module ClassMethods
5
+ class InclusionValidator < EachValidator
6
+ def check_validity!
7
+ raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
8
+ ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
9
+ end
10
+
11
+ def validate_each(record, attribute, value)
12
+ unless options[:in].include?(value)
13
+ record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value))
14
+ end
15
+ end
16
+ end
17
+
18
+ module HelperMethods
4
19
  # Validates whether the value of the specified attribute is available in a particular enumerable object.
5
20
  #
6
21
  # class Person < ActiveRecord::Base
7
22
  # validates_inclusion_of :gender, :in => %w( m f )
8
23
  # validates_inclusion_of :age, :in => 0..99
9
- # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
24
+ # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list"
10
25
  # end
11
26
  #
12
27
  # Configuration options:
@@ -21,17 +36,7 @@ module ActiveModel
21
36
  # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
22
37
  # method, proc or string should return or evaluate to a true or false value.
23
38
  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
39
+ validates_with InclusionValidator, _merge_attributes(attr_names)
35
40
  end
36
41
  end
37
42
  end
@@ -1,19 +1,75 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Length Validator
2
4
  module Validations
3
- module ClassMethods
4
- ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
5
+ class LengthValidator < EachValidator
6
+ MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
7
+ CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
8
+
9
+ DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
10
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
11
+
12
+ def initialize(options)
13
+ if range = (options.delete(:in) || options.delete(:within))
14
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
15
+ options[:minimum], options[:maximum] = range.begin, range.end
16
+ options[:maximum] -= 1 if range.exclude_end?
17
+ end
18
+
19
+ super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER))
20
+ end
21
+
22
+ def check_validity!
23
+ keys = CHECKS.keys & options.keys
24
+
25
+ if keys.empty?
26
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
27
+ end
28
+
29
+ keys.each do |key|
30
+ value = options[key]
31
+
32
+ unless value.is_a?(Integer) && value >= 0
33
+ raise ArgumentError, ":#{key} must be a nonnegative Integer"
34
+ end
35
+ end
36
+ end
37
+
38
+ def validate_each(record, attribute, value)
39
+ value = options[:tokenizer].call(value) if value.kind_of?(String)
40
+
41
+ CHECKS.each do |key, validity_check|
42
+ next unless check_value = options[key]
43
+ default_message = options[MESSAGES[key]]
44
+ options[:message] ||= default_message if default_message
45
+
46
+ valid_value = if key == :maximum
47
+ value.nil? || value.size.send(validity_check, check_value)
48
+ else
49
+ value && value.size.send(validity_check, check_value)
50
+ end
51
+
52
+ next if valid_value
53
+
54
+ record.errors.add(attribute, MESSAGES[key],
55
+ options.except(*RESERVED_OPTIONS).merge!(:count => check_value))
56
+ end
57
+ end
58
+ end
59
+
60
+ module HelperMethods
5
61
 
6
62
  # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
7
63
  #
8
64
  # class Person < ActiveRecord::Base
9
65
  # validates_length_of :first_name, :maximum=>30
10
- # validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
66
+ # validates_length_of :last_name, :maximum=>30, :message=>"less than 30 if you don't mind"
11
67
  # validates_length_of :fax, :in => 7..32, :allow_nil => true
12
68
  # validates_length_of :phone, :in => 7..32, :allow_blank => true
13
69
  # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
14
- # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
15
- # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
16
- # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
70
+ # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters"
71
+ # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me."
72
+ # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
17
73
  # end
18
74
  #
19
75
  # Configuration options:
@@ -24,9 +80,9 @@ module ActiveModel
24
80
  # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
25
81
  # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
26
82
  # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
27
- # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)").
28
- # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)").
29
- # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)").
83
+ # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %{count} characters)").
84
+ # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %{count} characters)").
85
+ # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %{count} characters)").
30
86
  # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
31
87
  # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
32
88
  # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
@@ -38,62 +94,8 @@ module ActiveModel
38
94
  # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
39
95
  # count words as in above example.)
40
96
  # 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
97
+ def validates_length_of(*attr_names)
98
+ validates_with LengthValidator, _merge_attributes(attr_names)
97
99
  end
98
100
 
99
101
  alias_method :validates_size_of, :validates_length_of
@@ -1,10 +1,88 @@
1
1
  module ActiveModel
2
+
3
+ # == Active Model Numericality Validator
2
4
  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
5
+ class NumericalityValidator < EachValidator
6
+ CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=,
7
+ :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=,
8
+ :odd => :odd?, :even => :even? }.freeze
9
+
10
+ RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
11
+
12
+ def initialize(options)
13
+ super(options.reverse_merge(:only_integer => false, :allow_nil => false))
14
+ end
15
+
16
+ def check_validity!
17
+ keys = CHECKS.keys - [:odd, :even]
18
+ options.slice(*keys).each do |option, value|
19
+ next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
20
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
21
+ end
22
+ end
23
+
24
+ def validate_each(record, attr_name, value)
25
+ before_type_cast = "#{attr_name}_before_type_cast"
26
+
27
+ raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym)
28
+ raw_value ||= value
29
+
30
+ return if options[:allow_nil] && raw_value.nil?
31
+
32
+ unless value = parse_raw_value_as_a_number(raw_value)
33
+ record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
34
+ return
35
+ end
36
+
37
+ if options[:only_integer]
38
+ unless value = parse_raw_value_as_an_integer(raw_value)
39
+ record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
40
+ return
41
+ end
42
+ end
43
+
44
+ options.slice(*CHECKS.keys).each do |option, option_value|
45
+ case option
46
+ when :odd, :even
47
+ unless value.to_i.send(CHECKS[option])
48
+ record.errors.add(attr_name, option, filtered_options(value))
49
+ end
50
+ else
51
+ option_value = option_value.call(record) if option_value.is_a?(Proc)
52
+ option_value = record.send(option_value) if option_value.is_a?(Symbol)
53
+
54
+ unless value.send(CHECKS[option], option_value)
55
+ record.errors.add(attr_name, option, filtered_options(value).merge(:count => option_value))
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def parse_raw_value_as_a_number(raw_value)
64
+ case raw_value
65
+ when /\A0[xX]/
66
+ nil
67
+ else
68
+ begin
69
+ Kernel.Float(raw_value)
70
+ rescue ArgumentError, TypeError
71
+ nil
72
+ end
73
+ end
74
+ end
75
+
76
+ def parse_raw_value_as_an_integer(raw_value)
77
+ raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
78
+ end
79
+
80
+ def filtered_options(value)
81
+ options.except(*RESERVED_OPTIONS).merge!(:value => value)
82
+ end
83
+ end
7
84
 
85
+ module HelperMethods
8
86
  # Validates whether the value of the specified attribute is numeric by trying to convert it to
9
87
  # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
10
88
  # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
@@ -44,61 +122,8 @@ module ActiveModel
44
122
  # validates_numericality_of :width, :greater_than => :minimum_weight
45
123
  # end
46
124
  #
47
- #
48
-
49
125
  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
126
+ validates_with NumericalityValidator, _merge_attributes(attr_names)
102
127
  end
103
128
  end
104
129
  end