activemodel 7.0.8.1 → 7.1.0.beta1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -196
  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 +102 -63
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +9 -0
  13. data/lib/active_model/attributes.rb +62 -45
  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 +17 -12
  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 +4 -3
  24. data/lib/active_model/model.rb +26 -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 +4 -0
  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/confirmation.rb +1 -1
  55. data/lib/active_model/validations/format.rb +6 -7
  56. data/lib/active_model/validations/length.rb +10 -8
  57. data/lib/active_model/validations/numericality.rb +35 -23
  58. data/lib/active_model/validations/presence.rb +2 -2
  59. data/lib/active_model/validations/resolve_value.rb +26 -0
  60. data/lib/active_model/validations/validates.rb +4 -4
  61. data/lib/active_model/validations/with.rb +9 -2
  62. data/lib/active_model/validations.rb +45 -10
  63. data/lib/active_model/validator.rb +7 -5
  64. data/lib/active_model/version.rb +1 -1
  65. data/lib/active_model.rb +5 -1
  66. metadata +15 -10
@@ -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,50 @@ 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
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
225
217
 
226
- modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
218
+ def generate_alias_attribute_methods(code_generator, new_name, old_name)
219
+ attribute_method_patterns.each do |pattern|
220
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
221
+ end
222
+ end
227
223
 
228
- batch <<
229
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
230
- body <<
231
- "end"
232
- end
224
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
225
+ method_name = pattern.method_name(new_name).to_s
226
+ target_name = pattern.method_name(old_name).to_s
227
+ parameters = pattern.parameters
228
+ mangled_name = target_name
229
+
230
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
231
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
232
+ end
233
+
234
+ code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
235
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
236
+ "self.#{target_name}(#{parameters || ''})"
237
+ else
238
+ call_args = [":'#{target_name}'"]
239
+ call_args << parameters if parameters
240
+ "send(#{call_args.join(", ")})"
233
241
  end
242
+
243
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
244
+
245
+ batch <<
246
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
247
+ body <<
248
+ "end"
234
249
  end
235
250
  end
236
251
 
@@ -245,7 +260,7 @@ module ActiveModel
245
260
  end
246
261
 
247
262
  # Declares the attributes that should be prefixed and suffixed by
248
- # <tt>ActiveModel::AttributeMethods</tt>.
263
+ # +ActiveModel::AttributeMethods+.
249
264
  #
250
265
  # To use, pass attribute names (as strings or symbols). Be sure to declare
251
266
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -269,12 +284,17 @@ module ActiveModel
269
284
  # end
270
285
  def define_attribute_methods(*attr_names)
271
286
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
272
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
287
+ attr_names.flatten.each do |attr_name|
288
+ define_attribute_method(attr_name, _owner: owner)
289
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
290
+ generate_alias_attribute_methods owner, aliased_name, attr_name
291
+ end
292
+ end
273
293
  end
274
294
  end
275
295
 
276
296
  # Declares an attribute that should be prefixed and suffixed by
277
- # <tt>ActiveModel::AttributeMethods</tt>.
297
+ # +ActiveModel::AttributeMethods+.
278
298
  #
279
299
  # To use, pass an attribute name (as string or symbol). Be sure to declare
280
300
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -303,24 +323,24 @@ module ActiveModel
303
323
  # person.name_short? # => true
304
324
  def define_attribute_method(attr_name, _owner: generated_attribute_methods)
305
325
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
306
- attribute_method_matchers.each do |matcher|
307
- method_name = matcher.method_name(attr_name)
326
+ attribute_method_patterns.each do |pattern|
327
+ method_name = pattern.method_name(attr_name)
308
328
 
309
329
  unless instance_method_already_implemented?(method_name)
310
- generate_method = "define_method_#{matcher.target}"
330
+ generate_method = "define_method_#{pattern.proxy_target}"
311
331
 
312
332
  if respond_to?(generate_method, true)
313
333
  send(generate_method, attr_name.to_s, owner: owner)
