activemodel 7.0.8.1 → 7.1.4

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -158
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +26 -1
  9. data/lib/active_model/attribute_assignment.rb +1 -1
  10. data/lib/active_model/attribute_methods.rb +129 -73
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +10 -1
  13. data/lib/active_model/attributes.rb +65 -48
  14. data/lib/active_model/callbacks.rb +5 -5
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +4 -3
  19. data/lib/active_model/errors.rb +37 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +4 -4
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +34 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +61 -23
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +7 -1
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/numeric.rb +6 -1
  40. data/lib/active_model/type/helpers/time_value.rb +28 -12
  41. data/lib/active_model/type/immutable_string.rb +37 -1
  42. data/lib/active_model/type/integer.rb +44 -1
  43. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  44. data/lib/active_model/type/string.rb +9 -1
  45. data/lib/active_model/type/time.rb +48 -7
  46. data/lib/active_model/type/value.rb +17 -1
  47. data/lib/active_model/type.rb +1 -0
  48. data/lib/active_model/validations/absence.rb +1 -1
  49. data/lib/active_model/validations/acceptance.rb +1 -1
  50. data/lib/active_model/validations/callbacks.rb +4 -4
  51. data/lib/active_model/validations/clusivity.rb +5 -8
  52. data/lib/active_model/validations/comparability.rb +0 -11
  53. data/lib/active_model/validations/comparison.rb +15 -7
  54. data/lib/active_model/validations/format.rb +6 -7
  55. data/lib/active_model/validations/length.rb +10 -8
  56. data/lib/active_model/validations/numericality.rb +35 -23
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/resolve_value.rb +26 -0
  59. data/lib/active_model/validations/validates.rb +4 -4
  60. data/lib/active_model/validations/with.rb +9 -2
  61. data/lib/active_model/validations.rb +44 -9
  62. data/lib/active_model/validator.rb +7 -5
  63. data/lib/active_model/version.rb +1 -1
  64. data/lib/active_model.rb +5 -1
  65. metadata +13 -8
@@ -11,17 +11,17 @@ module ActiveModel
11
11
  #
12
12
  # user = User.first
13
13
  # user.pets.select(:id).first.user_id
14
- # # => ActiveModel::MissingAttributeError: missing attribute: user_id
14
+ # # => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
15
15
  class MissingAttributeError < NoMethodError
16
16
  end
17
17
 
18
- # == Active \Model \Attribute \Methods
18
+ # = Active \Model \Attribute \Methods
19
19
  #
20
20
  # Provides a way to add prefixes and suffixes to your methods as
21
- # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
21
+ # well as handling the creation of ActiveRecord::Base - like
22
22
  # class methods such as +table_name+.
23
23
  #
24
- # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
24
+ # The requirements to implement +ActiveModel::AttributeMethods+ are to:
25
25
  #
26
26
  # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
27
27
  # * Call each of its methods you want to add, such as +attribute_method_suffix+
@@ -70,7 +70,7 @@ module ActiveModel
70
70
 
71
71
  included do
72
72
  class_attribute :attribute_aliases, instance_writer: false, default: {}
73
- class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
73
+ class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
74
74
  end
75
75
 
76
76
  module ClassMethods
@@ -105,7 +105,7 @@ module ActiveModel
105
105
  # person.clear_name
106
106
  # person.name # => nil
107
107
  def attribute_method_prefix(*prefixes, parameters: nil)
108
- self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new(prefix: prefix, parameters: parameters) }
108
+ self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
109
109
  undefine_attribute_methods
110
110
  end
111
111
 
@@ -139,7 +139,7 @@ module ActiveModel
139
139
  # person.name # => "Bob"
140
140
  # person.name_short? # => true
141
141
  def attribute_method_suffix(*suffixes, parameters: nil)
142
- self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new(suffix: suffix, parameters: parameters) }
142
+ self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
143
143
  undefine_attribute_methods
144
144
  end
145
145
 
@@ -174,7 +174,7 @@ module ActiveModel
174
174
  # person.reset_name_to_default!
175
175
  # person.name # => 'Default Name'
176
176
  def attribute_method_affix(*affixes)
