activemodel 7.0.8.1 → 7.1.4
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 +158 -158
- data/MIT-LICENSE +1 -1
- data/README.rdoc +9 -9
- 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 +26 -1
- data/lib/active_model/attribute_assignment.rb +1 -1
- data/lib/active_model/attribute_methods.rb +129 -73
- data/lib/active_model/attribute_registration.rb +77 -0
- data/lib/active_model/attribute_set.rb +10 -1
- data/lib/active_model/attributes.rb +65 -48
- data/lib/active_model/callbacks.rb +5 -5
- 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 +61 -23
- 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 +28 -12
- data/lib/active_model/type/immutable_string.rb +37 -1
- data/lib/active_model/type/integer.rb +44 -1
- 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 +4 -4
- 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 +15 -7
- 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 +13 -8
|
@@ -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+
|
|
@@ -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 :
|
|
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.
|
|
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.
|
|
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.
|
|
177
|
+
self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
|
|
178
178
|
undefine_attribute_methods
|
|
179
179
|
end
|
|
180
180
|
|
|
@@ -202,35 +202,48 @@ module ActiveModel
|
|
|
202
202
|
# person.name_short? # => true
|
|
203
203
|
# person.nickname_short? # => true
|
|
204
204
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
212
|
+
def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
|
|
213
|
+
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
|
|
214
|
+
generate_alias_attribute_methods(code_generator, new_name, old_name)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
|
|
219
|
+
define_attribute_method(old_name, _owner: code_generator, as: new_name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
|
|
223
|
+
method_name = pattern.method_name(new_name).to_s
|
|
224
|
+
target_name = pattern.method_name(old_name).to_s
|
|
225
|
+
parameters = pattern.parameters
|
|
226
|
+
mangled_name = target_name
|
|
227
|
+
|
|
228
|
+
unless NAME_COMPILABLE_REGEXP.match?(target_name)
|
|
229
|
+
mangled_name = "__temp__#{target_name.unpack1("h*")}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
|
|
233
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
|
|
234
|
+
"self.#{target_name}(#{parameters || ''})"
|
|
235
|
+
else
|
|
236
|
+
call_args = [":'#{target_name}'"]
|
|
237
|
+
call_args << parameters if parameters
|
|
238
|
+
"send(#{call_args.join(", ")})"
|
|
233
239
|
end
|
|
240
|
+
|
|
241
|
+
modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
|
|
242
|
+
|
|
243
|
+
batch <<
|
|
244
|
+
"#{modifier}def #{mangled_name}(#{parameters || ''})" <<
|
|
245
|
+
body <<
|
|
246
|
+
"end"
|
|
234
247
|
end
|
|
235
248
|
end
|
|
236
249
|
|
|
@@ -245,7 +258,7 @@ module ActiveModel
|
|
|
245
258
|
end
|
|
246
259
|
|
|
247
260
|
# Declares the attributes that should be prefixed and suffixed by
|
|
248
|
-
#
|
|
261
|
+
# +ActiveModel::AttributeMethods+.
|
|
249
262
|
#
|
|
250
263
|
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
|
251
264
|
# +define_attribute_methods+ after you define any prefix, suffix, or affix
|
|
@@ -269,12 +282,17 @@ module ActiveModel
|
|
|
269
282
|
# end
|
|
270
283
|
def define_attribute_methods(*attr_names)
|
|
271
284
|
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
|
272
|
-
attr_names.flatten.each
|
|
285
|
+
attr_names.flatten.each do |attr_name|
|
|
286
|
+
define_attribute_method(attr_name, _owner: owner)
|
|
287
|
+
aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
|
|
288
|
+
generate_alias_attribute_methods owner, aliased_name, attr_name
|
|
289
|
+
end
|
|
290
|
+
end
|
|
273
291
|
end
|
|
274
292
|
end
|
|
275
293
|
|
|
276
294
|
# Declares an attribute that should be prefixed and suffixed by
|
|
277
|
-
#
|
|
295
|
+
# +ActiveModel::AttributeMethods+.
|
|
278
296
|
#
|
|
279
297
|
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
|
280
298
|
# +define_attribute_method+ after you define any prefix, suffix or affix
|
|
@@ -301,26 +319,45 @@ module ActiveModel
|
|
|
301
319
|
# person.name = 'Bob'
|
|
302
320
|
# person.name # => "Bob"
|
|
303
321
|
# person.name_short? # => true
|
|
304
|
-
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
|
322
|
+
def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
|
|
305
323
|
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
|
306
|
-
|
|
307
|
-
|
|
324
|
+
attribute_method_patterns.each do |pattern|
|
|
325
|
+
define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
|
|
326
|
+
end
|
|
327
|
+
attribute_method_patterns_cache.clear
|
|
328
|
+
end
|
|
329
|
+
end
|
|
308
330
|
|
|
309
|
-
|
|
310
|
-
|
|
331
|
+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
|
|
332
|
+
canonical_method_name = pattern.method_name(attr_name)
|
|
333
|
+
public_method_name = pattern.method_name(as)
|
|
334
|
+
|
|
335
|
+
# If defining a regular attribute method, we don't override methods that are explictly
|
|
336
|
+
# defined in parrent classes.
|
|
337
|
+
if instance_method_already_implemented?(public_method_name)
|
|
338
|
+
# However, for `alias_attribute`, we always define the method.
|
|
339
|
+
# We check for override second because `instance_method_already_implemented?`
|
|
340
|
+
# also check for dangerous methods.
|
|
341
|
+
return unless override
|
|
342
|
+
end
|
|
311
343
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
344
|
+
generate_method = "define_method_#{pattern.proxy_target}"
|
|
345
|
+
if respond_to?(generate_method, true)
|
|
346
|
+
send(generate_method, attr_name.to_s, owner: owner, as: as)
|
|
347
|
+
else
|
|
348
|
+
define_proxy_call(
|
|
349
|
+
owner,
|
|
350
|
+
canonical_method_name,
|
|
351
|
+
pattern.proxy_target,
|
|
352
|
+
pattern.parameters,
|
|
353
|
+
attr_name.to_s,
|
|
354
|
+
namespace: :active_model_proxy,
|
|
355
|
+
as: public_method_name,
|
|
356
|
+
)
|
|
320
357
|
end
|
|
321
358
|
end
|
|
322
359
|
|
|
323
|
-
# Removes all the previously dynamically defined methods from the class.
|
|
360
|
+
# Removes all the previously dynamically defined methods from the class, including alias attribute methods.
|
|
324
361
|
#
|
|
325
362
|
# class Person
|
|
326
363
|
# include ActiveModel::AttributeMethods
|
|
@@ -328,6 +365,7 @@ module ActiveModel
|
|
|
328
365
|
# attr_accessor :name
|
|
329
366
|
# attribute_method_suffix '_short?'
|
|
330
367
|
# define_attribute_method :name
|
|
368
|
+
# alias_attribute :first_name, :name
|
|
331
369
|
#
|
|
332
370
|
# private
|
|
333
371
|
# def attribute_short?(attr)
|
|
@@ -337,19 +375,36 @@ module ActiveModel
|
|
|
337
375
|
#
|
|
338
376
|
# person = Person.new
|
|
339
377
|
# person.name = 'Bob'
|
|
378
|
+
# person.first_name # => "Bob"
|
|
340
379
|
# person.name_short? # => true
|
|
341
380
|
#
|
|
342
381
|
# Person.undefine_attribute_methods
|
|
343
382
|
#
|
|
344
383
|
# person.name_short? # => NoMethodError
|
|
384
|
+
# person.first_name # => NoMethodError
|
|
345
385
|
def undefine_attribute_methods
|
|
346
386
|
generated_attribute_methods.module_eval do
|
|
347
387
|
undef_method(*instance_methods)
|
|
348
388
|
end
|
|
349
|
-
|
|
389
|
+
attribute_method_patterns_cache.clear
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def aliases_by_attribute_name # :nodoc:
|
|
393
|
+
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
|
|
350
394
|
end
|
|
351
395
|
|
|
352
396
|
private
|
|
397
|
+
def inherited(base) # :nodoc:
|
|
398
|
+
super
|
|
399
|
+
base.class_eval do
|
|
400
|
+
@attribute_method_patterns_cache = nil
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def resolve_attribute_name(name)
|
|
405
|
+
attribute_aliases.fetch(super, &:itself)
|
|
406
|
+
end
|
|
407
|
+
|
|
353
408
|
def generated_attribute_methods
|
|
354
409
|
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
|
355
410
|
end
|
|
@@ -367,33 +422,34 @@ module ActiveModel
|
|
|
367
422
|
# used to alleviate the GC, which ultimately also speeds up the app
|
|
368
423
|
# significantly (in our case our test suite finishes 10% faster with
|
|
369
424
|
# this cache).
|
|
370
|
-
def
|
|
371
|
-
@
|
|
425
|
+
def attribute_method_patterns_cache
|
|
426
|
+
@attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
|
372
427
|
end
|
|
373
428
|
|
|
374
|
-
def
|
|
375
|
-
|
|
376
|
-
|
|
429
|
+
def attribute_method_patterns_matching(method_name)
|
|
430
|
+
attribute_method_patterns_cache.compute_if_absent(method_name) do
|
|
431
|
+
attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
|
|
377
432
|
end
|
|
378
433
|
end
|
|
379
434
|
|
|
380
435
|
# Define a method `name` in `mod` that dispatches to `send`
|
|
381
436
|
# using the given `extra` args. This falls back on `send`
|
|
382
437
|
# if the called name cannot be compiled.
|
|
383
|
-
def define_proxy_call(code_generator, name,
|
|
438
|
+
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
|
|
384
439
|
mangled_name = name
|
|
385
440
|
unless NAME_COMPILABLE_REGEXP.match?(name)
|
|
386
441
|
mangled_name = "__temp__#{name.unpack1("h*")}"
|
|
387
442
|
end
|
|
388
443
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
444
|
+
call_args.map!(&:inspect)
|
|
445
|
+
call_args << parameters if parameters
|
|
446
|
+
namespace = :"#{namespace}_#{proxy_target}"
|
|
392
447
|
|
|
393
|
-
|
|
394
|
-
|
|
448
|
+
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
|
|
449
|
+
body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
|
|
450
|
+
"self.#{proxy_target}(#{call_args.join(", ")})"
|
|
395
451
|
else
|
|
396
|
-
call_args.unshift(":'#{
|
|
452
|
+
call_args.unshift(":'#{proxy_target}'")
|
|
397
453
|
"send(#{call_args.join(", ")})"
|
|
398
454
|
end
|
|
399
455
|
|
|
@@ -406,23 +462,23 @@ module ActiveModel
|
|
|
406
462
|
end
|
|
407
463
|
end
|
|
408
464
|
|
|
409
|
-
class
|
|
410
|
-
attr_reader :prefix, :suffix, :
|
|
465
|
+
class AttributeMethodPattern # :nodoc:
|
|
466
|
+
attr_reader :prefix, :suffix, :proxy_target, :parameters
|
|
411
467
|
|
|
412
|
-
|
|
468
|
+
AttributeMethod = Struct.new(:proxy_target, :attr_name)
|
|
413
469
|
|
|
414
470
|
def initialize(prefix: "", suffix: "", parameters: nil)
|
|
415
471
|
@prefix = prefix
|
|
416
472
|
@suffix = suffix
|
|
417
473
|
@parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
|
|
418
474
|
@regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
|
|
419
|
-
@
|
|
475
|
+
@proxy_target = "#{@prefix}attribute#{@suffix}"
|
|
420
476
|
@method_name = "#{prefix}%s#{suffix}"
|
|
421
477
|
end
|
|
422
478
|
|
|
423
479
|
def match(method_name)
|
|
424
480
|
if @regex =~ method_name
|
|
425
|
-
|
|
481
|
+
AttributeMethod.new(proxy_target, $1)
|
|
426
482
|
end
|
|
427
483
|
end
|
|
428
484
|
|
|
@@ -457,7 +513,7 @@ module ActiveModel
|
|
|
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
515
|
def attribute_missing(match, *args, &block)
|
|
460
|
-
__send__(match.
|
|
516
|
+
__send__(match.proxy_target, match.attr_name, *args, &block)
|
|
461
517
|
end
|
|
462
518
|
ruby2_keywords(:attribute_missing)
|
|
463
519
|
|
|
@@ -485,12 +541,12 @@ module ActiveModel
|
|
|
485
541
|
# Returns a struct representing the matching attribute method.
|
|
486
542
|
# The struct's attributes are prefix, base and suffix.
|
|
487
543
|
def matched_attribute_method(method_name)
|
|
488
|
-
matches = self.class.send(:
|
|
544
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
|
489
545
|
matches.detect { |match| attribute_method?(match.attr_name) }
|
|
490
546
|
end
|
|
491
547
|
|
|
492
548
|
def missing_attribute(attr_name, stack)
|
|
493
|
-
raise ActiveModel::MissingAttributeError, "missing attribute
|
|
549
|
+
raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
|
|
494
550
|
end
|
|
495
551
|
|
|
496
552
|
def _read_attribute(attr)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/subclasses"
|
|
4
|
+
require "active_model/attribute_set"
|
|
5
|
+
require "active_model/attribute/user_provided_default"
|
|
6
|
+
|
|
7
|
+
module ActiveModel
|
|
8
|
+
module AttributeRegistration # :nodoc:
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
module ClassMethods # :nodoc:
|
|
12
|
+
def attribute(name, type = nil, default: (no_default = true), **options)
|
|
13
|
+
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
|
|
14
|
+
|
|
15
|
+
pending = pending_attribute(name)
|
|
16
|
+
pending.type = type if type
|
|
17
|
+
pending.default = default unless no_default
|
|
18
|
+
|
|
19
|
+
reset_default_attributes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def _default_attributes # :nodoc:
|
|
23
|
+
@default_attributes ||= build_default_attributes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def attribute_types # :nodoc:
|
|
27
|
+
@attribute_types ||= _default_attributes.cast_types.tap do |hash|
|
|
28
|
+
hash.default = Type.default_value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
class PendingAttribute # :nodoc:
|
|
34
|
+
attr_accessor :type, :default
|
|
35
|
+
|
|
36
|
+
def apply_to(attribute)
|
|
37
|
+
attribute = attribute.with_type(type || attribute.type)
|
|
38
|
+
attribute = attribute.with_user_default(default) if defined?(@default)
|
|
39
|
+
attribute
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pending_attribute(name)
|
|
44
|
+
@pending_attributes ||= {}
|
|
45
|
+
@pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_pending_attributes(attribute_set)
|
|
49
|
+
superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
|
|
50
|
+
|
|
51
|
+
defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
|
|
52
|
+
attribute_set[name] = pending.apply_to(attribute_set[name])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attribute_set
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_default_attributes
|
|
59
|
+
apply_pending_attributes(AttributeSet.new({}))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset_default_attributes
|
|
63
|
+
@default_attributes = nil
|
|
64
|
+
@attribute_types = nil
|
|
65
|
+
subclasses.each { |subclass| subclass.send(__method__) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolve_attribute_name(name)
|
|
69
|
+
name.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def resolve_type_name(name, **options)
|
|
73
|
+
Type.lookup(name, **options)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -21,6 +21,10 @@ module ActiveModel
|
|
|
21
21
|
@attributes[name] = value
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def cast_types
|
|
25
|
+
attributes.transform_values(&:type)
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
def values_before_type_cast
|
|
25
29
|
attributes.transform_values(&:value_before_type_cast)
|
|
26
30
|
end
|
|
@@ -37,6 +41,7 @@ module ActiveModel
|
|
|
37
41
|
def key?(name)
|
|
38
42
|
attributes.key?(name) && self[name].initialized?
|
|
39
43
|
end
|
|
44
|
+
alias :include? :key?
|
|
40
45
|
|
|
41
46
|
def keys
|
|
42
47
|
attributes.each_key.select { |name| self[name].initialized? }
|
|
@@ -94,8 +99,12 @@ module ActiveModel
|
|
|
94
99
|
AttributeSet.new(new_attributes)
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
def reverse_merge!(target_attributes)
|
|
103
|
+
attributes.reverse_merge!(target_attributes.attributes) && self
|
|
104
|
+
end
|
|
105
|
+
|
|
97
106
|
def ==(other)
|
|
98
|
-
attributes == other.attributes
|
|
107
|
+
other.is_a?(AttributeSet) && attributes == other.send(:attributes)
|
|
99
108
|
end
|
|
100
109
|
|
|
101
110
|
protected
|
|
@@ -1,33 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_model/attribute_set"
|
|
4
|
-
require "active_model/attribute/user_provided_default"
|
|
5
|
-
|
|
6
3
|
module ActiveModel
|
|
7
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
##
|
|
41
|
+
# :call-seq: attribute(name, cast_type = nil, default: nil, **options)
|
|
42
|
+
#
|
|
43
|
+
# Defines a model attribute. In addition to the attribute name, a cast
|
|
44
|
+
# type and default value may be specified, as well as any options
|
|
45
|
+
# supported by the given cast type.
|
|
46
|
+
#
|
|
47
|
+
# class Person
|
|
48
|
+
# include ActiveModel::Attributes
|
|
49
|
+
#
|
|
50
|
+
# attribute :name, :string
|
|
51
|
+
# attribute :active, :boolean, default: true
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# person = Person.new
|
|
55
|
+
# person.name = "Volmer"
|
|
56
|
+
#
|
|
57
|
+
# person.name # => "Volmer"
|
|
58
|
+
# person.active # => true
|
|
59
|
+
def attribute(name, ...)
|
|
60
|
+
super
|
|
27
61
|
define_attribute_method(name)
|
|
28
62
|
end
|
|
29
63
|
|
|
30
|
-
# Returns an array of attribute names as strings
|
|
64
|
+
# Returns an array of attribute names as strings.
|
|
31
65
|
#
|
|
32
66
|
# class Person
|
|
33
67
|
# include ActiveModel::Attributes
|
|
@@ -36,18 +70,17 @@ module ActiveModel
|
|
|
36
70
|
# attribute :age, :integer
|
|
37
71
|
# end
|
|
38
72
|
#
|
|
39
|
-
# Person.attribute_names
|
|
40
|
-
# # => ["name", "age"]
|
|
73
|
+
# Person.attribute_names # => ["name", "age"]
|
|
41
74
|
def attribute_names
|
|
42
75
|
attribute_types.keys
|
|
43
76
|
end
|
|
44
77
|
|
|
45
78
|
private
|
|
46
|
-
def define_method_attribute=(
|
|
79
|
+
def define_method_attribute=(canonical_name, owner:, as: canonical_name)
|
|
47
80
|
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
|
|
48
|
-
owner,
|
|
81
|
+
owner, canonical_name, writer: true,
|
|
49
82
|
) do |temp_method_name, attr_name_expr|
|
|
50
|
-
owner.define_cached_method("#{
|
|
83
|
+
owner.define_cached_method(temp_method_name, as: "#{as}=", namespace: :active_model) do |batch|
|
|
51
84
|
batch <<
|
|
52
85
|
"def #{temp_method_name}(value)" <<
|
|
53
86
|
" _write_attribute(#{attr_name_expr}, value)" <<
|
|
@@ -55,27 +88,9 @@ module ActiveModel
|
|
|
55
88
|
end
|
|
56
89
|
end
|
|
57
90
|
end
|
|
58
|
-
|
|
59
|
-
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
|
|
60
|
-
private_constant :NO_DEFAULT_PROVIDED
|
|
61
|
-
|
|
62
|
-
def define_default_attribute(name, value, type)
|
|
63
|
-
self._default_attributes = _default_attributes.deep_dup
|
|
64
|
-
if value == NO_DEFAULT_PROVIDED
|
|
65
|
-
default_attribute = _default_attributes[name].with_type(type)
|
|
66
|
-
else
|
|
67
|
-
default_attribute = Attribute::UserProvidedDefault.new(
|
|
68
|
-
name,
|
|
69
|
-
value,
|
|
70
|
-
type,
|
|
71
|
-
_default_attributes.fetch(name.to_s) { nil },
|
|
72
|
-
)
|
|
73
|
-
end
|
|
74
|
-
_default_attributes[name] = default_attribute
|
|
75
|
-
end
|
|
76
91
|
end
|
|
77
92
|
|
|
78
|
-
def initialize(*)
|
|
93
|
+
def initialize(*) # :nodoc:
|
|
79
94
|
@attributes = self.class._default_attributes.deep_dup
|
|
80
95
|
super
|
|
81
96
|
end
|
|
@@ -85,7 +100,8 @@ module ActiveModel
|
|
|
85
100
|
super
|
|
86
101
|
end
|
|
87
102
|
|
|
88
|
-
# Returns a hash of all the attributes with their names as keys and the
|
|
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
|
|
98
|
-
# person.
|
|
99
|
-
#
|
|
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
|