activemodel 7.0.8.7 → 7.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -226
  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 +18 -13
@@ -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!