activemodel 6.0.4 → 6.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -183
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +18 -17
  7. data/lib/active_model/attribute_assignment.rb +3 -4
  8. data/lib/active_model/attribute_methods.rb +74 -38
  9. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attribute_set/builder.rb +80 -13
  12. data/lib/active_model/attributes.rb +20 -24
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/dirty.rb +17 -4
  15. data/lib/active_model/error.rb +207 -0
  16. data/lib/active_model/errors.rb +316 -208
  17. data/lib/active_model/gem_version.rb +1 -1
  18. data/lib/active_model/lint.rb +1 -1
  19. data/lib/active_model/naming.rb +2 -2
  20. data/lib/active_model/nested_error.rb +22 -0
  21. data/lib/active_model/railtie.rb +1 -1
  22. data/lib/active_model/secure_password.rb +14 -14
  23. data/lib/active_model/serialization.rb +9 -6
  24. data/lib/active_model/serializers/json.rb +7 -0
  25. data/lib/active_model/type/date_time.rb +2 -2
  26. data/lib/active_model/type/float.rb +2 -0
  27. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  28. data/lib/active_model/type/helpers/numeric.rb +8 -3
  29. data/lib/active_model/type/helpers/time_value.rb +27 -17
  30. data/lib/active_model/type/helpers/timezone.rb +1 -1
  31. data/lib/active_model/type/immutable_string.rb +14 -10
  32. data/lib/active_model/type/integer.rb +11 -2
  33. data/lib/active_model/type/registry.rb +5 -0
  34. data/lib/active_model/type/string.rb +12 -2
  35. data/lib/active_model/type/value.rb +9 -1
  36. data/lib/active_model/validations.rb +6 -6
  37. data/lib/active_model/validations/absence.rb +1 -1
  38. data/lib/active_model/validations/acceptance.rb +1 -1
  39. data/lib/active_model/validations/callbacks.rb +15 -15
  40. data/lib/active_model/validations/clusivity.rb +5 -1
  41. data/lib/active_model/validations/confirmation.rb +2 -2
  42. data/lib/active_model/validations/exclusion.rb +1 -1
  43. data/lib/active_model/validations/format.rb +2 -2
  44. data/lib/active_model/validations/inclusion.rb +1 -1
  45. data/lib/active_model/validations/length.rb +2 -2
  46. data/lib/active_model/validations/numericality.rb +54 -41
  47. data/lib/active_model/validations/presence.rb +1 -1
  48. data/lib/active_model/validations/validates.rb +6 -4
  49. data/lib/active_model/validator.rb +7 -1
  50. metadata +9 -7
@@ -11,12 +11,22 @@ module ActiveModel
11
11
  end
12
12
  end
13
13
 
14
+ def to_immutable_string
15
+ ImmutableString.new(
16
+ true: @true,
17
+ false: @false,
18
+ limit: limit,
19
+ precision: precision,
20
+ scale: scale,
21
+ )
22
+ end
23
+
14
24
  private
15
25
  def cast_value(value)
16
26
  case value
17
27
  when ::String then ::String.new(value)
18
- when true then "t"
19
- when false then "f"
28
+ when true then @true
29
+ when false then @false
20
30
  else value.to_s
21
31
  end
22
32
  end
@@ -11,6 +11,14 @@ module ActiveModel
11
11
  @limit = limit
12
12
  end
13
13
 
14
+ # Returns true if this type can convert +value+ to a type that is usable
15
+ # by the database. For example a boolean type can return +true+ if the
16
+ # value parameter is a Ruby boolean, but may return +false+ if the value
17
+ # parameter is some other object.
18
+ def serializable?(value)
19
+ true
20
+ end
21
+
14
22
  def type # :nodoc:
15
23
  end
16
24
 
@@ -110,7 +118,7 @@ module ActiveModel
110
118
  [self.class, precision, scale, limit].hash
111
119
  end
112
120
 
113
- def assert_valid_value(*)
121
+ def assert_valid_value(_)
114
122
  end
115
123
 
116
124
  private
@@ -15,7 +15,7 @@ module ActiveModel
15
15
  # attr_accessor :first_name, :last_name
16
16
  #
17
17
  # validates_each :first_name, :last_name do |record, attr, value|
18
- # 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")
19
19
  # end
20
20
  # end
21
21
  #
@@ -61,7 +61,7 @@ module ActiveModel
61
61
  # attr_accessor :first_name, :last_name
62
62
  #
63
63
  # validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
64
- # 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")
65
65
  # end
66
66
  # end
67
67
  #
@@ -163,10 +163,10 @@ module ActiveModel
163
163
  if options.key?(:on)
164
164
  options = options.dup
165
165
  options[:on] = Array(options[:on])
166
- options[:if] = Array(options[:if])
167
- options[:if].unshift ->(o) {
168
- !(options[:on] & Array(o.validation_context)).empty?
169
- }
166
+ options[:if] = [
167
+ ->(o) { !(options[:on] & Array(o.validation_context)).empty? },
168
+ *options[:if]
169
+ ]
170
170
  end
171
171
 
172
172
  set_callback(:validate, *args, options, &block)
@@ -5,7 +5,7 @@ module ActiveModel
5
5
  # == \Active \Model Absence Validator
6
6
  class AbsenceValidator < EachValidator #:nodoc:
7
7
  def validate_each(record, attr_name, value)
8
- record.errors.add(attr_name, :present, options) if value.present?
8
+ record.errors.add(attr_name, :present, **options) if value.present?
9
9
  end
10
10
  end
11
11
 
@@ -10,7 +10,7 @@ module ActiveModel
10
10
 
11
11
  def validate_each(record, attribute, value)
12
12
  unless acceptable_option?(value)
13
- record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil))
13
+ record.errors.add(attribute, :accepted, **options.except(:accept, :allow_nil))
14
14
  end
15
15
  end
16
16
 
@@ -56,14 +56,7 @@ module ActiveModel
56
56
  def before_validation(*args, &block)
57
57
  options = args.extract_options!
58
58
 
59
- if options.key?(:on)
60
- options = options.dup
61
- options[:on] = Array(options[:on])
62
- options[:if] = Array(options[:if])
63
- options[:if].unshift ->(o) {
64
- !(options[:on] & Array(o.validation_context)).empty?
65
- }
66
- end
59
+ set_options_for_callback(options)
67
60
 
68
61
  set_callback(:validation, :before, *args, options, &block)
69
62
  end
@@ -99,16 +92,23 @@ module ActiveModel
99
92
  options = options.dup
100
93
  options[:prepend] = true
101
94
 
102
- if options.key?(:on)
103
- options[:on] = Array(options[:on])
104
- options[:if] = Array(options[:if])
105
- options[:if].unshift ->(o) {
106
- !(options[:on] & Array(o.validation_context)).empty?
107
- }
108
- end
95
+ set_options_for_callback(options)
109
96
 
110
97
  set_callback(:validation, :after, *args, options, &block)
111
98
  end
99
+
100
+ private
101
+ def set_options_for_callback(options)
102
+ if options.key?(:on)
103
+ options[:on] = Array(options[:on])
104
+ options[:if] = [
105
+ ->(o) {
106
+ !(options[:on] & Array(o.validation_context)).empty?
107
+ },
108
+ *options[:if]
109
+ ]
110
+ end
111
+ end
112
112
  end
113
113
 
114
114
  private
@@ -24,7 +24,11 @@ module ActiveModel
24
24
  delimiter
25
25
  end
26
26
 
27
- members.send(inclusion_method(members), value)
27
+ if value.is_a?(Array)
28
+ value.all? { |v| members.public_send(inclusion_method(members), v) }
29
+ else
30
+ members.public_send(inclusion_method(members), value)
31
+ end
28
32
  end
29
33
 
30
34
  def delimiter
@@ -9,10 +9,10 @@ module ActiveModel
9
9
  end
10
10
 
11
11
  def validate_each(record, attribute, value)
12
- unless (confirmed = record.send("#{attribute}_confirmation")).nil?
12
+ unless (confirmed = record.public_send("#{attribute}_confirmation")).nil?
13
13
  unless confirmation_value_equal?(record, attribute, value, confirmed)
14
14
  human_attribute_name = record.class.human_attribute_name(attribute)
15
- record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
15
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, **options.except(:case_sensitive).merge!(attribute: human_attribute_name))
16
16
  end
17
17
  end
18
18
  end
@@ -9,7 +9,7 @@ module ActiveModel
9
9
 
10
10
  def validate_each(record, attribute, value)
11
11
  if include?(record, value)
12
- record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(value: value))
12
+ record.errors.add(attribute, :exclusion, **options.except(:in, :within).merge!(value: value))
13
13
  end
14
14
  end
15
15
  end
@@ -6,7 +6,7 @@ module ActiveModel
6
6
  def validate_each(record, attribute, value)
7
7
  if options[:with]
8
8
  regexp = option_call(record, :with)
9
- record_error(record, attribute, :with, value) if !value.to_s&.match?(regexp)
9
+ record_error(record, attribute, :with, value) unless regexp.match?(value.to_s)
10
10
  elsif options[:without]
11
11
  regexp = option_call(record, :without)
12
12
  record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
@@ -29,7 +29,7 @@ module ActiveModel
29
29
  end
30
30
 
31
31
  def record_error(record, attribute, name, value)
32
- record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
32
+ record.errors.add(attribute, :invalid, **options.except(name).merge!(value: value))
33
33
  end
34
34
 
35
35
  def check_options_validity(name)
@@ -9,7 +9,7 @@ module ActiveModel
9
9
 
10
10
  def validate_each(record, attribute, value)
11
11
  unless include?(record, value)
12
- record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value))
12
+ record.errors.add(attribute, :inclusion, **options.except(:in, :within).merge!(value: value))
13
13
  end
14
14
  end
15
15
  end
@@ -51,7 +51,7 @@ module ActiveModel
51
51
  when Symbol
52
52
  check_value = record.send(check_value)
53
53
  end
54
- next if value_length.send(validity_check, check_value)
54
+ next if value_length.public_send(validity_check, check_value)
55
55
  end
56
56
 
57
57
  errors_options[:count] = check_value
@@ -59,7 +59,7 @@ module ActiveModel
59
59
  default_message = options[MESSAGES[key]]
60
60
  errors_options[:message] ||= default_message if default_message
61
61
 
62
- record.errors.add(attribute, MESSAGES[key], errors_options)
62
+ record.errors.add(attribute, MESSAGES[key], **errors_options)
63
63
  end
64
64
  end
65
65
 
@@ -24,44 +24,24 @@ module ActiveModel
24
24
  end
25
25
  end
26
26
 
27
- def validate_each(record, attr_name, value)
28
- came_from_user = :"#{attr_name}_came_from_user?"
29
-
30
- if record.respond_to?(came_from_user)
31
- if record.public_send(came_from_user)
32
- raw_value = record.read_attribute_before_type_cast(attr_name)
33
- elsif record.respond_to?(:read_attribute)
34
- raw_value = record.read_attribute(attr_name)
35
- end
36
- else
37
- before_type_cast = :"#{attr_name}_before_type_cast"
38
- if record.respond_to?(before_type_cast)
39
- raw_value = record.public_send(before_type_cast)
40
- end
41
- end
42
- raw_value ||= value
43
-
44
- if record_attribute_changed_in_place?(record, attr_name)
45
- raw_value = value
46
- end
47
-
48
- unless is_number?(raw_value)
49
- record.errors.add(attr_name, :not_a_number, filtered_options(raw_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))
50
30
  return
51
31
  end
52
32
 
53
- if allow_only_integer?(record) && !is_integer?(raw_value)
54
- record.errors.add(attr_name, :not_an_integer, 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))
55
35
  return
56
36
  end
57
37
 
58
- value = parse_as_number(raw_value)
38
+ value = parse_as_number(value, precision, scale)
59
39
 
60
40
  options.slice(*CHECKS.keys).each do |option, option_value|
61
41
  case option
62
42
  when :odd, :even
63
- unless value.to_i.send(CHECKS[option])
64
- 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))
65
45
  end
66
46
  else
67
47
  case option_value
@@ -71,34 +51,44 @@ module ActiveModel
71
51
  option_value = record.send(option_value)
72
52
  end
73
53
 
74
- option_value = parse_as_number(option_value)
54
+ option_value = parse_as_number(option_value, precision, scale)
75
55
 
76
- unless value.send(CHECKS[option], option_value)
77
- record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
56
+ unless value.public_send(CHECKS[option], option_value)
57
+ record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
78
58
  end
79
59
  end
80
60
  end
81
61
  end
82
62
 
83
63
  private
84
- def is_number?(raw_value)
85
- !parse_as_number(raw_value).nil?
86
- rescue ArgumentError, TypeError
87
- false
88
- end
89
-
90
- def parse_as_number(raw_value)
64
+ def parse_as_number(raw_value, precision, scale)
91
65
  if raw_value.is_a?(Float)
92
- raw_value.to_d(Float::DIG)
66
+ parse_float(raw_value, precision, scale)
67
+ elsif raw_value.is_a?(BigDecimal)
68
+ round(raw_value, scale)
93
69
  elsif raw_value.is_a?(Numeric)
94
70
  raw_value
95
71
  elsif is_integer?(raw_value)
96
72
  raw_value.to_i
97
73
  elsif !is_hexadecimal_literal?(raw_value)
98
- Kernel.Float(raw_value).to_d
74
+ parse_float(Kernel.Float(raw_value), precision, scale)
99
75
  end
100
76
  end
101
77
 
78
+ def parse_float(raw_value, precision, scale)
79
+ round(raw_value, scale).to_d(precision)
80
+ end
81
+
82
+ def round(raw_value, scale)
83
+ scale ? raw_value.round(scale) : raw_value
84
+ end
85
+
86
+ def is_number?(raw_value, precision, scale)
87
+ !parse_as_number(raw_value, precision, scale).nil?
88
+ rescue ArgumentError, TypeError
89
+ false
90
+ end
91
+
102
92
  def is_integer?(raw_value)
103
93
  INTEGER_REGEX.match?(raw_value.to_s)
104
94
  end
@@ -124,6 +114,27 @@ module ActiveModel
124
114
  end
125
115
  end
126
116
 
117
+ def prepare_value_for_validation(value, record, attr_name)
118
+ return value if record_attribute_changed_in_place?(record, attr_name)
119
+
120
+ came_from_user = :"#{attr_name}_came_from_user?"
121
+
122
+ if record.respond_to?(came_from_user)
123
+ if record.public_send(came_from_user)
124
+ raw_value = record.public_send(:"#{attr_name}_before_type_cast")
125
+ elsif record.respond_to?(:read_attribute)
126
+ raw_value = record.read_attribute(attr_name)
127
+ end
128
+ else
129
+ before_type_cast = :"#{attr_name}_before_type_cast"
130
+ if record.respond_to?(before_type_cast)
131
+ raw_value = record.public_send(before_type_cast)
132
+ end
133
+ end
134
+
135
+ raw_value || value
136
+ end
137
+
127
138
  def record_attribute_changed_in_place?(record, attr_name)
128
139
  record.respond_to?(:attribute_changed_in_place?) &&
129
140
  record.attribute_changed_in_place?(attr_name.to_s)
@@ -134,7 +145,8 @@ module ActiveModel
134
145
  # Validates whether the value of the specified attribute is numeric by
135
146
  # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
136
147
  # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
137
- # (if <tt>only_integer</tt> is set to +true+).
148
+ # (if <tt>only_integer</tt> is set to +true+). Precision of Kernel.Float values
149
+ # are guaranteed up to 15 digits.
138
150
  #
139
151
  # class Person < ActiveRecord::Base
140
152
  # validates_numericality_of :value, on: :create
@@ -175,6 +187,7 @@ module ActiveModel
175
187
  # * <tt>:less_than</tt>
176
188
  # * <tt>:less_than_or_equal_to</tt>
177
189
  # * <tt>:only_integer</tt>
190
+ # * <tt>:other_than</tt>
178
191
  #
179
192
  # For example:
180
193
  #
@@ -4,7 +4,7 @@ module ActiveModel
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
 
@@ -12,6 +12,7 @@ module ActiveModel
12
12
  #
13
13
  # Examples of using the default rails validators:
14
14
  #
15
+ # validates :username, absence: true
15
16
  # validates :terms, acceptance: true
16
17
  # validates :password, confirmation: true
17
18
  # validates :username, exclusion: { in: %w(admin superuser) }
@@ -27,7 +28,7 @@ module ActiveModel
27
28
  # class EmailValidator < ActiveModel::EachValidator
28
29
  # def validate_each(record, attribute, value)
29
30
  # record.errors.add attribute, (options[:message] || "is not an email") unless
30
- # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
31
+ # /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
31
32
  # end
32
33
  # end
33
34
  #
@@ -47,7 +48,7 @@ module ActiveModel
47
48
  #
48
49
  # class TitleValidator < ActiveModel::EachValidator
49
50
  # def validate_each(record, attribute, value)
50
- # 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)
51
52
  # end
52
53
  # end
53
54
  #
@@ -63,7 +64,7 @@ module ActiveModel
63
64
  # and strings in shortcut form.
64
65
  #
65
66
  # validates :email, format: /@/
66
- # validates :role, inclusion: %(admin contributor)
67
+ # validates :role, inclusion: %w(admin contributor)
67
68
  # validates :password, length: 6..20
68
69
  #
69
70
  # When using shortcut form, ranges and arrays are passed to your
@@ -112,7 +113,6 @@ module ActiveModel
112
113
  defaults[:attributes] = attributes
113
114
 
114
115
  validations.each do |key, options|
115
- next unless options
116
116
  key = "#{key.to_s.camelize}Validator"
117
117
 
118
118
  begin
@@ -121,6 +121,8 @@ module ActiveModel
121
121
  raise ArgumentError, "Unknown validator: '#{key}'"
122
122
  end
123
123
 
124
+ next unless options
125
+
124
126
  validates_with(validator, defaults.merge(_parse_validates_options(options)))
125
127
  end
126
128
  end