177
- self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new(**affix) }
177
+ self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
178
178
  undefine_attribute_methods
179
179
  end
180
180
 
@@ -202,35 +202,48 @@ module ActiveModel
202
202
  # person.name_short? # => true
203
203
  # person.nickname_short? # => true
204
204
  def alias_attribute(new_name, old_name)
205
- self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
206
- ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
207
- attribute_method_matchers.each do |matcher|
208
- method_name = matcher.method_name(new_name).to_s
209
- target_name = matcher.method_name(old_name).to_s
210
- parameters = matcher.parameters
211
-
212
- mangled_name = target_name
213
- unless NAME_COMPILABLE_REGEXP.match?(target_name)
214
- mangled_name = "__temp__#{target_name.unpack1("h*")}"
215
- end
205
+ old_name = old_name.to_s
206
+ new_name = new_name.to_s
207
+ self.attribute_aliases = attribute_aliases.merge(new_name => old_name)
208
+ aliases_by_attribute_name[old_name] << new_name
209
+ eagerly_generate_alias_attribute_methods(new_name, old_name)
210
+ end
216
211
 
217
- code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
218
- body = if CALL_COMPILABLE_REGEXP.match?(target_name)
219
- "self.#{target_name}(#{parameters || ''})"
220
- else
221
- call_args = [":'#{target_name}'"]
222
- call_args << parameters if parameters
223
- "send(#{call_args.join(", ")})"
224
- end
225
-
226
- modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
227
-
228
- batch <<
229
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
230
- body <<
231
- "end"
232
- end
212
+ def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
213
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
214
+ generate_alias_attribute_methods(code_generator, new_name, old_name)
215
+ end
216
+ end
217
+
218
+ def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
219
+ define_attribute_method(old_name, _owner: code_generator, as: new_name)
220
+ end
221
+
222
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
223
+ method_name = pattern.method_name(new_name).to_s
224
+ target_name = pattern.method_name(old_name).to_s
225
+ parameters = pattern.parameters
226
+ mangled_name = target_name
227
+
228
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
229
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
230
+ end
231
+
232
+ code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
233
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
234
+ "self.#{target_name}(#{parameters || ''})"
235
+ else
236
+ call_args = [":'#{target_name}'"]
237
+ call_args << parameters if parameters
238
+ "send(#{call_args.join(", ")})"
233
239
  end
240
+
241
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
242
+
243
+ batch <<
244
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
245
+ body <<
246
+ "end"
234
247
  end
235
248
  end
236
249
 
@@ -245,7 +258,7 @@ module ActiveModel
245
258
  end
246
259
 
247
260
  # Declares the attributes that should be prefixed and suffixed by
248
- # <tt>ActiveModel::AttributeMethods</tt>.
261
+ # +ActiveModel::AttributeMethods+.
249
262
  #
250
263
  # To use, pass attribute names (as strings or symbols). Be sure to declare
251
264
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -269,12 +282,17 @@ module ActiveModel
269
282
  # end
270
283
  def define_attribute_methods(*attr_names)
271
284
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
272
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
285
+ attr_names.flatten.each do |attr_name|
286
+ define_attribute_method(attr_name, _owner: owner)
287
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
288
+ generate_alias_attribute_methods owner, aliased_name, attr_name
289
+ end
290
+ end
273
291
  end
274
292
  end
275
293
 
276
294
  # Declares an attribute that should be prefixed and suffixed by
277
- # <tt>ActiveModel::AttributeMethods</tt>.
295
+ # +ActiveModel::AttributeMethods+.
278
296
  #
279
297
  # To use, pass an attribute name (as string or symbol). Be sure to declare
280
298
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -301,26 +319,45 @@ module ActiveModel
301
319
  # person.name = 'Bob'
302
320
  # person.name # => "Bob"
303
321
  # person.name_short? # => true
304
- def define_attribute_method(attr_name, _owner: generated_attribute_methods)
322
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
305
323
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
306
- attribute_method_matchers.each do |matcher|
307
- method_name = matcher.method_name(attr_name)
324
+ attribute_method_patterns.each do |pattern|
325
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
326
+ end
327
+ attribute_method_patterns_cache.clear
328
+ end
329
+ end
308
330
 
