activemodel 6.1.7.6 → 7.0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -148
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +3 -3
  5. data/lib/active_model/api.rb +99 -0
  6. data/lib/active_model/attribute.rb +4 -0
  7. data/lib/active_model/attribute_methods.rb +65 -81
  8. data/lib/active_model/attribute_set/builder.rb +1 -10
  9. data/lib/active_model/attribute_set.rb +4 -1
  10. data/lib/active_model/attributes.rb +15 -12
  11. data/lib/active_model/callbacks.rb +1 -1
  12. data/lib/active_model/conversion.rb +2 -2
  13. data/lib/active_model/dirty.rb +5 -4
  14. data/lib/active_model/errors.rb +35 -235
  15. data/lib/active_model/gem_version.rb +4 -4
  16. data/lib/active_model/locale/en.yml +1 -0
  17. data/lib/active_model/model.rb +6 -59
  18. data/lib/active_model/naming.rb +15 -8
  19. data/lib/active_model/secure_password.rb +1 -1
  20. data/lib/active_model/serialization.rb +7 -2
  21. data/lib/active_model/translation.rb +1 -1
  22. data/lib/active_model/type/date.rb +1 -1
  23. data/lib/active_model/type/helpers/numeric.rb +9 -1
  24. data/lib/active_model/type/helpers/time_value.rb +3 -3
  25. data/lib/active_model/type/integer.rb +4 -1
  26. data/lib/active_model/type/registry.rb +8 -38
  27. data/lib/active_model/type/time.rb +1 -1
  28. data/lib/active_model/type.rb +6 -6
  29. data/lib/active_model/validations/absence.rb +1 -1
  30. data/lib/active_model/validations/clusivity.rb +1 -1
  31. data/lib/active_model/validations/comparability.rb +29 -0
  32. data/lib/active_model/validations/comparison.rb +82 -0
  33. data/lib/active_model/validations/confirmation.rb +4 -4
  34. data/lib/active_model/validations/numericality.rb +28 -21
  35. data/lib/active_model/validations.rb +4 -4
  36. data/lib/active_model/validator.rb +2 -2
  37. data/lib/active_model.rb +2 -1
  38. metadata +15 -12
@@ -67,6 +67,7 @@ module ActiveModel
67
67
 
68
68
  NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
69
69
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
70
+ FORWARD_PARAMETERS = "*args"
70
71
 
71
72
  included do
72
73
  class_attribute :attribute_aliases, instance_writer: false, default: {}
@@ -105,8 +106,8 @@ module ActiveModel
105
106
  # person.name # => "Bob"
106
107
  # person.clear_name
107
108
  # person.name # => nil
108
- def attribute_method_prefix(*prefixes)
109
- self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
109
+ def attribute_method_prefix(*prefixes, parameters: nil)
110
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new(prefix: prefix, parameters: parameters) }
110
111
  undefine_attribute_methods
111
112
  end
112
113
 
@@ -140,8 +141,8 @@ module ActiveModel
140
141
  # person.name = 'Bob'
141
142
  # person.name # => "Bob"
142
143
  # person.name_short? # => true
143
- def attribute_method_suffix(*suffixes)
144
- self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
144
+ def attribute_method_suffix(*suffixes, parameters: nil)
145
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new(suffix: suffix, parameters: parameters) }
145
146
  undefine_attribute_methods
146
147
  end
147
148
 
@@ -177,7 +178,7 @@ module ActiveModel
177
178
  # person.reset_name_to_default!
178
179
  # person.name # => 'Default Name'
179
180
  def attribute_method_affix(*affixes)
180
- self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
181
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new(**affix) }
181
182
  undefine_attribute_methods
182
183
  end
183
184
 
@@ -207,11 +208,33 @@ module ActiveModel
207
208
  # person.nickname_short? # => true
208
209
  def alias_attribute(new_name, old_name)
209
210
  self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
210
- CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
211
+ ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
211
212
  attribute_method_matchers.each do |matcher|
