activemodel 7.0.8.7 → 7.1.5.1

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 +174 -167
  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 +134 -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 +11 -6
@@ -10,7 +10,7 @@ module ActiveModel
10
10
  # keys matching the attribute names.
11
11
  #
12
12
  # If the passed hash responds to <tt>permitted?</tt> method and the return value
13
- # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
13
+ # of this method is +false+ an ActiveModel::ForbiddenAttributesError
14
14
  # exception is raised.
15
15
  #
16
16
  # class Cat
@@ -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,53 @@ 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
+ ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
220
+ attribute_method_patterns.each do |pattern|
221
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
233
222
  end
223
+ attribute_method_patterns_cache.clear
224
+ end
225
+ end
226
+
227
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
228
+ method_name = pattern.method_name(new_name).to_s
229
+ target_name = pattern.method_name(old_name).to_s
230
+ parameters = pattern.parameters
231
+ mangled_name = target_name
232
+
233
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
234
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
235
+ end
236
+
237
+ code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
238
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
239
+ "self.#{target_name}(#{parameters || ''})"
240
+ else
241
+ call_args = [":'#{target_name}'"]
242
+ call_args << parameters if parameters
243
+ "send(#{call_args.join(", ")})"
244
+ end
245
+
246
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
247
+
248
+ batch <<
249
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
250
+ body <<
251
+ "end"
234
252
  end
235
253
  end
236
254
 
@@ -245,7 +263,7 @@ module ActiveModel
245
263
  end
246
264
 
247
265
  # Declares the attributes that should be prefixed and suffixed by
248
- # <tt>ActiveModel::AttributeMethods</tt>.
266
+ # +ActiveModel::AttributeMethods+.
249
267
  #
250
268
  # To use, pass attribute names (as strings or symbols). Be sure to declare
251
269
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -269,12 +287,17 @@ module ActiveModel
269
287
  # end
270
288
  def define_attribute_methods(*attr_names)
271
289
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
272
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
290
+ attr_names.flatten.each do |attr_name|
291
+ define_attribute_method(attr_name, _owner: owner)
292
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
293
+ generate_alias_attribute_methods owner, aliased_name, attr_name
294
+ end
295
+ end
273
296
  end
274
297
  end
275
298
 
276
299
  # Declares an attribute that should be prefixed and suffixed by
277
- # <tt>ActiveModel::AttributeMethods</tt>.
300
+ # +ActiveModel::AttributeMethods+.
278
301
  #
279
302
  # To use, pass an attribute name (as string or symbol). Be sure to declare
280
303
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -301,26 +324,45 @@ module ActiveModel
301
324
  # person.name = 'Bob'
302
325
  # person.name # => "Bob"
303
326
  # person.name_short? # => true
304
- def define_attribute_method(attr_name, _owner: generated_attribute_methods)
327
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
305
328
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
306
- attribute_method_matchers.each do |matcher|
307
- method_name = matcher.method_name(attr_name)
329
+ attribute_method_patterns.each do |pattern|
330
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
331
+ end
332
+ attribute_method_patterns_cache.clear
333
+ end
334
+ end
308
335
 
309
- unless instance_method_already_implemented?(method_name)
310
- generate_method = "define_method_#{matcher.target}"
336
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
337
+ canonical_method_name = pattern.method_name(attr_name)
338
+ public_method_name = pattern.method_name(as)
339
+
340
+ # If defining a regular attribute method, we don't override methods that are explictly
341
+ # defined in parrent classes.
342
+ if instance_method_already_implemented?(public_method_name)
343
+ # However, for `alias_attribute`, we always define the method.
344
+ # We check for override second because `instance_method_already_implemented?`
345
+ # also check for dangerous methods.
346
+ return unless override
347
+ end
311
348
 
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
349
+ generate_method = "define_method_#{pattern.proxy_target}"
350
+ if respond_to?(generate_method, true)
351
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
352
+ else
353
+ define_proxy_call(
354
+ owner,
355
+ canonical_method_name,
356
+ pattern.proxy_target,
357
+ pattern.parameters,
358
+ attr_name.to_s,
359
+ namespace: :active_model_proxy,
360
+ as: public_method_name,
361
+ )
320
362
  end
321
363
  end
322
364
 
323
- # Removes all the previously dynamically defined methods from the class.
365
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
324
366
  #
325
367
  # class Person
326
368
  # include ActiveModel::AttributeMethods
@@ -328,6 +370,7 @@ module ActiveModel
328
370
  # attr_accessor :name
329
371
  # attribute_method_suffix '_short?'
330
372
  # define_attribute_method :name
373
+ # alias_attribute :first_name, :name
331
374
  #
332
375
  # private
333
376
  # def attribute_short?(attr)