309
- unless instance_method_already_implemented?(method_name)
310
- generate_method = "define_method_#{matcher.target}"
331
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
332
+ canonical_method_name = pattern.method_name(attr_name)
333
+ public_method_name = pattern.method_name(as)
334
+
335
+ # If defining a regular attribute method, we don't override methods that are explictly
336
+ # defined in parrent classes.
337
+ if instance_method_already_implemented?(public_method_name)
338
+ # However, for `alias_attribute`, we always define the method.
339
+ # We check for override second because `instance_method_already_implemented?`
340
+ # also check for dangerous methods.
341
+ return unless override
342
+ end
311
343
 
312
- if respond_to?(generate_method, true)
313
- send(generate_method, attr_name.to_s, owner: owner)
314
- else
315
- define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model_proxy)
316
- end
317
- end
318
- end
319
- attribute_method_matchers_cache.clear
344
+ generate_method = "define_method_#{pattern.proxy_target}"
345
+ if respond_to?(generate_method, true)
346
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
347
+ else
348
+ define_proxy_call(
349
+ owner,
350
+ canonical_method_name,
351
+ pattern.proxy_target,
352
+ pattern.parameters,
353
+ attr_name.to_s,
354
+ namespace: :active_model_proxy,
355
+ as: public_method_name,
356
+ )
320
357
  end
321
358
  end
322
359
 
323
- # Removes all the previously dynamically defined methods from the class.
360
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
324
361
  #
325
362
  # class Person
326
363
  # include ActiveModel::AttributeMethods
@@ -328,6 +365,7 @@ module ActiveModel
328
365
  # attr_accessor :name
329
366
  # attribute_method_suffix '_short?'
330
367
  # define_attribute_method :name
368
+ # alias_attribute :first_name, :name
331
369
  #
332
370
  # private
333
371
  # def attribute_short?(attr)
@@ -337,19 +375,36 @@ module ActiveModel
337
375
  #
338
376
  # person = Person.new
339
377
  # person.name = 'Bob'
378
+ # person.first_name # => "Bob"
340
379
  # person.name_short? # => true
341
380
  #
342
381
  # Person.undefine_attribute_methods
343
382
  #
344
383
  # person.name_short? # => NoMethodError
384
+ # person.first_name # => NoMethodError
345
385
  def undefine_attribute_methods
346
386
  generated_attribute_methods.module_eval do
347
387
  undef_method(*instance_methods)
348
388
  end
349
- attribute_method_matchers_cache.clear
389
+ attribute_method_patterns_cache.clear
390
+ end
391
+
392
+ def aliases_by_attribute_name # :nodoc:
393
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
350
394
  end
351
395
 
352
396
  private
397
+ def inherited(base) # :nodoc:
398
+ super
399
+ base.class_eval do
400
+ @attribute_method_patterns_cache = nil
401
+ end
402
+ end
403
+
404
+ def resolve_attribute_name(name)
405
+ attribute_aliases.fetch(super, &:itself)
406
+ end
407
+
353
408
  def generated_attribute_methods
354
409
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
355
410
  end
@@ -367,33 +422,34 @@ module ActiveModel
367
422
  # used to alleviate the GC, which ultimately also speeds up the app
368
423
  # significantly (in our case our test suite finishes 10% faster with
369
424
  # this cache).
370
- def attribute_method_matchers_cache
371
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
425
+ def attribute_method_patterns_cache
426
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
372
427
  end
373
428
 
374
- def attribute_method_matchers_matching(method_name)
375
- attribute_method_matchers_cache.compute_if_absent(method_name) do
376
- attribute_method_matchers.filter_map { |matcher| matcher.match(method_name) }
429
+ def attribute_method_patterns_matching(method_name)
430
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
431
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
377
432
  end
378
433
  end
379
434
 
380
435
  # Define a method `name` in `mod` that dispatches to `send`
381
436
  # using the given `extra` args. This falls back on `send`
382
437
  # if the called name cannot be compiled.
383
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
438
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
384
439
  mangled_name = name
385
440
  unless NAME_COMPILABLE_REGEXP.match?(name)
386
441
  mangled_name = "__temp__#{name.unpack1("h*")}"