314
334
  else
315
- define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model_proxy)
335
+ define_proxy_call(owner, method_name, pattern.proxy_target, pattern.parameters, attr_name.to_s, namespace: :active_model_proxy)
316
336
  end
317
337
  end
318
338
  end
319
- attribute_method_matchers_cache.clear
339
+ attribute_method_patterns_cache.clear
320
340
  end
321
341
  end
322
342
 
323
- # Removes all the previously dynamically defined methods from the class.
343
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
324
344
  #
325
345
  # class Person
326
346
  # include ActiveModel::AttributeMethods
@@ -328,6 +348,7 @@ module ActiveModel
328
348
  # attr_accessor :name
329
349
  # attribute_method_suffix '_short?'
330
350
  # define_attribute_method :name
351
+ # alias_attribute :first_name, :name
331
352
  #
332
353
  # private
333
354
  # def attribute_short?(attr)
@@ -337,19 +358,36 @@ module ActiveModel
337
358
  #
338
359
  # person = Person.new
339
360
  # person.name = 'Bob'
361
+ # person.first_name # => "Bob"
340
362
  # person.name_short? # => true
341
363
  #
342
364
  # Person.undefine_attribute_methods
343
365
  #
344
366
  # person.name_short? # => NoMethodError
367
+ # person.first_name # => NoMethodError
345
368
  def undefine_attribute_methods
346
369
  generated_attribute_methods.module_eval do
347
370
  undef_method(*instance_methods)
348
371
  end
349
- attribute_method_matchers_cache.clear
372
+ attribute_method_patterns_cache.clear
373
+ end
374
+
375
+ def aliases_by_attribute_name # :nodoc:
376
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
350
377
  end
351
378
 
352
379
  private
380
+ def inherited(base) # :nodoc:
381
+ super
382
+ base.class_eval do
383
+ @attribute_method_patterns_cache = nil
384
+ end
385
+ end
386
+
387
+ def resolve_attribute_name(name)
388
+ attribute_aliases.fetch(super, &:itself)
389
+ end
390
+
353
391
  def generated_attribute_methods
354
392
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
355
393
  end
@@ -367,33 +405,34 @@ module ActiveModel
367
405
  # used to alleviate the GC, which ultimately also speeds up the app
368
406
  # significantly (in our case our test suite finishes 10% faster with
369
407
  # this cache).
370
- def attribute_method_matchers_cache
371
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
408
+ def attribute_method_patterns_cache
409
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
372
410
  end
373
411
 
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) }
412
+ def attribute_method_patterns_matching(method_name)
413
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
414
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
377
415
  end
378
416
  end
379
417
 
380
418
  # Define a method `name` in `mod` that dispatches to `send`
381
419
  # using the given `extra` args. This falls back on `send`
382
420
  # if the called name cannot be compiled.
383
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
421
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:)
384
422
  mangled_name = name
385
423
  unless NAME_COMPILABLE_REGEXP.match?(name)
386
424
  mangled_name = "__temp__#{name.unpack1("h*")}"
387
425
  end
388
426
 
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
427
+ call_args.map!(&:inspect)
428
+ call_args << parameters if parameters
429
+ namespace = :"#{namespace}_#{proxy_target}_#{call_args.join("_")}}"
392
430
 
393
- body = if CALL_COMPILABLE_REGEXP.match?(target)
394
- "self.#{target}(#{call_args.join(", ")})"
431
+ code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch|
432
+ body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
433
+ "self.#{proxy_target}(#{call_args.join(", ")})"
395
434
  else
396
- call_args.unshift(":'#{target}'")
435
+ call_args.unshift(":'#{proxy_target}'")
397
436
  "send(#{call_args.join(", ")})"
398
437
  end
399
438
 
@@ -406,23 +445,23 @@ module ActiveModel
406
445
  end
407
446
  end
408
447
 
409
- class AttributeMethodMatcher # :nodoc:
410
- attr_reader :prefix, :suffix, :target, :parameters
448
+ class AttributeMethodPattern # :nodoc:
449
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
411
450
 
