activemodel 7.0.8.7 → 7.2.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -263
- 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 +145 -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 +4 -4
- 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 +12 -7
@@ -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,41 @@ 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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
batch <<
|
229
|
-
"#{modifier}def #{mangled_name}(#{parameters || ''})" <<
|
230
|
-
body <<
|
231
|
-
"end"
|
232
|
-
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)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def generate_alias_attribute_methods(code_generator, new_name, old_name)
|
218
|
+
ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
|
219
|
+
attribute_method_patterns.each do |pattern|
|
220
|
+
alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
|
233
221
|
end
|
222
|
+
attribute_method_patterns_cache.clear
|
234
223
|
end
|
235
224
|
end
|
236
225
|
|
226
|
+
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
|
227
|
+
method_name = pattern.method_name(new_name).to_s
|
228
|
+
target_name = pattern.method_name(old_name).to_s
|
229
|
+
parameters = pattern.parameters
|
230
|
+
|
231
|
+
mangled_name = build_mangled_name(target_name)
|
232
|
+
|
233
|
+
call_args = []
|
234
|
+
call_args << parameters if parameters
|
235
|
+
|
236
|
+
define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
|
237
|
+
end
|
238
|
+
|
237
239
|
# Is +new_name+ an alias?
|
238
240
|
def attribute_alias?(new_name)
|
239
241
|
attribute_aliases.key? new_name.to_s
|
@@ -245,7 +247,7 @@ module ActiveModel
|
|
245
247
|
end
|
246
248
|
|
247
249
|
# Declares the attributes that should be prefixed and suffixed by
|
248
|
-
#
|
250
|
+
# +ActiveModel::AttributeMethods+.
|
249
251
|
#
|
250
252
|
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
251
253
|
# +define_attribute_methods+ after you define any prefix, suffix, or affix
|
@@ -269,12 +271,17 @@ module ActiveModel
|
|
269
271
|
# end
|
270
272
|
def define_attribute_methods(*attr_names)
|
271
273
|
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
272
|
-
attr_names.flatten.each
|
274
|
+
attr_names.flatten.each do |attr_name|
|
275
|
+
define_attribute_method(attr_name, _owner: owner)
|
276
|
+
aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
|
277
|
+
generate_alias_attribute_methods owner, aliased_name, attr_name
|
278
|
+
end
|
279
|
+
end
|
273
280
|
end
|
274
281
|
end
|
275
282
|
|
276
283
|
# Declares an attribute that should be prefixed and suffixed by
|
277
|
-
#
|
284
|
+
# +ActiveModel::AttributeMethods+.
|
278
285
|
#
|
279
286
|
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
280
287
|
# +define_attribute_method+ after you define any prefix, suffix or affix
|
@@ -301,26 +308,46 @@ module ActiveModel
|
|
301
308
|
# person.name = 'Bob'
|
302
309
|
# person.name # => "Bob"
|
303
310
|
# person.name_short? # => true
|
304
|
-
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
311
|
+
def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
|
305
312
|
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
306
|
-
|
307
|
-
|
313
|
+
attribute_method_patterns.each do |pattern|
|
314
|
+
define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
|
315
|
+
end
|
316
|
+
attribute_method_patterns_cache.clear
|
317
|
+
end
|
318
|
+
end
|
308
319
|
|
309
|
-
|
310
|
-
|
320
|
+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
|
321
|
+
canonical_method_name = pattern.method_name(attr_name)
|
322
|
+
public_method_name = pattern.method_name(as)
|
323
|
+
|
324
|
+
# If defining a regular attribute method, we don't override methods that are explictly
|
325
|
+
# defined in parrent classes.
|
326
|
+
if instance_method_already_implemented?(public_method_name)
|
327
|
+
# However, for `alias_attribute`, we always define the method.
|
328
|
+
# We check for override second because `instance_method_already_implemented?`
|
329
|
+
# also check for dangerous methods.
|
330
|
+
return unless override
|
331
|
+
end
|
311
332
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
333
|
+
generate_method = "define_method_#{pattern.proxy_target}"
|
334
|
+
|
335
|
+
if respond_to?(generate_method, true)
|
336
|
+
send(generate_method, attr_name.to_s, owner: owner, as: as)
|
337
|
+
else
|
338
|
+
define_proxy_call(
|
339
|
+
owner,
|
340
|
+
canonical_method_name,
|
341
|
+
pattern.proxy_target,
|
342
|
+
pattern.parameters,
|
343
|
+
attr_name.to_s,
|
344
|
+
namespace: :active_model_proxy,
|
345
|
+
as: public_method_name
|
346
|
+
)
|
320
347
|
end
|
321
348
|
end
|
322
349
|
|
323
|
-
# Removes all the previously dynamically defined methods from the class.
|
350
|
+
# Removes all the previously dynamically defined methods from the class, including alias attribute methods.
|
324
351
|
#
|
325
352
|
# class Person
|
326
353
|
# include ActiveModel::AttributeMethods
|
@@ -328,6 +355,7 @@ module ActiveModel
|
|
328
355
|
# attr_accessor :name
|
329
356
|
# attribute_method_suffix '_short?'
|
330
357
|
# define_attribute_method :name
|
358
|
+
# alias_attribute :first_name, :name
|
331
359
|
#
|
332
360
|
# private
|
333
361
|
# def attribute_short?(attr)
|
@@ -337,19 +365,38 @@ module ActiveModel
|
|
337
365
|
#
|
338
366
|
# person = Person.new
|
339
367
|
# person.name = 'Bob'
|
368
|
+
# person.first_name # => "Bob"
|
340
369
|
# person.name_short? # => true
|
341
370
|
#
|
342
371
|
# Person.undefine_attribute_methods
|
343
372
|
#
|
344
373
|
# person.name_short? # => NoMethodError
|
374
|
+
# person.first_name # => NoMethodError
|
345
375
|
def undefine_attribute_methods
|
346
376
|
generated_attribute_methods.module_eval do
|
347
377
|
undef_method(*instance_methods)
|
348
378
|
end
|
349
|
-
|
379
|
+
attribute_method_patterns_cache.clear
|
380
|
+
end
|
381
|
+
|
382
|
+
def aliases_by_attribute_name # :nodoc:
|
383
|
+
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
|
350
384
|
end
|
351
385
|
|
352
386
|
private
|
387
|
+
def inherited(base) # :nodoc:
|
388
|
+
super
|
389
|
+
base.class_eval do
|
390
|
+
@attribute_method_patterns_cache = nil
|
391
|
+
@aliases_by_attribute_name = nil
|
392
|
+
@generated_attribute_methods = nil
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def resolve_attribute_name(name)
|
397
|
+
attribute_aliases.fetch(super, &:itself)
|
398
|
+
end
|
399
|
+
|
353
400
|
def generated_attribute_methods
|
354
401
|
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
355
402
|
end
|
@@ -367,62 +414,77 @@ module ActiveModel
|
|
367
414
|
# used to alleviate the GC, which ultimately also speeds up the app
|
368
415
|
# significantly (in our case our test suite finishes 10% faster with
|
369
416
|
# this cache).
|
370
|
-
def
|
371
|
-
@
|
417
|
+
def attribute_method_patterns_cache
|
418
|
+
@attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
372
419
|
end
|
373
420
|
|
374
|
-
def
|
375
|
-
|
376
|
-
|
421
|
+
def attribute_method_patterns_matching(method_name)
|
422
|
+
attribute_method_patterns_cache.compute_if_absent(method_name) do
|
423
|
+
attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
|
377
424
|
end
|
378
425
|
end
|
379
426
|
|
380
427
|
# Define a method `name` in `mod` that dispatches to `send`
|
381
428
|
# using the given `extra` args. This falls back on `send`
|
382
429
|
# if the called name cannot be compiled.
|
383
|
-
def define_proxy_call(code_generator, name,
|
430
|
+
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
|
431
|
+
mangled_name = build_mangled_name(name)
|
432
|
+
|
433
|
+
call_args.map!(&:inspect)
|
434
|
+
call_args << parameters if parameters
|
435
|
+
|
436
|
+
# We have to use a different namespace for every target method, because
|
437
|
+
# if someone defines an attribute that look like an attribute method we could clash, e.g.
|
438
|
+
# attribute :title_was
|
439
|
+
# attribute :title
|
440
|
+
namespace = :"#{namespace}_#{proxy_target}"
|
441
|
+
|
442
|
+
define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
|
443
|
+
end
|
444
|
+
|
445
|
+
def build_mangled_name(name)
|
384
446
|
mangled_name = name
|
447
|
+
|
385
448
|
unless NAME_COMPILABLE_REGEXP.match?(name)
|
386
|
-
mangled_name = "__temp__#{name.unpack1("h*")}"
|
449
|
+
mangled_name = :"__temp__#{name.unpack1("h*")}"
|
387
450
|
end
|
388
451
|
|
389
|
-
|
390
|
-
|
391
|
-
call_args << parameters if parameters
|
452
|
+
mangled_name
|
453
|
+
end
|
392
454
|
|
393
|
-
|
394
|
-
|
455
|
+
def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
|
456
|
+
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
|
457
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
|
458
|
+
"self.#{target_name}(#{call_args.join(", ")})"
|
395
459
|
else
|
396
|
-
call_args.unshift(":'#{
|
460
|
+
call_args.unshift(":'#{target_name}'")
|
397
461
|
"send(#{call_args.join(", ")})"
|
398
462
|
end
|
399
463
|
|
400
|
-
modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
|
401
|
-
|
402
464
|
batch <<
|
403
|
-
"
|
465
|
+
"def #{mangled_name}(#{parameters || ''})" <<
|
404
466
|
body <<
|
405
467
|
"end"
|
406
468
|
end
|
407
469
|
end
|
408
470
|
|
409
|
-
class
|
410
|
-
attr_reader :prefix, :suffix, :
|
471
|
+
class AttributeMethodPattern # :nodoc:
|
472
|
+
attr_reader :prefix, :suffix, :proxy_target, :parameters
|
411
473
|
|
412
|
-
|
474
|
+
AttributeMethod = Struct.new(:proxy_target, :attr_name)
|
413
475
|
|
414
476
|
def initialize(prefix: "", suffix: "", parameters: nil)
|
415
477
|
@prefix = prefix
|
416
478
|
@suffix = suffix
|
417
|
-
@parameters = parameters.nil? ?
|
479
|
+
@parameters = parameters.nil? ? "..." : parameters
|
418
480
|
@regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
|
419
|
-
@
|
481
|
+
@proxy_target = "#{@prefix}attribute#{@suffix}"
|
420
482
|
@method_name = "#{prefix}%s#{suffix}"
|
421
483
|
end
|
422
484
|
|
423
485
|
def match(method_name)
|
424
486
|
if @regex =~ method_name
|
425
|
-
|
487
|
+
AttributeMethod.new(proxy_target, $1)
|
426
488
|
end
|
427
489
|
end
|
428
490
|
|
@@ -442,24 +504,22 @@ module ActiveModel
|
|
442
504
|
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
443
505
|
# class belonging to the +clients+ table with a +master_id+ foreign key
|
444
506
|
# can instantiate master through <tt>Client#master</tt>.
|
445
|
-
def method_missing(method,
|
507
|
+
def method_missing(method, ...)
|
446
508
|
if respond_to_without_attributes?(method, true)
|
447
509
|
super
|
448
510
|
else
|
449
|
-
match = matched_attribute_method(method.
|
450
|
-
match ? attribute_missing(match,
|
511
|
+
match = matched_attribute_method(method.name)
|
512
|
+
match ? attribute_missing(match, ...) : super
|
451
513
|
end
|
452
514
|
end
|
453
|
-
ruby2_keywords(:method_missing)
|
454
515
|
|
455
516
|
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
456
517
|
# +method_missing+ is called we check to see if there is a matching
|
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
|
-
def attribute_missing(match,
|
460
|
-
__send__(match.
|
520
|
+
def attribute_missing(match, ...)
|
521
|
+
__send__(match.proxy_target, match.attr_name, ...)
|
461
522
|
end
|
462
|
-
ruby2_keywords(:attribute_missing)
|
463
523
|
|
464
524
|
# A +Person+ instance with a +name+ attribute can ask
|
465
525
|
# <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
|
@@ -485,12 +545,12 @@ module ActiveModel
|
|
485
545
|
# Returns a struct representing the matching attribute method.
|
486
546
|
# The struct's attributes are prefix, base and suffix.
|
487
547
|
def matched_attribute_method(method_name)
|
488
|
-
matches = self.class.send(:
|
548
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
489
549
|
matches.detect { |match| attribute_method?(match.attr_name) }
|
490
550
|
end
|
491
551
|
|
492
552
|
def missing_attribute(attr_name, stack)
|
493
|
-
raise ActiveModel::MissingAttributeError, "missing attribute
|
553
|
+
raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
|
494
554
|
end
|
495
555
|
|
496
556
|
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
|