activemodel 4.2.0 → 6.1.0

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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. data/lib/active_model/test_case.rb +0 -4
@@ -1,12 +1,12 @@
1
- module ActiveModel
1
+ # frozen_string_literal: true
2
2
 
3
- # == Active \Model Length Validator
3
+ module ActiveModel
4
4
  module Validations
5
5
  class LengthValidator < EachValidator # :nodoc:
6
6
  MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
7
7
  CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
8
8
 
9
- RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
9
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
10
10
 
11
11
  def initialize(options)
12
12
  if range = (options.delete(:in) || options.delete(:within))
@@ -25,20 +25,19 @@ module ActiveModel
25
25
  keys = CHECKS.keys & options.keys
26
26
 
27
27
  if keys.empty?
28
- raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
28
+ raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
29
29
  end
30
30
 
31
31
  keys.each do |key|
32
32
  value = options[key]
33
33
 
34
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
35
- raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
34
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
35
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
36
36
  end
37
37
  end
38
38
  end
39
39
 
40
40
  def validate_each(record, attribute, value)
41
- value = tokenize(value)
42
41
  value_length = value.respond_to?(:length) ? value.length : value.to_s.length
43
42
  errors_options = options.except(*RESERVED_OPTIONS)
44
43
 
@@ -46,7 +45,13 @@ module ActiveModel
46
45
  next unless check_value = options[key]
47
46
 
48
47
  if !value.nil? || skip_nil_check?(key)
49
- next if value_length.send(validity_check, check_value)
48
+ case check_value
49
+ when Proc
50
+ check_value = check_value.call(record)
51
+ when Symbol
52
+ check_value = record.send(check_value)
53
+ end
54
+ next if value_length.public_send(validity_check, check_value)
50
55
  end
51
56
 
52
57
  errors_options[:count] = check_value
@@ -54,27 +59,20 @@ module ActiveModel
54
59
  default_message = options[MESSAGES[key]]
55
60
  errors_options[:message] ||= default_message if default_message
56
61
 
57
- record.errors.add(attribute, MESSAGES[key], errors_options)
62
+ record.errors.add(attribute, MESSAGES[key], **errors_options)
58
63
  end
59
64
  end
60
65
 
61
66
  private
62
-
63
- def tokenize(value)
64
- if options[:tokenizer] && value.kind_of?(String)
65
- options[:tokenizer].call(value)
66
- end || value
67
- end
68
-
69
- def skip_nil_check?(key)
70
- key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
71
- end
67
+ def skip_nil_check?(key)
68
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
69
+ end
72
70
  end
73
71
 
74
72
  module HelperMethods
75
-
76
- # Validates that the specified attribute matches the length restrictions
77
- # supplied. Only one option can be used at a time:
73
+ # Validates that the specified attributes match the length restrictions
74
+ # supplied. Only one constraint option can be used at a time apart from
75
+ # +:minimum+ and +:maximum+ that can be combined together:
78
76
  #
79
77
  # class Person < ActiveRecord::Base
80
78
  # validates_length_of :first_name, maximum: 30
@@ -84,38 +82,43 @@ module ActiveModel
84
82
  # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
85
83
  # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
86
84
  # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
87
- # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.',
88
- # tokenizer: ->(str) { str.scan(/\w+/) }
85
+ # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.'
86
+ #
87
+ # private
88
+ #
89
+ # def words_in_essay
90
+ # essay.scan(/\w+/)
91
+ # end
89
92
  # end
90
93
  #
91
- # Configuration options:
94
+ # Constraint options:
95
+ #
92
96
  # * <tt>:minimum</tt> - The minimum size of the attribute.
93
97
  # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
94
- # default if not used with :minimum.
98
+ # default if not used with +:minimum+.
95
99
  # * <tt>:is</tt> - The exact size of the attribute.
96
100
  # * <tt>:within</tt> - A range specifying the minimum and maximum size of
97
101
  # the attribute.
98
102
  # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
103
+ #
104
+ # Other options:
105
+ #
99
106
  # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
100
107
  # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
101
108
  # * <tt>:too_long</tt> - The error message if the attribute goes over the