@@ -337,19 +380,36 @@ module ActiveModel
337
380
  #
338
381
  # person = Person.new
339
382
  # person.name = 'Bob'
383
+ # person.first_name # => "Bob"
340
384
  # person.name_short? # => true
341
385
  #
342
386
  # Person.undefine_attribute_methods
343
387
  #
344
388
  # person.name_short? # => NoMethodError
389
+ # person.first_name # => NoMethodError
345
390
  def undefine_attribute_methods
346
391
  generated_attribute_methods.module_eval do
347
392
  undef_method(*instance_methods)
348
393
  end
349
- attribute_method_matchers_cache.clear
394
+ attribute_method_patterns_cache.clear
395
+ end
396
+
397
+ def aliases_by_attribute_name # :nodoc:
398
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
350
399
  end
351
400
 
352
401
  private
402
+ def inherited(base) # :nodoc:
403
+ super
404
+ base.class_eval do
405
+ @attribute_method_patterns_cache = nil
406
+ end
407
+ end
408
+
409
+ def resolve_attribute_name(name)
410
+ attribute_aliases.fetch(super, &:itself)
411
+ end
412
+
353
413
  def generated_attribute_methods
354
414
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
355
415
  end
@@ -367,33 +427,34 @@ module ActiveModel
367
427
  # used to alleviate the GC, which ultimately also speeds up the app
368
428
  # significantly (in our case our test suite finishes 10% faster with
369
429
  # this cache).
370
- def attribute_method_matchers_cache
371
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
430
+ def attribute_method_patterns_cache
431
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
372
432
  end
373
433
 
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) }
434
+ def attribute_method_patterns_matching(method_name)
435
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
436
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
377
437
  end
378
438
  end
379
439
 
380
440
  # Define a method `name` in `mod` that dispatches to `send`
381
441
  # using the given `extra` args. This falls back on `send`
382
442
  # if the called name cannot be compiled.
383
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
443
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
384
444
  mangled_name = name
385
445
  unless NAME_COMPILABLE_REGEXP.match?(name)
386
446
  mangled_name = "__temp__#{name.unpack1("h*")}"
387
447
  end
388
448
 
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
449
+ call_args.map!(&:inspect)
450
+ call_args << parameters if parameters
451
+ namespace = :"#{namespace}_#{proxy_target}"
392
452
 
393
- body = if CALL_COMPILABLE_REGEXP.match?(target)
394
- "self.#{target}(#{call_args.join(", ")})"
453
+ code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
454
+ body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
455
+ "self.#{proxy_target}(#{call_args.join(", ")})"
395
456
  else
396
- call_args.unshift(":'#{target}'")
457
+ call_args.unshift(":'#{proxy_target}'")
397
458
  "send(#{call_args.join(", ")})"
398
459
  end
399
460
 
@@ -406,23 +467,23 @@ module ActiveModel
406
467
  end
407
468
  end
408
469
 
409
- class AttributeMethodMatcher # :nodoc:
410
- attr_reader :prefix, :suffix, :target, :parameters
470
+ class AttributeMethodPattern # :nodoc:
471
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
411
472
 
412
- AttributeMethodMatch = Struct.new(:target, :attr_name)
473
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
413
474
 
414
475
  def initialize(prefix: "", suffix: "", parameters: nil)
415
476
  @prefix = prefix
416
477
  @suffix = suffix
417
478
  @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
418
479
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
419
- @target = "#{@prefix}attribute#{@suffix}"
480
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
420
481
  @method_name = "#{prefix}%s#{suffix}"
421
482
  end
422
483
 
423
484
  def match(method_name)
424
485
  if @regex =~ method_name
425
- AttributeMethodMatch.new(target, $1)
486
+ AttributeMethod.new(proxy_target, $1)
426
487
  end
427
488
  end
428
489
 
@@ -457,7 +518,7 @@ module ActiveModel
457
518
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
458
519
  # attribute. This method can be overloaded to customize the behavior.
459
520
  def attribute_missing(match, *args, &block)
460
- __send__(match.target, match.attr_name, *args, &block)
521
+ __send__(match.proxy_target, match.attr_name, *args, &block)
461
522
  end
462
523
  ruby2_keywords(:attribute_missing)
463
524
 
@@ -485,12 +546,12 @@ module ActiveModel
485
546
  # Returns a struct representing the matching attribute method.
486
547
  # The struct's attributes are prefix, base and suffix.
487
548
  def matched_attribute_method(method_name)
488
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
549
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
489
550
  matches.detect { |match| attribute_method?(match.attr_name) }
490
551
  end
491
552
 
492
553
  def missing_attribute(attr_name, stack)
493
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
554
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
494
555
  end
495
556
 
496
557
  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