387
442
  end
388
443
 
389
- code_generator.define_cached_method(name, as: mangled_name, namespace: :"#{namespace}_#{target}") do |batch|
390
- call_args.map!(&:inspect)
391
- call_args << parameters if parameters
444
+ call_args.map!(&:inspect)
445
+ call_args << parameters if parameters
446
+ namespace = :"#{namespace}_#{proxy_target}"
392
447
 
393
- body = if CALL_COMPILABLE_REGEXP.match?(target)
394
- "self.#{target}(#{call_args.join(", ")})"
448
+ code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
449
+ body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
450
+ "self.#{proxy_target}(#{call_args.join(", ")})"
395
451
  else
396
- call_args.unshift(":'#{target}'")
452
+ call_args.unshift(":'#{proxy_target}'")
397
453
  "send(#{call_args.join(", ")})"
398
454
  end
399
455
 
@@ -406,23 +462,23 @@ module ActiveModel
406
462
  end
407
463
  end
408
464
 
409
- class AttributeMethodMatcher # :nodoc:
410
- attr_reader :prefix, :suffix, :target, :parameters
465
+ class AttributeMethodPattern # :nodoc:
466
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
411
467
 
412
- AttributeMethodMatch = Struct.new(:target, :attr_name)
468
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
413
469
 
414
470
  def initialize(prefix: "", suffix: "", parameters: nil)
415
471
  @prefix = prefix
416
472
  @suffix = suffix
417
473
  @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
418
474
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
419
- @target = "#{@prefix}attribute#{@suffix}"
475
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
420
476
  @method_name = "#{prefix}%s#{suffix}"
421
477
  end
422
478
 
423
479
  def match(method_name)
424
480
  if @regex =~ method_name
425
- AttributeMethodMatch.new(target, $1)
481
+ AttributeMethod.new(proxy_target, $1)
426
482
  end
427
483
  end
428
484
 
@@ -457,7 +513,7 @@ module ActiveModel
457
513
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
458
514
  # attribute. This method can be overloaded to customize the behavior.
459
515
  def attribute_missing(match, *args, &block)
460
- __send__(match.target, match.attr_name, *args, &block)
516
+ __send__(match.proxy_target, match.attr_name, *args, &block)
461
517
  end
462
518
  ruby2_keywords(:attribute_missing)
463
519
 
@@ -485,12 +541,12 @@ module ActiveModel
485
541
  # Returns a struct representing the matching attribute method.
486
542
  # The struct's attributes are prefix, base and suffix.
487
543
  def matched_attribute_method(method_name)
488
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
544
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
489
545
  matches.detect { |match| attribute_method?(match.attr_name) }
490
546
  end
491
547
 
492
548
  def missing_attribute(attr_name, stack)
493
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
549
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
494
550
  end
495
551
 
496
552
  def _read_attribute(attr)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/subclasses"
4
+ require "active_model/attribute_set"
5
+ require "active_model/attribute/user_provided_default"
6
+
7
+ module ActiveModel
8
+ module AttributeRegistration # :nodoc:
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods # :nodoc:
12
+ def attribute(name, type = nil, default: (no_default = true), **options)
13
+ type = resolve_type_name(type, **options) if type.is_a?(Symbol)
14
+
15
+ pending = pending_attribute(name)
16
+ pending.type = type if type
17
+ pending.default = default unless no_default
18
+
19
+ reset_default_attributes
20
+ end
21
+
22
+ def _default_attributes # :nodoc:
23
+ @default_attributes ||= build_default_attributes
24
+ end
25
+
26
+ def attribute_types # :nodoc:
27
+ @attribute_types ||= _default_attributes.cast_types.tap do |hash|
28
+ hash.default = Type.default_value
29
+ end
30
+ end
31
+
32
+ private
33
+ class PendingAttribute # :nodoc:
34
+ attr_accessor :type, :default
35
+
36
+ def apply_to(attribute)
37
+ attribute = attribute.with_type(type || attribute.type)
38
+ attribute = attribute.with_user_default(default) if defined?(@default)
39
+ attribute
40
+ end
41
+ end
42
+
43
+ def pending_attribute(name)
44
+ @pending_attributes ||= {}
45
+ @pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
46
+ end
47
+
48
+ def apply_pending_attributes(attribute_set)
49
+ superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
50
+
51
+ defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
52
+ attribute_set[name] = pending.apply_to(attribute_set[name])
53
+ end
54
+
55
+ attribute_set
56
+ end
57
+
58
+ def build_default_attributes
59
+ apply_pending_attributes(AttributeSet.new({}))
60
+ end
61
+
62
+ def reset_default_attributes
63
+ @default_attributes = nil
64
+ @attribute_types = nil
65
+ subclasses.each { |subclass| subclass.send(__method__) }
66
+ end
67
+
68
+ def resolve_attribute_name(name)
69
+ name.to_s
70
+ end
71
+
72
+ def resolve_type_name(name, **options)
73
+ Type.lookup(name, **options)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -21,6 +21,10 @@ module ActiveModel
21
21
  @attributes[name] = value