102
109
  # maximum (default is: "is too long (maximum is %{count} characters)").
103
110
  # * <tt>:too_short</tt> - The error message if the attribute goes under the
104
- # minimum (default is: "is too short (min is %{count} characters)").
111
+ # minimum (default is: "is too short (minimum is %{count} characters)").
105
112
  # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
106
113
  # method and the attribute is the wrong size (default is: "is the wrong
107
114
  # length (should be %{count} characters)").
108
115
  # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
109
116
  # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
110
117
  # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
111
- # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string.
112
- # (e.g. <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words
113
- # as in above example). Defaults to <tt>->(value) { value.split(//) }</tt>
114
- # which counts individual characters.
115
118
  #
116
119
  # There is also a list of default options supported by every validator:
117
120
  # +:if+, +:unless+, +:on+ and +:strict+.
118
- # See <tt>ActiveModel::Validation#validates</tt> for more information
121
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
119
122
  def validates_length_of(*attr_names)
120
123
  validates_with LengthValidator, _merge_attributes(attr_names)
121
124
  end
@@ -1,5 +1,8 @@
1
- module ActiveModel
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal/util"
2
4
 
5
+ module ActiveModel
3
6
  module Validations
4
7
  class NumericalityValidator < EachValidator # :nodoc:
5
8
  CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
@@ -8,6 +11,10 @@ module ActiveModel
8
11
 
9
12
  RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
10
13
 
14
+ INTEGER_REGEX = /\A[+-]?\d+\z/
15
+
16
+ HEXADECIMAL_REGEX = /\A[+-]?0[xX]/
17
+
11
18
  def check_validity!
12
19
  keys = CHECKS.keys - [:odd, :even]
13
20
  options.slice(*keys).each do |option, value|
@@ -17,35 +24,24 @@ module ActiveModel
17
24
  end
18
25
  end
19
26
 
20
- def validate_each(record, attr_name, value)
21
- before_type_cast = :"#{attr_name}_before_type_cast"
22
-
23
- raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
24
- raw_value ||= value
25
-
26
- if record_attribute_changed_in_place?(record, attr_name)
27
- raw_value = value
27
+ def validate_each(record, attr_name, value, precision: Float::DIG, scale: nil)
28
+ unless is_number?(value, precision, scale)
29
+ record.errors.add(attr_name, :not_a_number, **filtered_options(value))
30
+ return
28
31
  end
29
32
 
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))
33
+ if allow_only_integer?(record) && !is_integer?(value)
34
+ record.errors.add(attr_name, :not_an_integer, **filtered_options(value))
34
35
  return
35
36
  end
36
37
 
37
- if allow_only_integer?(record)
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
38
+ value = parse_as_number(value, precision, scale)
43
39
 
44
40
  options.slice(*CHECKS.keys).each do |option, option_value|
45
41
  case option
46
42
  when :odd, :even
47
- unless value.to_i.send(CHECKS[option])
48
- record.errors.add(attr_name, option, filtered_options(value))
43
+ unless value.to_i.public_send(CHECKS[option])
44
+ record.errors.add(attr_name, option, **filtered_options(value))
49
45
  end
50
46
  else
51
47
  case option_value
@@ -55,23 +51,44 @@ module ActiveModel
55
51
  option_value = record.send(option_value)
56
52
  end
57
53
 
58
- unless value.send(CHECKS[option], option_value)
59
- record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
54
+ option_value = parse_as_number(option_value, precision, scale)
55
+
56
+ unless value.public_send(CHECKS[option], option_value)
57
+ record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
60
58
  end
61
59
  end
62
60
  end
63
61
  end
64
62
 
65
- protected
63
+ private
64
+ def parse_as_number(raw_value, precision, scale)
65
+ if raw_value.is_a?(Float)
66
+ parse_float(raw_value, precision, scale)
67
+ elsif raw_value.is_a?(Numeric)
68
+ raw_value
69
+ elsif is_integer?(raw_value)
70
+ raw_value.to_i
71
+ elsif !is_hexadecimal_literal?(raw_value)
72
+ parse_float(Kernel.Float(raw_value), precision, scale)
73
+ end
74
+ end
75
+
76
+ def parse_float(raw_value, precision, scale)
77
+ (scale ? raw_value.truncate(scale) : raw_value).to_d(precision)
78
+ end
66
79
 