412
- AttributeMethodMatch = Struct.new(:target, :attr_name)
451
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
413
452
 
414
453
  def initialize(prefix: "", suffix: "", parameters: nil)
415
454
  @prefix = prefix
416
455
  @suffix = suffix
417
456
  @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
418
457
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
419
- @target = "#{@prefix}attribute#{@suffix}"
458
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
420
459
  @method_name = "#{prefix}%s#{suffix}"
421
460
  end
422
461
 
423
462
  def match(method_name)
424
463
  if @regex =~ method_name
425
- AttributeMethodMatch.new(target, $1)
464
+ AttributeMethod.new(proxy_target, $1)
426
465
  end
427
466
  end
428
467
 
@@ -457,7 +496,7 @@ module ActiveModel
457
496
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
458
497
  # attribute. This method can be overloaded to customize the behavior.
459
498
  def attribute_missing(match, *args, &block)
460
- __send__(match.target, match.attr_name, *args, &block)
499
+ __send__(match.proxy_target, match.attr_name, *args, &block)
461
500
  end
462
501
  ruby2_keywords(:attribute_missing)
463
502
 
@@ -485,12 +524,12 @@ module ActiveModel
485
524
  # Returns a struct representing the matching attribute method.
486
525
  # The struct's attributes are prefix, base and suffix.
487
526
  def matched_attribute_method(method_name)
488
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
527
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
489
528
  matches.detect { |match| attribute_method?(match.attr_name) }
490
529
  end
491
530
 
492
531
  def missing_attribute(attr_name, stack)
493
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
532
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
494
533
  end
495
534
 
496
535
  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,6 +99,10 @@ 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
107
  attributes == other.attributes
99
108
  end
@@ -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,8 +70,7 @@ 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
@@ -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
@@ -4,14 +4,14 @@ require "active_support/core_ext/array/extract_options"
4
4
  require "active_support/core_ext/hash/keys"
5
5
 
6
6
  module ActiveModel
7
- # == Active \Model \Callbacks
7
+ # = Active \Model \Callbacks
8
8
  #
9
9
  # Provides an interface for any class to have Active Record like callbacks.
10
10
  #
11
11
  # Like the Active Record methods, the callback chain is aborted as soon as
12
12
  # one of the methods throws +:abort+.
13
13
  #
14
- # First, extend ActiveModel::Callbacks from the class you are creating:
14
+ # First, extend +ActiveModel::Callbacks+ from the class you are creating:
15
15
  #
16
16
  # class MyModel
17
17
  # extend ActiveModel::Callbacks
@@ -69,7 +69,7 @@ module ActiveModel
69
69
  end
70
70
  end
71
71
 
72
- # define_model_callbacks accepts the same options +define_callbacks+ does,
72
+ # +define_model_callbacks+ accepts the same options +define_callbacks+ does,
73
73
  # in case you want to overwrite a default. Besides that, it also accepts an
74
74
  # <tt>:only</tt> option, where you can choose if you want all types (before,
75
75
  # around or after) or just some.
@@ -77,7 +77,7 @@ module ActiveModel
77
77
  # define_model_callbacks :initialize, only: :after
78
78
  #
79
79
  # Note, the <tt>only: <type></tt> hash will apply to all callbacks defined
80
- # on that method call. To get around this you can call the define_model_callbacks
80
+ # on that method call. To get around this you can call the +define_model_callbacks+
81
81
  # method as many times as you need.
82
82
  #
83
83
  # define_model_callbacks :create, only: :after
@@ -104,7 +104,7 @@ module ActiveModel
104
104
  # end
105
105
  # end
106
106
  #
107
- # NOTE: +method_name+ passed to define_model_callbacks must not end with
107
+ # NOTE: +method_name+ passed to +define_model_callbacks+ must not end with
108
108
  # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
109
109
  def define_model_callbacks(*callbacks)
110
110
  options = callbacks.extract_options!