22
22
  end
23
23
 
24
+ def cast_types
25
+ attributes.transform_values(&:type)
26
+ end
27
+
24
28
  def values_before_type_cast
25
29
  attributes.transform_values(&:value_before_type_cast)
26
30
  end
@@ -37,6 +41,7 @@ module ActiveModel
37
41
  def key?(name)
38
42
  attributes.key?(name) && self[name].initialized?
39
43
  end
44
+ alias :include? :key?
40
45
 
41
46
  def keys
42
47
  attributes.each_key.select { |name| self[name].initialized? }
@@ -94,8 +99,12 @@ module ActiveModel
94
99
  AttributeSet.new(new_attributes)
95
100
  end
96
101
 
102
+ def reverse_merge!(target_attributes)
103
+ attributes.reverse_merge!(target_attributes.attributes) && self
104
+ end
105
+
97
106
  def ==(other)
98
- attributes == other.attributes
107
+ other.is_a?(AttributeSet) && attributes == other.send(:attributes)
99
108
  end
100
109
 
101
110
  protected
@@ -1,33 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_model/attribute_set"
4
- require "active_model/attribute/user_provided_default"
5
-
6
3
  module ActiveModel
7
- module Attributes # :nodoc:
4
+ # = Active \Model \Attributes
5
+ #
6
+ # The Attributes module allows models to define attributes beyond simple Ruby
7
+ # readers and writers. Similar to Active Record attributes, which are
8
+ # typically inferred from the database schema, Active Model Attributes are
9
+ # aware of data types, can have default values, and can handle casting and
10
+ # serialization.
11
+ #
12
+ # To use Attributes, include the module in your model class and define your
13
+ # attributes using the +attribute+ macro. It accepts a name, a type, a default
14
+ # value, and any other options supported by the attribute type.
15
+ #
16
+ # ==== Examples
17
+ #
18
+ # class Person
19
+ # include ActiveModel::Attributes
20
+ #
21
+ # attribute :name, :string
22
+ # attribute :active, :boolean, default: true
23
+ # end
24
+ #
25
+ # person = Person.new
26
+ # person.name = "Volmer"
27
+ #
28
+ # person.name # => "Volmer"
29
+ # person.active # => true
30
+ module Attributes
8
31
  extend ActiveSupport::Concern
32
+ include ActiveModel::AttributeRegistration
9
33
  include ActiveModel::AttributeMethods
10
34
 
11
35
  included do
12
36
  attribute_method_suffix "=", parameters: "value"
13
- class_attribute :attribute_types, :_default_attributes, instance_accessor: false
14
- self.attribute_types = Hash.new(Type.default_value)
15
- self._default_attributes = AttributeSet.new({})
16
37
  end
17
38
 
18
39
  module ClassMethods
19
- def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
20
- name = name.to_s
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)
40
+ ##
41
+ # :call-seq: attribute(name, cast_type = nil, default: nil, **options)
42
+ #
43
+ # Defines a model attribute. In addition to the attribute name, a cast
44
+ # type and default value may be specified, as well as any options
45
+ # supported by the given cast type.
46
+ #
47
+ # class Person
48
+ # include ActiveModel::Attributes
49
+ #
50
+ # attribute :name, :string
51
+ # attribute :active, :boolean, default: true
52
+ # end
53
+ #
54
+ # person = Person.new
55
+ # person.name = "Volmer"
56
+ #
57
+ # person.name # => "Volmer"
58
+ # person.active # => true
59
+ def attribute(name, ...)
60
+ super
27
61
  define_attribute_method(name)