67
- def parse_raw_value_as_a_number(raw_value)
68
- Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
80
+ def is_number?(raw_value, precision, scale)
81
+ !parse_as_number(raw_value, precision, scale).nil?
69
82
  rescue ArgumentError, TypeError
70
- nil
83
+ false
71
84
  end
72
85
 
73
- def parse_raw_value_as_an_integer(raw_value)
74
- raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/
86
+ def is_integer?(raw_value)
87
+ INTEGER_REGEX.match?(raw_value.to_s)
88
+ end
89
+
90
+ def is_hexadecimal_literal?(raw_value)
91
+ HEXADECIMAL_REGEX.match?(raw_value.to_s)
75
92
  end
76
93
 
77
94
  def filtered_options(value)
@@ -91,7 +108,26 @@ module ActiveModel
91
108
  end
92
109
  end
93
110
 
94
- private
111
+ def prepare_value_for_validation(value, record, attr_name)
112
+ return value if record_attribute_changed_in_place?(record, attr_name)
113
+
114
+ came_from_user = :"#{attr_name}_came_from_user?"
115
+
116
+ if record.respond_to?(came_from_user)
117
+ if record.public_send(came_from_user)
118
+ raw_value = record.public_send(:"#{attr_name}_before_type_cast")
119
+ elsif record.respond_to?(:read_attribute)
120
+ raw_value = record.read_attribute(attr_name)
121
+ end
122
+ else
123
+ before_type_cast = :"#{attr_name}_before_type_cast"
124
+ if record.respond_to?(before_type_cast)
125
+ raw_value = record.public_send(before_type_cast)
126
+ end
127
+ end
128
+
129
+ raw_value || value
130
+ end
95
131
 
96
132
  def record_attribute_changed_in_place?(record, attr_name)
97
133
  record.respond_to?(:attribute_changed_in_place?) &&
@@ -102,8 +138,9 @@ module ActiveModel
102
138
  module HelperMethods
103
139
  # Validates whether the value of the specified attribute is numeric by
104
140
  # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
105
- # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt>
106
- # (if <tt>only_integer</tt> is set to +true+).
141
+ # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
142
+ # (if <tt>only_integer</tt> is set to +true+). Precision of Kernel.Float values
143
+ # are guaranteed up to 15 digits.
107
144
  #
108
145
  # class Person < ActiveRecord::Base
109
146
  # validates_numericality_of :value, on: :create
@@ -114,7 +151,7 @@ module ActiveModel
114
151
  # * <tt>:only_integer</tt> - Specifies whether the value has to be an
115
152
  # integer, e.g. an integral value (default is +false+).
116
153
  # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
117
- # +false+). Notice that for fixnum and float columns empty strings are
154
+ # +false+). Notice that for Integer and Float columns empty strings are
118
155
  # converted to +nil+.
119
156
  # * <tt>:greater_than</tt> - Specifies the value must be greater than the
120
157
  # supplied value.
@@ -133,7 +170,7 @@ module ActiveModel
133
170
  #
134
171
  # There is also a list of default options supported by every validator:
135
172
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
136
- # See <tt>ActiveModel::Validation#validates</tt> for more information
173
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
137
174
  #
138
175
  # The following checks can also be supplied with a proc or a symbol which
139
176
  # corresponds to a method:
@@ -144,6 +181,7 @@ module ActiveModel
144
181
  # * <tt>:less_than</tt>
145
182
  # * <tt>:less_than_or_equal_to</tt>
146
183
  # * <tt>:only_integer</tt>
184
+ # * <tt>:other_than</tt>
147
185
  #
148
186
  # For example:
149
187
  #
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveModel
3
-
4
4
  module Validations
5
5
  class PresenceValidator < EachValidator # :nodoc:
6
6
  def validate_each(record, attr_name, value)
7
- record.errors.add(attr_name, :blank, options) if value.blank?
7
+ record.errors.add(attr_name, :blank, **options) if value.blank?
8
8
  end
