activemodel 4.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 }