28
62
  end
29
63
 
30
- # Returns an array of attribute names as strings
64
+ # Returns an array of attribute names as strings.
31
65
  #
32
66
  # class Person
33
67
  # include ActiveModel::Attributes
@@ -36,18 +70,17 @@ module ActiveModel
36
70
  # attribute :age, :integer
37
71
  # end
38
72
  #
39
- # Person.attribute_names
40
- # # => ["name", "age"]
73
+ # Person.attribute_names # => ["name", "age"]
41
74
  def attribute_names
42
75
  attribute_types.keys
43
76
  end
44
77
 
45
78
  private
46
- def define_method_attribute=(name, owner:)
79
+ def define_method_attribute=(canonical_name, owner:, as: canonical_name)
47
80
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
48
- owner, name, writer: true,
81
+ owner, canonical_name, writer: true,
49
82
  ) do |temp_method_name, attr_name_expr|
50
- owner.define_cached_method("#{name}=", as: temp_method_name, namespace: :active_model) do |batch|
83
+ owner.define_cached_method(temp_method_name, as: "#{as}=", namespace: :active_model) do |batch|
51
84
  batch <<
52
85
  "def #{temp_method_name}(value)" <<
53
86
  " _write_attribute(#{attr_name_expr}, value)" <<
@@ -55,27 +88,9 @@ module ActiveModel
55
88
  end
56
89
  end
57
90
  end
58
-
59
- NO_DEFAULT_PROVIDED = Object.new # :nodoc:
60
- private_constant :NO_DEFAULT_PROVIDED
61
-
62
- def define_default_attribute(name, value, type)
63
- self._default_attributes = _default_attributes.deep_dup
64
- if value == NO_DEFAULT_PROVIDED
65
- default_attribute = _default_attributes[name].with_type(type)
66
- else
67
- default_attribute = Attribute::UserProvidedDefault.new(
68
- name,
69
- value,
70
- type,
71
- _default_attributes.fetch(name.to_s) { nil },
72
- )
73
- end
74
- _default_attributes[name] = default_attribute
75
- end
76
91
  end
77
92
 
78
- def initialize(*)
93
+ def initialize(*) # :nodoc:
79
94
  @attributes = self.class._default_attributes.deep_dup
80
95
  super
81
96
  end
@@ -85,7 +100,8 @@ module ActiveModel
85
100
  super
86
101
  end
87
102
 
88
- # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
103
+ # Returns a hash of all the attributes with their names as keys and the
104
+ # values of the attributes as values.
89
105
  #
90
106
  # class Person
91
107
  # include ActiveModel::Attributes
@@ -94,14 +110,16 @@ module ActiveModel
94
110
  # attribute :age, :integer
95
111
  # end
96
112
  #
97
- # person = Person.new(name: 'Francesco', age: 22)
98
- # person.attributes
99
- # # => {"name"=>"Francesco", "age"=>22}
113
+ # person = Person.new
114
+ # person.name = "Francesco"
115
+ # person.age = 22
116
+ #
117
+ # person.attributes # => { "name" => "Francesco", "age" => 22}
100
118
  def attributes
101
119
  @attributes.to_hash
102
120
  end
103
121
 
104
- # Returns an array of attribute names as strings
122
+ # Returns an array of attribute names as strings.
105
123
  #
106
124
  # class Person
107
125
  # include ActiveModel::Attributes
@@ -111,13 +129,12 @@ module ActiveModel
111
129
  # end
112
130
  #
113
131
  # person = Person.new
114
- # person.attribute_names
115
- # # => ["name", "age"]
132
+ # person.attribute_names # => ["name", "age"]
116
133
  def attribute_names
117
134
  @attributes.keys
118
135
  end
119
136
 
120
- def freeze
137
+ def freeze # :nodoc:
121
138
  @attributes = @attributes.clone.freeze unless frozen?
122
139
  super
123
140
  end