9
9
  end
10
10
 
@@ -30,7 +30,7 @@ module ActiveModel
30
30
  #
31
31
  # There is also a list of default options supported by every validator:
32
32
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
33
- # See <tt>ActiveModel::Validation#validates</tt> for more information
33
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
34
34
  def validates_presence_of(*attr_names)
35
35
  validates_with PresenceValidator, _merge_attributes(attr_names)
36
36
  end
@@ -1,4 +1,6 @@
1
- require 'active_support/core_ext/hash/slice'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
2
4
 
3
5
  module ActiveModel
4
6
  module Validations
@@ -10,6 +12,7 @@ module ActiveModel
10
12
  #
11
13
  # Examples of using the default rails validators:
12
14
  #
15
+ # validates :username, absence: true
13
16
  # validates :terms, acceptance: true
14
17
  # validates :password, confirmation: true
15
18
  # validates :username, exclusion: { in: %w(admin superuser) }
@@ -18,7 +21,6 @@ module ActiveModel
18
21
  # validates :first_name, length: { maximum: 30 }
19
22
  # validates :age, numericality: true
20
23
  # validates :username, presence: true
21
- # validates :username, uniqueness: true
22
24
  #
23
25
  # The power of the +validates+ method comes when using custom validators
24
26
  # and default validators in one call for a given attribute.
@@ -26,7 +28,7 @@ module ActiveModel
26
28
  # class EmailValidator < ActiveModel::EachValidator
27
29
  # def validate_each(record, attribute, value)
28
30
  # record.errors.add attribute, (options[:message] || "is not an email") unless
29
- # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
31
+ # /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
30
32
  # end
31
33
  # end
32
34
  #
@@ -34,7 +36,7 @@ module ActiveModel
34
36
  # include ActiveModel::Validations
35
37
  # attr_accessor :name, :email
36
38
  #
37
- # validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
39
+ # validates :name, presence: true, length: { maximum: 100 }
38
40
  # validates :email, presence: true, email: true
39
41
  # end
40
42
  #
@@ -46,7 +48,7 @@ module ActiveModel
46
48
  #
47
49
  # class TitleValidator < ActiveModel::EachValidator
48
50
  # def validate_each(record, attribute, value)
49
- # record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
51
+ # record.errors.add attribute, "must start with 'the'" unless /\Athe/i.match?(value)
50
52
  # end
51
53
  # end
52
54
  #
@@ -62,7 +64,7 @@ module ActiveModel
62
64
  # and strings in shortcut form.
63
65
  #
64
66
  # validates :email, format: /@/
65
- # validates :gender, inclusion: %w(male female)
67
+ # validates :role, inclusion: %w(admin contributor)
66
68
  # validates :password, length: 6..20
67
69
  #
68
70
  # When using shortcut form, ranges and arrays are passed to your
@@ -72,7 +74,7 @@ module ActiveModel
72
74
  # There is also a list of options that could be used along with validators:
73
75
  #
74
76
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
75
- # Runs in all validation contexts by default (nil). You can pass a symbol
77
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
76
78
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
77
79
  # <tt>on: :custom_validation_context</tt> or
78
80
  # <tt>on: [:create, :custom_validation_context]</tt>)
@@ -94,7 +96,7 @@ module ActiveModel
94
96
  # Example:
95
97
  #
96
98
  # validates :password, presence: true, confirmation: true, if: :password_required?
97
- # validates :token, uniqueness: true, strict: TokenGenerationException
99
+ # validates :token, length: 24, strict: TokenLengthException
98
100
  #
99
101
  #
100
102
  # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
@@ -111,15 +113,16 @@ module ActiveModel
111
113
  defaults[:attributes] = attributes
112
114
 
113
115
  validations.each do |key, options|
114
- next unless options
115
116
  key = "#{key.to_s.camelize}Validator"
116
117
 
117
118
  begin
118
- validator = key.include?('::') ? key.constantize : const_get(key)
119
+ validator = key.include?("::") ? key.constantize : const_get(key)
119
120
  rescue NameError
120
121
  raise ArgumentError, "Unknown validator: '#{key}'"