212
- matcher_new = matcher.method_name(new_name).to_s
213
- matcher_old = matcher.method_name(old_name).to_s
214
- define_proxy_call false, owner, matcher_new, matcher_old
213
+ method_name = matcher.method_name(new_name).to_s
214
+ target_name = matcher.method_name(old_name).to_s
215
+ parameters = matcher.parameters
216
+
217
+ mangled_name = target_name
218
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
219
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
220
+ end
221
+
222
+ code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
223
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
224
+ "self.#{target_name}(#{parameters || ''})"
225
+ else
226
+ call_args = [":'#{target_name}'"]
227
+ call_args << parameters if parameters
228
+ "send(#{call_args.join(", ")})"
229
+ end
230
+
231
+ modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
232
+
233
+ batch <<
234
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
235
+ body <<
236
+ "end"
237
+ end
215
238
  end
216
239
  end
217
240
  end
@@ -251,7 +274,7 @@ module ActiveModel
251
274
  # end
252
275
  # end
253
276
  def define_attribute_methods(*attr_names)
254
- CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
277
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
255
278
  attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
256
279
  end
257
280
  end
@@ -286,7 +309,7 @@ module ActiveModel
286
309
  # person.name # => "Bob"
287
310
  # person.name_short? # => true
288
311
  def define_attribute_method(attr_name, _owner: generated_attribute_methods)
289
- CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
312
+ ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
290
313
  attribute_method_matchers.each do |matcher|
291
314
  method_name = matcher.method_name(attr_name)
292
315
 
@@ -296,7 +319,7 @@ module ActiveModel
296
319
  if respond_to?(generate_method, true)
297
320
  send(generate_method, attr_name.to_s, owner: owner)
298
321
  else
299
- define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
322
+ define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model)
300
323
  end
301
324
  end
302
325
  end
@@ -335,46 +358,6 @@ module ActiveModel
335
358
  end
336
359
 
337
360
  private
338
- class CodeGenerator
339
- class << self
340
- def batch(owner, path, line)
341
- if owner.is_a?(CodeGenerator)
342
- yield owner
343
- else
344
- instance = new(owner, path, line)
345
- result = yield instance
346
- instance.execute
347
- result
348
- end
349
- end
350
- end
351
-
352
- def initialize(owner, path, line)
353
- @owner = owner
354
- @path = path
355
- @line = line
356
- @sources = ["# frozen_string_literal: true\n"]
357
- @renames = {}
358
- end
359
-
360
- def <<(source_line)
361
- @sources << source_line
362
- end
363
-
364
- def rename_method(old_name, new_name)
365
- @renames[old_name] = new_name
366
- end
367
-
368
- def execute
369
- @owner.module_eval(@sources.join(";"), @path, @line - 1)
370
- @renames.each do |old_name, new_name|
371
- @owner.alias_method new_name, old_name
372
- @owner.undef_method old_name
373
- end
374
- end
375
- end
376
- private_constant :CodeGenerator
377
-
378
361
  def generated_attribute_methods
379
362
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
380
363
  end
@@ -398,42 +381,48 @@ module ActiveModel
398
381
 
399
382
  def attribute_method_matchers_matching(method_name)
400
383
  attribute_method_matchers_cache.compute_if_absent(method_name) do
401
- attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
384
+ attribute_method_matchers.filter_map { |matcher| matcher.match(method_name) }
402
385
  end
403
386
  end
404
387
 
405
388
  # Define a method `name` in `mod` that dispatches to `send`
406
- # using the given `extra` args. This falls back on `define_method`
407
- # and `send` if the given names cannot be compiled.
408
- def define_proxy_call(include_private, code_generator, name, target, *extra)
409
- defn = if NAME_COMPILABLE_REGEXP.match?(name)
410
- "def #{name}(*args)"
411
- else
412
- "define_method(:'#{name}') do |*args|"
389
+ # using the given `extra` args. This falls back on `send`
390
+ # if the called name cannot be compiled.
391
+ def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
392
+ mangled_name = name
393
+ unless NAME_COMPILABLE_REGEXP.match?(name)
394
+ mangled_name = "__temp__#{name.unpack1("h*")}"
413
395
  end
414
396
 
415
- extra = (extra.map!(&:inspect) << "*args").join(", ")
397
+ code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch|
398
+ call_args.map!(&:inspect)
399
+ call_args << parameters if parameters
416
400
 
417
- body = if CALL_COMPILABLE_REGEXP.match?(target)
418
- "#{"self." unless include_private}#{target}(#{extra})"
419
- else
420
- "send(:'#{target}', #{extra})"
421
- end
401
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
402
+ "self.#{target}(#{call_args.join(", ")})"
403
+ else
404
+ call_args.unshift(":'#{target}'")
405
+ "send(#{call_args.join(", ")})"
406
+ end
407
+
408
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
422
409
 
423
- code_generator <<
424
- defn <<
425
- body <<
426
- "end" <<
427
- "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
410
+ batch <<
411
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
412
+ body <<
413
+ "end"
414
+ end
428
415
  end
429
416
 
430
- class AttributeMethodMatcher #:nodoc:
431
- attr_reader :prefix, :suffix, :target
417
+ class AttributeMethodMatcher # :nodoc:
418
+ attr_reader :prefix, :suffix, :target, :parameters
432
419
 
433
420
  AttributeMethodMatch = Struct.new(:target, :attr_name)
434
421
 
435
- def initialize(options = {})
436
- @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
422
+ def initialize(prefix: "", suffix: "", parameters: nil)
423
+ @prefix = prefix
424
+ @suffix = suffix
425
+ @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
437
426
  @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
438
427
  @target = "#{@prefix}attribute#{@suffix}"
439
428
  @method_name = "#{prefix}%s#{suffix}"
@@ -469,7 +458,7 @@ module ActiveModel
469
458
  match ? attribute_missing(match, *args, &block) : super
470
459
  end
471
460
  end
472
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
461
+ ruby2_keywords(:method_missing)
473
462
 
474
463
  # +attribute_missing+ is like +method_missing+, but for attributes. When
475
464
  # +method_missing+ is called we check to see if there is a matching
@@ -520,10 +509,6 @@ module ActiveModel
520
509
 
521
510
  # We want to generate the methods via module_eval rather than
522
511
  # define_method, because define_method is slower on dispatch.
523
- # Evaluating many similar methods may use more memory as the instruction
524
- # sequences are duplicated and cached (in MRI). define_method may
525
- # be slower on dispatch, but if you're careful about the closure
526
- # created, then define_method will consume much less memory.
527
512
  #
528
513
  # But sometimes the database might return columns with
529
514
  # characters that are not allowed in normal method names (like
@@ -547,7 +532,6 @@ module ActiveModel
547
532
  temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
548
533
  attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
549
534
  yield temp_method_name, attr_name_expr
550
- owner.rename_method(temp_method_name, method_name)
551
535
  end
552
536
  end
553
537
  end
@@ -144,16 +144,7 @@ module ActiveModel
144
144
  end
145
145
 
146
146
  def marshal_load(values)
147
- if values.is_a?(Hash)
148
- ActiveSupport::Deprecation.warn(<<~MSG)
149
- Marshalling load from legacy attributes format is deprecated and will be removed in Rails 7.0.
150
- MSG
151
- empty_hash = {}.freeze
152
- initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
153
- @materialized = true
154
- else
155
- initialize(*values)
156
- end
147
+ initialize(*values)
157
148
  end
158
149
 
159
150
  protected
@@ -25,6 +25,10 @@ module ActiveModel
25
25
  attributes.transform_values(&:value_before_type_cast)
26
26
  end
27
27
 
28
+ def values_for_database
29
+ attributes.transform_values(&:value_for_database)
30
+ end
31
+
28
32
  def to_hash
29
33
  keys.index_with { |name| self[name].value }
30
34
  end
@@ -54,7 +58,6 @@ module ActiveModel
54
58
 
55
59
  def write_cast_value(name, value)
56
60
  @attributes[name] = self[name].with_cast_value(value)
57
- value
58
61
  end
59
62
 
60
63
  def freeze
@@ -4,25 +4,26 @@ require "active_model/attribute_set"
4
4
  require "active_model/attribute/user_provided_default"
5
5
 
6
6
  module ActiveModel
7
- module Attributes #:nodoc:
7
+ module Attributes # :nodoc:
8
8
  extend ActiveSupport::Concern
9
9
  include ActiveModel::AttributeMethods
10
10
 
11
11
  included do
12
- attribute_method_suffix "="
12
+ attribute_method_suffix "=", parameters: "value"
13
13
  class_attribute :attribute_types, :_default_attributes, instance_accessor: false
14
14
  self.attribute_types = Hash.new(Type.default_value)
15
15
  self._default_attributes = AttributeSet.new({})
16
16
  end
17
17
 
18
18
  module ClassMethods
19
- def attribute(name, type = Type::Value.new, **options)
19
+ def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
20
20
  name = name.to_s
21
- if type.is_a?(Symbol)
22
- type = ActiveModel::Type.lookup(type, **options.except(:default))
23
- end
24
- self.attribute_types = attribute_types.merge(name => type)
25
- define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
21
+
22
+ cast_type = Type.lookup(cast_type, **options) if Symbol === cast_type
23
+ cast_type ||= attribute_types[name]
24
+
25
+ self.attribute_types = attribute_types.merge(name => cast_type)
26
+ define_default_attribute(name, default, cast_type)
26
27
  define_attribute_method(name)
27
28
  end
28
29
 
@@ -46,10 +47,12 @@ module ActiveModel
46
47
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
47
48
  owner, name, writer: true,
48
49
  ) do |temp_method_name, attr_name_expr|
49
- owner <<
50
- "def #{temp_method_name}(value)" <<
51
- " _write_attribute(#{attr_name_expr}, value)" <<
52
- "end"
50
+ owner.define_cached_method("#{name}=", as: temp_method_name, namespace: :active_model) do |batch|
51
+ batch <<
52
+ "def #{temp_method_name}(value)" <<
53
+ " _write_attribute(#{attr_name_expr}, value)" <<
54
+ "end"
55
+ end
53
56
  end
54
57
  end
55
58
 
@@ -63,7 +63,7 @@ module ActiveModel
63
63
  # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
64
64
  #
65
65
  module Callbacks
66
- def self.extended(base) #:nodoc:
66
+ def self.extended(base) # :nodoc:
67
67
  base.class_eval do
68
68
  include ActiveSupport::Callbacks
69
69
  end
@@ -96,10 +96,10 @@ module ActiveModel
96
96
  self.class._to_partial_path
97
97
  end
98
98
 
99
- module ClassMethods #:nodoc:
99
+ module ClassMethods # :nodoc:
100
100
  # Provide a class level cache for #to_partial_path. This is an
101
101
  # internal method and should not be accessed directly.
102
- def _to_partial_path #:nodoc:
102
+ def _to_partial_path # :nodoc:
103
103
  @_to_partial_path ||= begin
104
104
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
105
105
  collection = ActiveSupport::Inflector.tableize(name)
@@ -123,10 +123,11 @@ module ActiveModel
123
123
  include ActiveModel::AttributeMethods
124
124
 
125
125
  included do
126
- attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
127
- attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
128
- attribute_method_affix prefix: "restore_", suffix: "!"
129
- attribute_method_affix prefix: "clear_", suffix: "_change"
126
+ attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
127
+ attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
128
+ attribute_method_suffix "_previous_change", "_previously_was", parameters: false
129
+ attribute_method_affix prefix: "restore_", suffix: "!", parameters: false
130
+ attribute_method_affix prefix: "clear_", suffix: "_change", parameters: false
130
131
  end
131
132
 
132
133
  def initialize_dup(other) # :nodoc: