activemodel 7.0.8 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -256
- data/MIT-LICENSE +1 -1
- data/README.rdoc +18 -18
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +5 -5
- data/lib/active_model/attribute/user_provided_default.rb +4 -0
- data/lib/active_model/attribute.rb +27 -2
- data/lib/active_model/attribute_assignment.rb +4 -2
- data/lib/active_model/attribute_methods.rb +140 -85
- data/lib/active_model/attribute_registration.rb +117 -0
- data/lib/active_model/attribute_set.rb +10 -1
- data/lib/active_model/attributes.rb +78 -48
- data/lib/active_model/callbacks.rb +6 -6
- data/lib/active_model/conversion.rb +14 -4
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +134 -13
- data/lib/active_model/error.rb +4 -3
- data/lib/active_model/errors.rb +37 -6
- data/lib/active_model/forbidden_attributes_protection.rb +2 -0
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/locale/en.yml +1 -0
- data/lib/active_model/model.rb +34 -2
- data/lib/active_model/naming.rb +29 -10
- data/lib/active_model/railtie.rb +4 -0
- data/lib/active_model/secure_password.rb +62 -24
- data/lib/active_model/serialization.rb +3 -3
- data/lib/active_model/serializers/json.rb +1 -1
- data/lib/active_model/translation.rb +18 -16
- data/lib/active_model/type/big_integer.rb +23 -1
- data/lib/active_model/type/binary.rb +7 -1
- data/lib/active_model/type/boolean.rb +11 -9
- data/lib/active_model/type/date.rb +28 -2
- data/lib/active_model/type/date_time.rb +45 -3
- data/lib/active_model/type/decimal.rb +39 -1
- data/lib/active_model/type/float.rb +30 -1
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
- data/lib/active_model/type/helpers/numeric.rb +6 -1
- data/lib/active_model/type/helpers/time_value.rb +50 -13
- data/lib/active_model/type/helpers/timezone.rb +5 -1
- data/lib/active_model/type/immutable_string.rb +37 -1
- data/lib/active_model/type/integer.rb +44 -1
- data/lib/active_model/type/registry.rb +2 -3
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +9 -1
- data/lib/active_model/type/time.rb +48 -7
- data/lib/active_model/type/value.rb +17 -1
- data/lib/active_model/type.rb +1 -0
- data/lib/active_model/validations/absence.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +1 -1
- data/lib/active_model/validations/callbacks.rb +5 -5
- data/lib/active_model/validations/clusivity.rb +5 -8
- data/lib/active_model/validations/comparability.rb +0 -11
- data/lib/active_model/validations/comparison.rb +16 -8
- data/lib/active_model/validations/format.rb +6 -7
- data/lib/active_model/validations/length.rb +10 -8
- data/lib/active_model/validations/numericality.rb +35 -23
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +4 -4
- data/lib/active_model/validations/with.rb +9 -2
- data/lib/active_model/validations.rb +44 -9
- data/lib/active_model/validator.rb +7 -5
- data/lib/active_model/version.rb +1 -1
- data/lib/active_model.rb +5 -1
- metadata +14 -9
|
@@ -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
|
|
14
|
+
# # => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
|
|
15
15
|
class MissingAttributeError < NoMethodError
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
#
|
|
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
|
|
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
|
|
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+
|
|
@@ -66,11 +66,10 @@ module ActiveModel
|
|
|
66
66
|
|
|
67
67
|
NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
|
|
68
68
|
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
|
|
69
|
-
FORWARD_PARAMETERS = "*args"
|
|
70
69
|
|
|
71
70
|
included do
|
|
72
71
|
class_attribute :attribute_aliases, instance_writer: false, default: {}
|
|
73
|
-
class_attribute :
|
|
72
|
+
class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
module ClassMethods
|
|
@@ -105,7 +104,7 @@ module ActiveModel
|
|
|
105
104
|
# person.clear_name
|
|
106
105
|
# person.name # => nil
|
|
107
106
|
def attribute_method_prefix(*prefixes, parameters: nil)
|
|
108
|
-
self.
|
|
107
|
+
self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
|
|
109
108
|
undefine_attribute_methods
|
|
110
109
|
end
|
|
111
110
|
|
|
@@ -139,7 +138,7 @@ module ActiveModel
|
|
|
139
138
|
# person.name # => "Bob"
|
|
140
139
|
# person.name_short? # => true
|
|
141
140
|
def attribute_method_suffix(*suffixes, parameters: nil)
|
|
142
|
-
self.
|
|
141
|
+
self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
|
|
143
142
|
undefine_attribute_methods
|
|
144
143
|
end
|
|
145
144
|
|
|
@@ -174,7 +173,7 @@ module ActiveModel
|
|
|
174
173
|
# person.reset_name_to_default!
|
|
175
174
|
# person.name # => 'Default Name'
|
|
176
175
|
def attribute_method_affix(*affixes)
|
|
177
|
-
self.
|
|
176
|
+
self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
|
|
178
177
|
undefine_attribute_methods
|
|
179
178
|
end
|
|
180
179
|
|
|
@@ -202,38 +201,36 @@ module ActiveModel
|
|
|
202
201
|
# person.name_short? # => true
|
|
203
202
|
# person.nickname_short? # => true
|
|
204
203
|
def alias_attribute(new_name, old_name)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
mangled_name = target_name
|
|
213
|
-
unless NAME_COMPILABLE_REGEXP.match?(target_name)
|
|
214
|
-
mangled_name = "__temp__#{target_name.unpack1("h*")}"
|
|
215
|
-
end
|
|
204
|
+
old_name = old_name.to_s
|
|
205
|
+
new_name = new_name.to_s
|
|
206
|
+
self.attribute_aliases = attribute_aliases.merge(new_name => old_name)
|
|
207
|
+
aliases_by_attribute_name[old_name] << new_name
|
|
208
|
+
eagerly_generate_alias_attribute_methods(new_name, old_name)
|
|
209
|
+
end
|
|
216
210
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
233
|
-
end
|
|
211
|
+
def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
|
|
212
|
+
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
|
|
213
|
+
generate_alias_attribute_methods(code_generator, new_name, old_name)
|
|
234
214
|
end
|
|
235
215
|
end
|
|
236
216
|
|
|
217
|
+
def generate_alias_attribute_methods(code_generator, new_name, old_name)
|
|
218
|
+
define_attribute_method(old_name, _owner: code_generator, as: new_name)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
|
|
222
|
+
method_name = pattern.method_name(new_name).to_s
|
|
223
|
+
target_name = pattern.method_name(old_name).to_s
|
|
224
|
+
parameters = pattern.parameters
|
|
225
|
+
|
|
226
|
+
mangled_name = build_mangled_name(target_name)
|
|
227
|
+
|
|
228
|
+
call_args = []
|
|
229
|
+
call_args << parameters if parameters
|
|
230
|
+
|
|
231
|
+
define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute)
|
|
232
|
+
end
|
|
233
|
+
|
|
237
234
|
# Is +new_name+ an alias?
|
|
238
235
|
def attribute_alias?(new_name)
|
|
239
236
|
attribute_aliases.key? new_name.to_s
|
|
@@ -245,7 +242,7 @@ module ActiveModel
|
|
|
245
242
|
end
|
|
246
243
|
|
|
247
244
|
# Declares the attributes that should be prefixed and suffixed by
|
|
248
|
-
#
|
|
245
|
+
# +ActiveModel::AttributeMethods+.
|
|
249
246
|
#
|
|
250
247
|
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
|
251
248
|
# +define_attribute_methods+ after you define any prefix, suffix, or affix
|
|
@@ -269,12 +266,17 @@ module ActiveModel
|
|
|
269
266
|
# end
|
|
270
267
|
def define_attribute_methods(*attr_names)
|
|
271
268
|
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
|
272
|
-
attr_names.flatten.each
|
|
269
|
+
attr_names.flatten.each do |attr_name|
|
|
270
|
+
define_attribute_method(attr_name, _owner: owner)
|
|
271
|
+
aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
|
|
272
|
+
generate_alias_attribute_methods owner, aliased_name, attr_name
|
|
273
|
+
end
|
|
274
|
+
end
|
|
273
275
|
end
|
|
274
276
|
end
|
|
275
277
|
|
|
276
278
|
# Declares an attribute that should be prefixed and suffixed by
|
|
277
|
-
#
|
|
279
|
+
# +ActiveModel::AttributeMethods+.
|
|
278
280
|
#
|
|
279
281
|
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
|
280
282
|
# +define_attribute_method+ after you define any prefix, suffix or affix
|
|
@@ -301,26 +303,46 @@ module ActiveModel
|
|
|
301
303
|
# person.name = 'Bob'
|
|
302
304
|
# person.name # => "Bob"
|
|
303
305
|
# person.name_short? # => true
|
|
304
|
-
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
|
306
|
+
def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
|
|
305
307
|
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
attribute_method_patterns.each do |pattern|
|
|
309
|
+
define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
|
|
310
|
+
end
|
|
311
|
+
attribute_method_patterns_cache.clear
|
|
312
|
+
end
|
|
313
|
+
end
|
|
308
314
|
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
|
|
316
|
+
canonical_method_name = pattern.method_name(attr_name)
|
|
317
|
+
public_method_name = pattern.method_name(as)
|
|
318
|
+
|
|
319
|
+
# If defining a regular attribute method, we don't override methods that are explictly
|
|
320
|
+
# defined in parrent classes.
|
|
321
|
+
if instance_method_already_implemented?(public_method_name)
|
|
322
|
+
# However, for `alias_attribute`, we always define the method.
|
|
323
|
+
# We check for override second because `instance_method_already_implemented?`
|
|
324
|
+
# also check for dangerous methods.
|
|
325
|
+
return unless override
|
|
326
|
+
end
|
|
311
327
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
generate_method = "define_method_#{pattern.proxy_target}"
|
|
329
|
+
|
|
330
|
+
if respond_to?(generate_method, true)
|
|
331
|
+
send(generate_method, attr_name.to_s, owner: owner, as: as)
|
|
332
|
+
else
|
|
333
|
+
define_proxy_call(
|
|
334
|
+
owner,
|
|
335
|
+
canonical_method_name,
|
|
336
|
+
pattern.proxy_target,
|
|
337
|
+
pattern.parameters,
|
|
338
|
+
attr_name.to_s,
|
|
339
|
+
namespace: :active_model_proxy,
|
|
340
|
+
as: public_method_name
|
|
341
|
+
)
|
|
320
342
|
end
|
|
321
343
|
end
|
|
322
344
|
|
|
323
|
-
# Removes all the previously dynamically defined methods from the class.
|
|
345
|
+
# Removes all the previously dynamically defined methods from the class, including alias attribute methods.
|
|
324
346
|
#
|
|
325
347
|
# class Person
|
|
326
348
|
# include ActiveModel::AttributeMethods
|
|
@@ -328,6 +350,7 @@ module ActiveModel
|
|
|
328
350
|
# attr_accessor :name
|
|
329
351
|
# attribute_method_suffix '_short?'
|
|
330
352
|
# define_attribute_method :name
|
|
353
|
+
# alias_attribute :first_name, :name
|
|
331
354
|
#
|
|
332
355
|
# private
|
|
333
356
|
# def attribute_short?(attr)
|
|
@@ -337,19 +360,38 @@ module ActiveModel
|
|
|
337
360
|
#
|
|
338
361
|
# person = Person.new
|
|
339
362
|
# person.name = 'Bob'
|
|
363
|
+
# person.first_name # => "Bob"
|
|
340
364
|
# person.name_short? # => true
|
|
341
365
|
#
|
|
342
366
|
# Person.undefine_attribute_methods
|
|
343
367
|
#
|
|
344
368
|
# person.name_short? # => NoMethodError
|
|
369
|
+
# person.first_name # => NoMethodError
|
|
345
370
|
def undefine_attribute_methods
|
|
346
371
|
generated_attribute_methods.module_eval do
|
|
347
372
|
undef_method(*instance_methods)
|
|
348
373
|
end
|
|
349
|
-
|
|
374
|
+
attribute_method_patterns_cache.clear
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def aliases_by_attribute_name # :nodoc:
|
|
378
|
+
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
|
|
350
379
|
end
|
|
351
380
|
|
|
352
381
|
private
|
|
382
|
+
def inherited(base) # :nodoc:
|
|
383
|
+
super
|
|
384
|
+
base.class_eval do
|
|
385
|
+
@attribute_method_patterns_cache = nil
|
|
386
|
+
@aliases_by_attribute_name = nil
|
|
387
|
+
@generated_attribute_methods = nil
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def resolve_attribute_name(name)
|
|
392
|
+
attribute_aliases.fetch(super, &:itself)
|
|
393
|
+
end
|
|
394
|
+
|
|
353
395
|
def generated_attribute_methods
|
|
354
396
|
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
|
355
397
|
end
|
|
@@ -367,62 +409,77 @@ module ActiveModel
|
|
|
367
409
|
# used to alleviate the GC, which ultimately also speeds up the app
|
|
368
410
|
# significantly (in our case our test suite finishes 10% faster with
|
|
369
411
|
# this cache).
|
|
370
|
-
def
|
|
371
|
-
@
|
|
412
|
+
def attribute_method_patterns_cache
|
|
413
|
+
@attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
|
372
414
|
end
|
|
373
415
|
|
|
374
|
-
def
|
|
375
|
-
|
|
376
|
-
|
|
416
|
+
def attribute_method_patterns_matching(method_name)
|
|
417
|
+
attribute_method_patterns_cache.compute_if_absent(method_name) do
|
|
418
|
+
attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
|
|
377
419
|
end
|
|
378
420
|
end
|
|
379
421
|
|
|
380
422
|
# Define a method `name` in `mod` that dispatches to `send`
|
|
381
423
|
# using the given `extra` args. This falls back on `send`
|
|
382
424
|
# if the called name cannot be compiled.
|
|
383
|
-
def define_proxy_call(code_generator, name,
|
|
425
|
+
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
|
|
426
|
+
mangled_name = build_mangled_name(name)
|
|
427
|
+
|
|
428
|
+
call_args.map!(&:inspect)
|
|
429
|
+
call_args << parameters if parameters
|
|
430
|
+
|
|
431
|
+
# We have to use a different namespace for every target method, because
|
|
432
|
+
# if someone defines an attribute that look like an attribute method we could clash, e.g.
|
|
433
|
+
# attribute :title_was
|
|
434
|
+
# attribute :title
|
|
435
|
+
namespace = :"#{namespace}_#{proxy_target}"
|
|
436
|
+
|
|
437
|
+
define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def build_mangled_name(name)
|
|
384
441
|
mangled_name = name
|
|
442
|
+
|
|
385
443
|
unless NAME_COMPILABLE_REGEXP.match?(name)
|
|
386
444
|
mangled_name = "__temp__#{name.unpack1("h*")}"
|
|
387
445
|
end
|
|
388
446
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
call_args << parameters if parameters
|
|
447
|
+
mangled_name
|
|
448
|
+
end
|
|
392
449
|
|
|
393
|
-
|
|
394
|
-
|
|
450
|
+
def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
|
|
451
|
+
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
|
|
452
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
|
|
453
|
+
"self.#{target_name}(#{call_args.join(", ")})"
|
|
395
454
|
else
|
|
396
|
-
call_args.unshift(":'#{
|
|
455
|
+
call_args.unshift(":'#{target_name}'")
|
|
397
456
|
"send(#{call_args.join(", ")})"
|
|
398
457
|
end
|
|
399
458
|
|
|
400
|
-
modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
|
|
401
|
-
|
|
402
459
|
batch <<
|
|
403
|
-
"
|
|
460
|
+
"def #{mangled_name}(#{parameters || ''})" <<
|
|
404
461
|
body <<
|
|
405
462
|
"end"
|
|
406
463
|
end
|
|
407
464
|
end
|
|
408
465
|
|
|
409
|
-
class
|
|
410
|
-
attr_reader :prefix, :suffix, :
|
|
466
|
+
class AttributeMethodPattern # :nodoc:
|
|
467
|
+
attr_reader :prefix, :suffix, :proxy_target, :parameters
|
|
411
468
|
|
|
412
|
-
|
|
469
|
+
AttributeMethod = Struct.new(:proxy_target, :attr_name)
|
|
413
470
|
|
|
414
471
|
def initialize(prefix: "", suffix: "", parameters: nil)
|
|
415
472
|
@prefix = prefix
|
|
416
473
|
@suffix = suffix
|
|
417
|
-
@parameters = parameters.nil? ?
|
|
474
|
+
@parameters = parameters.nil? ? "..." : parameters
|
|
418
475
|
@regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
|
|
419
|
-
@
|
|
476
|
+
@proxy_target = "#{@prefix}attribute#{@suffix}"
|
|
420
477
|
@method_name = "#{prefix}%s#{suffix}"
|
|
421
478
|
end
|
|
422
479
|
|
|
423
480
|
def match(method_name)
|
|
424
481
|
if @regex =~ method_name
|
|
425
|
-
|
|
482
|
+
AttributeMethod.new(proxy_target, $1)
|
|
426
483
|
end
|
|
427
484
|
end
|
|
428
485
|
|
|
@@ -442,24 +499,22 @@ module ActiveModel
|
|
|
442
499
|
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
|
443
500
|
# class belonging to the +clients+ table with a +master_id+ foreign key
|
|
444
501
|
# can instantiate master through <tt>Client#master</tt>.
|
|
445
|
-
def method_missing(method,
|
|
502
|
+
def method_missing(method, ...)
|
|
446
503
|
if respond_to_without_attributes?(method, true)
|
|
447
504
|
super
|
|
448
505
|
else
|
|
449
|
-
match = matched_attribute_method(method.
|
|
450
|
-
match ? attribute_missing(match,
|
|
506
|
+
match = matched_attribute_method(method.name)
|
|
507
|
+
match ? attribute_missing(match, ...) : super
|
|
451
508
|
end
|
|
452
509
|
end
|
|
453
|
-
ruby2_keywords(:method_missing)
|
|
454
510
|
|
|
455
511
|
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
|
456
512
|
# +method_missing+ is called we check to see if there is a matching
|
|
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
|
-
def attribute_missing(match,
|
|
460
|
-
__send__(match.
|
|
515
|
+
def attribute_missing(match, ...)
|
|
516
|
+
__send__(match.proxy_target, match.attr_name, ...)
|
|
461
517
|
end
|
|
462
|
-
ruby2_keywords(:attribute_missing)
|
|
463
518
|
|
|
464
519
|
# A +Person+ instance with a +name+ attribute can ask
|
|
465
520
|
# <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
|
|
@@ -485,12 +540,12 @@ module ActiveModel
|
|
|
485
540
|
# Returns a struct representing the matching attribute method.
|
|
486
541
|
# The struct's attributes are prefix, base and suffix.
|
|
487
542
|
def matched_attribute_method(method_name)
|
|
488
|
-
matches = self.class.send(:
|
|
543
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
|
489
544
|
matches.detect { |match| attribute_method?(match.attr_name) }
|
|
490
545
|
end
|
|
491
546
|
|
|
492
547
|
def missing_attribute(attr_name, stack)
|
|
493
|
-
raise ActiveModel::MissingAttributeError, "missing attribute
|
|
548
|
+
raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
|
|
494
549
|
end
|
|
495
550
|
|
|
496
551
|
def _read_attribute(attr)
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
name = resolve_attribute_name(name)
|
|
14
|
+
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
|
|
15
|
+
type = hook_attribute_type(name, type) if type
|
|
16
|
+
|
|
17
|
+
pending_attribute_modifications << PendingType.new(name, type) if type || no_default
|
|
18
|
+
pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
|
|
19
|
+
|
|
20
|
+
reset_default_attributes
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decorate_attributes(names = nil, &decorator) # :nodoc:
|
|
24
|
+
names = names&.map { |name| resolve_attribute_name(name) }
|
|
25
|
+
|
|
26
|
+
pending_attribute_modifications << PendingDecorator.new(names, decorator)
|
|
27
|
+
|
|
28
|
+
reset_default_attributes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def _default_attributes # :nodoc:
|
|
32
|
+
@default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
|
|
33
|
+
apply_pending_attribute_modifications(attribute_set)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def attribute_types # :nodoc:
|
|
38
|
+
@attribute_types ||= _default_attributes.cast_types.tap do |hash|
|
|
39
|
+
hash.default = Type.default_value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def type_for_attribute(attribute_name, &block)
|
|
44
|
+
attribute_name = resolve_attribute_name(attribute_name)
|
|
45
|
+
|
|
46
|
+
if block
|
|
47
|
+
attribute_types.fetch(attribute_name, &block)
|
|
48
|
+
else
|
|
49
|
+
attribute_types[attribute_name]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
PendingType = Struct.new(:name, :type) do # :nodoc:
|
|
55
|
+
def apply_to(attribute_set)
|
|
56
|
+
attribute = attribute_set[name]
|
|
57
|
+
attribute_set[name] = attribute.with_type(type || attribute.type)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
PendingDefault = Struct.new(:name, :default) do # :nodoc:
|
|
62
|
+
def apply_to(attribute_set)
|
|
63
|
+
attribute_set[name] = attribute_set[name].with_user_default(default)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
|
|
68
|
+
def apply_to(attribute_set)
|
|
69
|
+
(names || attribute_set.keys).each do |name|
|
|
70
|
+
attribute = attribute_set[name]
|
|
71
|
+
type = decorator.call(name, attribute.type)
|
|
72
|
+
attribute_set[name] = attribute.with_type(type) if type
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pending_attribute_modifications
|
|
78
|
+
@pending_attribute_modifications ||= []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def apply_pending_attribute_modifications(attribute_set)
|
|
82
|
+
if superclass.respond_to?(:apply_pending_attribute_modifications, true)
|
|
83
|
+
superclass.send(:apply_pending_attribute_modifications, attribute_set)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
pending_attribute_modifications.each do |modification|
|
|
87
|
+
modification.apply_to(attribute_set)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def reset_default_attributes
|
|
92
|
+
reset_default_attributes!
|
|
93
|
+
subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def reset_default_attributes!
|
|
97
|
+
@default_attributes = nil
|
|
98
|
+
@attribute_types = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_attribute_name(name)
|
|
102
|
+
name.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def resolve_type_name(name, **options)
|
|
106
|
+
Type.lookup(name, **options)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Hook for other modules to override. The attribute type is passed
|
|
110
|
+
# through this method immediately after it is resolved, before any type
|
|
111
|
+
# decorations are applied.
|
|
112
|
+
def hook_attribute_type(attribute, type)
|
|
113
|
+
type
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
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
|