121
122
  end
122
123
 
124
+ next unless options
125
+
123
126
  validates_with(validator, defaults.merge(_parse_validates_options(options)))
124
127
  end
125
128
  end
@@ -148,15 +151,14 @@ module ActiveModel
148
151
  validates(*(attributes << options))
149
152
  end
150
153
 
151
- protected
152
-
154
+ private
153
155
  # When creating custom validators, it might be useful to be able to specify
154
156
  # additional default keys. This can be done by overwriting this method.
155
- def _validates_default_keys # :nodoc:
156
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
157
+ def _validates_default_keys
158
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
157
159
  end
158
160
 
159
- def _parse_validates_options(options) # :nodoc:
161
+ def _parse_validates_options(options)
160
162
  case options
161
163
  when TrueClass
162
164
  {}
@@ -1,15 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+
1
5
  module ActiveModel
2
6
  module Validations
3
- module HelperMethods
4
- private
5
- def _merge_attributes(attr_names)
6
- options = attr_names.extract_options!.symbolize_keys
7
- attr_names.flatten!
8
- options[:attributes] = attr_names
9
- options
10
- end
11
- end
12
-
13
7
  class WithValidator < EachValidator # :nodoc:
14
8
  def validate_each(record, attr, val)
15
9
  method_name = options[:with]
@@ -53,7 +47,7 @@ module ActiveModel
53
47
  #
54
48
  # Configuration options:
55
49
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
56
- # Runs in all validation contexts by default (nil). You can pass a symbol
50
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
57
51
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
58
52
  # <tt>on: :custom_validation_context</tt> or
59
53
  # <tt>on: [:create, :custom_validation_context]</tt>)
@@ -69,7 +63,7 @@ module ActiveModel
69
63
  # The method, proc or string should return or evaluate to a +true+ or
70
64
  # +false+ value.
71
65
  # * <tt>:strict</tt> - Specifies whether validation should be strict.
72
- # See <tt>ActiveModel::Validation#validates!</tt> for more information.
66
+ # See <tt>ActiveModel::Validations#validates!</tt> for more information.
73
67
  #
74
68
  # If you pass any additional configuration options, they will be passed
75
69
  # to the class and available as +options+:
@@ -1,9 +1,8 @@
1
- require 'active_support/core_ext/array/extract_options'
2
- require 'active_support/core_ext/hash/keys'
3
- require 'active_support/core_ext/hash/except'
1
+ # frozen_string_literal: true
4
2
 
5
- module ActiveModel
3
+ require "active_support/core_ext/array/extract_options"
6
4
 
5
+ module ActiveModel
7
6
  # == Active \Model \Validations
8
7
  #
9
8
  # Provides a full validation framework to your objects.
@@ -16,7 +15,7 @@ module ActiveModel
16
15
  # attr_accessor :first_name, :last_name
17
16
  #
18
17
  # validates_each :first_name, :last_name do |record, attr, value|
19
- # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
18
+ # record.errors.add attr, "starts with z." if value.start_with?("z")
20
19
  # end
21
20
  # end
22
21
  #
@@ -47,10 +46,10 @@ module ActiveModel
47
46
  include HelperMethods
48
47
 
49
48
  attr_accessor :validation_context
49
+ private :validation_context=
50
50
  define_callbacks :validate, scope: :name
51
51
 
52
- class_attribute :_validators
53
- self._validators = Hash.new { |h,k| h[k] = [] }
52
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
54
53
  end
55
54
 
56
55
  module ClassMethods
@@ -62,13 +61,13 @@ module ActiveModel
62
61
  # attr_accessor :first_name, :last_name
63
62
  #
64
63
  # validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
65
- # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
64
+ # record.errors.add attr, "starts with z." if value.start_with?("z")
66
65
  # end
67
66
  # end
68
67
  #
69
68
  # Options:
70
69
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
71
- # Runs in all validation contexts by default (nil). You can pass a symbol
70
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
72
71
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
73
72
  # <tt>on: :custom_validation_context</tt> or
74
73
  # <tt>on: [:create, :custom_validation_context]</tt>)
@@ -87,7 +86,7 @@ module ActiveModel
87
86
  validates_with BlockValidator, _merge_attributes(attr_names), &block
88
87
  end
89
88
 
90
- VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze
89
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
91
90
 
92
91
  # Adds a validation method or block to the class. This is useful when
93
92
  # overriding the +validate+ instance method becomes too unwieldy and
@@ -129,9 +128,12 @@ module ActiveModel
129
128
  # end
130
129
  # end
131
130
  #
131
+ # Note that the return value of validation methods is not relevant.
132
+ # It's not possible to halt the validate callback chain.
133
+ #
132
134
  # Options:
133
135
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
134
- # Runs in all validation contexts by default (nil). You can pass a symbol
136
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
135
137
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
136
138
  # <tt>on: :custom_validation_context</tt> or
137
139
  # <tt>on: [:create, :custom_validation_context]</tt>)
@@ -144,6 +146,9 @@ module ActiveModel
144
146
  # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
145
147
  # method, proc or string should return or evaluate to a +true+ or +false+
146
148
  # value.
149
+ #
150
+ # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
151
+ #
147
152
  def validate(*args, &block)
148
153
  options = args.extract_options!
149
154
 
@@ -157,14 +162,14 @@ module ActiveModel
157
162
 
158
163
  if options.key?(:on)
159
164
  options = options.dup
160
- options[:if] = Array(options[:if])
161
- options[:if].unshift ->(o) {
162
- Array(options[:on]).include?(o.validation_context)
163
- }
165
+ options[:on] = Array(options[:on])
166
+ options[:if] = [
167
+ ->(o) { !(options[:on] & Array(o.validation_context)).empty? },
168
+ *options[:if]
169
+ ]
164
170
  end
165
171
 
166
- args << options
167
- set_callback(:validate, *args, &block)
172
+ set_callback(:validate, *args, options, &block)
168
173
  end
169
174
 
170
175
  # List all validators that are being used to validate the model using
@@ -300,8 +305,6 @@ module ActiveModel
300
305
  # Runs all the specified validations and returns +true+ if no errors were
301
306
  # added otherwise +false+.
302
307
  #
303
- # Aliased as validate.
304
- #
305
308
  # class Person
306
309
  # include ActiveModel::Validations
307
310
  #
@@ -371,6 +374,15 @@ module ActiveModel
371
374
  !valid?(context)
372
375
  end
373
376
 
377
+ # Runs all the validations within the specified context. Returns +true+ if
378
+ # no errors are found, raises +ValidationError+ otherwise.
379
+ #
380
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
381
+ # some <tt>:on</tt> option will only run in the specified context.
382
+ def validate!(context = nil)
383
+ valid?(context) || raise_validation_error
384
+ end
385
+
374
386
  # Hook method defining how an attribute value should be retrieved. By default
375
387
  # this is assumed to be an instance named after the attribute. Override this
376
388
  # method in subclasses should you need to retrieve the value for a given
@@ -389,13 +401,36 @@ module ActiveModel
389
401
  # end
390
402
  alias :read_attribute_for_validation :send
391
403
 
392
- protected
393
-
394
- def run_validations! #:nodoc:
404
+ private
405
+ def run_validations!
395
406
  _run_validate_callbacks
396
407
  errors.empty?
397
408
  end
409
+
410
+ def raise_validation_error # :doc:
411
+ raise(ValidationError.new(self))
412
+ end
413
+ end
414
+
415
+ # = Active Model ValidationError
416
+ #
417
+ # Raised by <tt>validate!</tt> when the model is invalid. Use the
418
+ # +model+ method to retrieve the record which did not validate.
419
+ #
420
+ # begin
421
+ # complex_operation_that_internally_calls_validate!
422
+ # rescue ActiveModel::ValidationError => invalid
423
+ # puts invalid.model.errors
424
+ # end
425
+ class ValidationError < StandardError
426
+ attr_reader :model
427
+
428
+ def initialize(model)
429
+ @model = model
430
+ errors = @model.errors.full_messages.join(", ")
431
+ super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
432
+ end
398
433
  end
399
434
  end
400
435
 
401
- Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
436
+ Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }