activemodel 7.0.4 → 7.1.5.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 +191 -94
- data/MIT-LICENSE +1 -1
- data/README.rdoc +11 -11
- 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 +165 -112
- data/lib/active_model/attribute_mutation_tracker.rb +10 -2
- 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 +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 +5 -4
- 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 +13 -3
- 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/mutable.rb +4 -0
- 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 +25 -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 +10 -12
- 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 +13 -12
- 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 +5 -5
- 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 +6 -1
- metadata +16 -11
@@ -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+
|
@@ -49,18 +49,17 @@ module ActiveModel
|
|
49
49
|
# end
|
50
50
|
#
|
51
51
|
# private
|
52
|
+
# def attribute_contrived?(attr)
|
53
|
+
# true
|
54
|
+
# end
|
52
55
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
# def clear_attribute(attr)
|
58
|
-
# send("#{attr}=", nil)
|
59
|
-
# end
|
56
|
+
# def clear_attribute(attr)
|
57
|
+
# send("#{attr}=", nil)
|
58
|
+
# end
|
60
59
|
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
60
|
+
# def reset_attribute_to_default!(attr)
|
61
|
+
# send("#{attr}=", 'Default Name')
|
62
|
+
# end
|
64
63
|
# end
|
65
64
|
module AttributeMethods
|
66
65
|
extend ActiveSupport::Concern
|
@@ -71,7 +70,7 @@ module ActiveModel
|
|
71
70
|
|
72
71
|
included do
|
73
72
|
class_attribute :attribute_aliases, instance_writer: false, default: {}
|
74
|
-
class_attribute :
|
73
|
+
class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
|
75
74
|
end
|
76
75
|
|
77
76
|
module ClassMethods
|
@@ -95,10 +94,9 @@ module ActiveModel
|
|
95
94
|
# define_attribute_methods :name
|
96
95
|
#
|
97
96
|
# private
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
# end
|
97
|
+
# def clear_attribute(attr)
|
98
|
+
# send("#{attr}=", nil)
|
99
|
+
# end
|
102
100
|
# end
|
103
101
|
#
|
104
102
|
# person = Person.new
|
@@ -107,7 +105,7 @@ module ActiveModel
|
|
107
105
|
# person.clear_name
|
108
106
|
# person.name # => nil
|
109
107
|
def attribute_method_prefix(*prefixes, parameters: nil)
|
110
|
-
self.
|
108
|
+
self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
|
111
109
|
undefine_attribute_methods
|
112
110
|
end
|
113
111
|
|
@@ -131,10 +129,9 @@ module ActiveModel
|
|
131
129
|
# define_attribute_methods :name
|
132
130
|
#
|
133
131
|
# private
|
134
|
-
#
|
135
|
-
#
|
136
|
-
#
|
137
|
-
# end
|
132
|
+
# def attribute_short?(attr)
|
133
|
+
# send(attr).length < 5
|
134
|
+
# end
|
138
135
|
# end
|
139
136
|
#
|
140
137
|
# person = Person.new
|
@@ -142,7 +139,7 @@ module ActiveModel
|
|
142
139
|
# person.name # => "Bob"
|
143
140
|
# person.name_short? # => true
|
144
141
|
def attribute_method_suffix(*suffixes, parameters: nil)
|
145
|
-
self.
|
142
|
+
self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
|
146
143
|
undefine_attribute_methods
|
147
144
|
end
|
148
145
|
|
@@ -167,10 +164,9 @@ module ActiveModel
|
|
167
164
|
# define_attribute_methods :name
|
168
165
|
#
|
169
166
|
# private
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
# end
|
167
|
+
# def reset_attribute_to_default!(attr)
|
168
|
+
# send("#{attr}=", 'Default Name')
|
169
|
+
# end
|
174
170
|
# end
|
175
171
|
#
|
176
172
|
# person = Person.new
|
@@ -178,7 +174,7 @@ module ActiveModel
|
|
178
174
|
# person.reset_name_to_default!
|
179
175
|
# person.name # => 'Default Name'
|
180
176
|
def attribute_method_affix(*affixes)
|
181
|
-
self.
|
177
|
+
self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
|
182
178
|
undefine_attribute_methods
|
183
179
|
end
|
184
180
|
|
@@ -194,10 +190,9 @@ module ActiveModel
|
|
194
190
|
# alias_attribute :nickname, :name
|
195
191
|
#
|
196
192
|
# private
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
# end
|
193
|
+
# def attribute_short?(attr)
|
194
|
+
# send(attr).length < 5
|
195
|
+
# end
|
201
196
|
# end
|
202
197
|
#
|
203
198
|
# person = Person.new
|
@@ -207,35 +202,53 @@ module ActiveModel
|
|
207
202
|
# person.name_short? # => true
|
208
203
|
# person.nickname_short? # => true
|
209
204
|
def alias_attribute(new_name, old_name)
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
mangled_name = target_name
|
218
|
-
unless NAME_COMPILABLE_REGEXP.match?(target_name)
|
219
|
-
mangled_name = "__temp__#{target_name.unpack1("h*")}"
|
220
|
-
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
|
221
211
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
+
ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
|
220
|
+
attribute_method_patterns.each do |pattern|
|
221
|
+
alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
|
222
|
+
end
|
223
|
+
attribute_method_patterns_cache.clear
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
|
228
|
+
method_name = pattern.method_name(new_name).to_s
|
229
|
+
target_name = pattern.method_name(old_name).to_s
|
230
|
+
parameters = pattern.parameters
|
231
|
+
mangled_name = target_name
|
232
|
+
|
233
|
+
unless NAME_COMPILABLE_REGEXP.match?(target_name)
|
234
|
+
mangled_name = "__temp__#{target_name.unpack1("h*")}"
|
235
|
+
end
|
236
|
+
|
237
|
+
code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
|
238
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
|
239
|
+
"self.#{target_name}(#{parameters || ''})"
|
240
|
+
else
|
241
|
+
call_args = [":'#{target_name}'"]
|
242
|
+
call_args << parameters if parameters
|
243
|
+
"send(#{call_args.join(", ")})"
|
238
244
|
end
|
245
|
+
|
246
|
+
modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
|
247
|
+
|
248
|
+
batch <<
|
249
|
+
"#{modifier}def #{mangled_name}(#{parameters || ''})" <<
|
250
|
+
body <<
|
251
|
+
"end"
|
239
252
|
end
|
240
253
|
end
|
241
254
|
|
@@ -250,7 +263,7 @@ module ActiveModel
|
|
250
263
|
end
|
251
264
|
|
252
265
|
# Declares the attributes that should be prefixed and suffixed by
|
253
|
-
#
|
266
|
+
# +ActiveModel::AttributeMethods+.
|
254
267
|
#
|
255
268
|
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
256
269
|
# +define_attribute_methods+ after you define any prefix, suffix, or affix
|
@@ -268,19 +281,23 @@ module ActiveModel
|
|
268
281
|
# define_attribute_methods :name, :age, :address
|
269
282
|
#
|
270
283
|
# private
|
271
|
-
#
|
272
|
-
#
|
273
|
-
#
|
274
|
-
# end
|
284
|
+
# def clear_attribute(attr)
|
285
|
+
# send("#{attr}=", nil)
|
286
|
+
# end
|
275
287
|
# end
|
276
288
|
def define_attribute_methods(*attr_names)
|
277
289
|
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
278
|
-
attr_names.flatten.each
|
290
|
+
attr_names.flatten.each do |attr_name|
|
291
|
+
define_attribute_method(attr_name, _owner: owner)
|
292
|
+
aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
|
293
|
+
generate_alias_attribute_methods owner, aliased_name, attr_name
|
294
|
+
end
|
295
|
+
end
|
279
296
|
end
|
280
297
|
end
|
281
298
|
|
282
299
|
# Declares an attribute that should be prefixed and suffixed by
|
283
|
-
#
|
300
|
+
# +ActiveModel::AttributeMethods+.
|
284
301
|
#
|
285
302
|
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
286
303
|
# +define_attribute_method+ after you define any prefix, suffix or affix
|
@@ -298,36 +315,54 @@ module ActiveModel
|
|
298
315
|
# define_attribute_method :name
|
299
316
|
#
|
300
317
|
# private
|
301
|
-
#
|
302
|
-
#
|
303
|
-
#
|
304
|
-
# end
|
318
|
+
# def attribute_short?(attr)
|
319
|
+
# send(attr).length < 5
|
320
|
+
# end
|
305
321
|
# end
|
306
322
|
#
|
307
323
|
# person = Person.new
|
308
324
|
# person.name = 'Bob'
|
309
325
|
# person.name # => "Bob"
|
310
326
|
# person.name_short? # => true
|
311
|
-
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
327
|
+
def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
|
312
328
|
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
313
|
-
|
314
|
-
|
329
|
+
attribute_method_patterns.each do |pattern|
|
330
|
+
define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
|
331
|
+
end
|
332
|
+
attribute_method_patterns_cache.clear
|
333
|
+
end
|
334
|
+
end
|
315
335
|
|
316
|
-
|
317
|
-
|
336
|
+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
|
337
|
+
canonical_method_name = pattern.method_name(attr_name)
|
338
|
+
public_method_name = pattern.method_name(as)
|
339
|
+
|
340
|
+
# If defining a regular attribute method, we don't override methods that are explictly
|
341
|
+
# defined in parrent classes.
|
342
|
+
if instance_method_already_implemented?(public_method_name)
|
343
|
+
# However, for `alias_attribute`, we always define the method.
|
344
|
+
# We check for override second because `instance_method_already_implemented?`
|
345
|
+
# also check for dangerous methods.
|
346
|
+
return unless override
|
347
|
+
end
|
318
348
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
349
|
+
generate_method = "define_method_#{pattern.proxy_target}"
|
350
|
+
if respond_to?(generate_method, true)
|
351
|
+
send(generate_method, attr_name.to_s, owner: owner, as: as)
|
352
|
+
else
|
353
|
+
define_proxy_call(
|
354
|
+
owner,
|
355
|
+
canonical_method_name,
|
356
|
+
pattern.proxy_target,
|
357
|
+
pattern.parameters,
|
358
|
+
attr_name.to_s,
|
359
|
+
namespace: :active_model_proxy,
|
360
|
+
as: public_method_name,
|
361
|
+
)
|
327
362
|
end
|
328
363
|
end
|
329
364
|
|
330
|
-
# Removes all the previously dynamically defined methods from the class.
|
365
|
+
# Removes all the previously dynamically defined methods from the class, including alias attribute methods.
|
331
366
|
#
|
332
367
|
# class Person
|
333
368
|
# include ActiveModel::AttributeMethods
|
@@ -335,29 +370,46 @@ module ActiveModel
|
|
335
370
|
# attr_accessor :name
|
336
371
|
# attribute_method_suffix '_short?'
|
337
372
|
# define_attribute_method :name
|
373
|
+
# alias_attribute :first_name, :name
|
338
374
|
#
|
339
375
|
# private
|
340
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
# end
|
376
|
+
# def attribute_short?(attr)
|
377
|
+
# send(attr).length < 5
|
378
|
+
# end
|
344
379
|
# end
|
345
380
|
#
|
346
381
|
# person = Person.new
|
347
382
|
# person.name = 'Bob'
|
383
|
+
# person.first_name # => "Bob"
|
348
384
|
# person.name_short? # => true
|
349
385
|
#
|
350
386
|
# Person.undefine_attribute_methods
|
351
387
|
#
|
352
388
|
# person.name_short? # => NoMethodError
|
389
|
+
# person.first_name # => NoMethodError
|
353
390
|
def undefine_attribute_methods
|
354
391
|
generated_attribute_methods.module_eval do
|
355
392
|
undef_method(*instance_methods)
|
356
393
|
end
|
357
|
-
|
394
|
+
attribute_method_patterns_cache.clear
|
395
|
+
end
|
396
|
+
|
397
|
+
def aliases_by_attribute_name # :nodoc:
|
398
|
+
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
|
358
399
|
end
|
359
400
|
|
360
401
|
private
|
402
|
+
def inherited(base) # :nodoc:
|
403
|
+
super
|
404
|
+
base.class_eval do
|
405
|
+
@attribute_method_patterns_cache = nil
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def resolve_attribute_name(name)
|
410
|
+
attribute_aliases.fetch(super, &:itself)
|
411
|
+
end
|
412
|
+
|
361
413
|
def generated_attribute_methods
|
362
414
|
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
363
415
|
end
|
@@ -375,33 +427,34 @@ module ActiveModel
|
|
375
427
|
# used to alleviate the GC, which ultimately also speeds up the app
|
376
428
|
# significantly (in our case our test suite finishes 10% faster with
|
377
429
|
# this cache).
|
378
|
-
def
|
379
|
-
@
|
430
|
+
def attribute_method_patterns_cache
|
431
|
+
@attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
380
432
|
end
|
381
433
|
|
382
|
-
def
|
383
|
-
|
384
|
-
|
434
|
+
def attribute_method_patterns_matching(method_name)
|
435
|
+
attribute_method_patterns_cache.compute_if_absent(method_name) do
|
436
|
+
attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
|
385
437
|
end
|
386
438
|
end
|
387
439
|
|
388
440
|
# Define a method `name` in `mod` that dispatches to `send`
|
389
441
|
# using the given `extra` args. This falls back on `send`
|
390
442
|
# if the called name cannot be compiled.
|
391
|
-
def define_proxy_call(code_generator, name,
|
443
|
+
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
|
392
444
|
mangled_name = name
|
393
445
|
unless NAME_COMPILABLE_REGEXP.match?(name)
|
394
446
|
mangled_name = "__temp__#{name.unpack1("h*")}"
|
395
447
|
end
|
396
448
|
|
397
|
-
|
398
|
-
|
399
|
-
|
449
|
+
call_args.map!(&:inspect)
|
450
|
+
call_args << parameters if parameters
|
451
|
+
namespace = :"#{namespace}_#{proxy_target}"
|
400
452
|
|
401
|
-
|
402
|
-
|
453
|
+
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
|
454
|
+
body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
|
455
|
+
"self.#{proxy_target}(#{call_args.join(", ")})"
|
403
456
|
else
|
404
|
-
call_args.unshift(":'#{
|
457
|
+
call_args.unshift(":'#{proxy_target}'")
|
405
458
|
"send(#{call_args.join(", ")})"
|
406
459
|
end
|
407
460
|
|
@@ -414,23 +467,23 @@ module ActiveModel
|
|
414
467
|
end
|
415
468
|
end
|
416
469
|
|
417
|
-
class
|
418
|
-
attr_reader :prefix, :suffix, :
|
470
|
+
class AttributeMethodPattern # :nodoc:
|
471
|
+
attr_reader :prefix, :suffix, :proxy_target, :parameters
|
419
472
|
|
420
|
-
|
473
|
+
AttributeMethod = Struct.new(:proxy_target, :attr_name)
|
421
474
|
|
422
475
|
def initialize(prefix: "", suffix: "", parameters: nil)
|
423
476
|
@prefix = prefix
|
424
477
|
@suffix = suffix
|
425
478
|
@parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
|
426
|
-
@regex =
|
427
|
-
@
|
479
|
+
@regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
|
480
|
+
@proxy_target = "#{@prefix}attribute#{@suffix}"
|
428
481
|
@method_name = "#{prefix}%s#{suffix}"
|
429
482
|
end
|
430
483
|
|
431
484
|
def match(method_name)
|
432
485
|
if @regex =~ method_name
|
433
|
-
|
486
|
+
AttributeMethod.new(proxy_target, $1)
|
434
487
|
end
|
435
488
|
end
|
436
489
|
|
@@ -465,7 +518,7 @@ module ActiveModel
|
|
465
518
|
# attribute method. If so, we tell +attribute_missing+ to dispatch the
|
466
519
|
# attribute. This method can be overloaded to customize the behavior.
|
467
520
|
def attribute_missing(match, *args, &block)
|
468
|
-
__send__(match.
|
521
|
+
__send__(match.proxy_target, match.attr_name, *args, &block)
|
469
522
|
end
|
470
523
|
ruby2_keywords(:attribute_missing)
|
471
524
|
|
@@ -493,12 +546,12 @@ module ActiveModel
|
|
493
546
|
# Returns a struct representing the matching attribute method.
|
494
547
|
# The struct's attributes are prefix, base and suffix.
|
495
548
|
def matched_attribute_method(method_name)
|
496
|
-
matches = self.class.send(:
|
549
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
497
550
|
matches.detect { |match| attribute_method?(match.attr_name) }
|
498
551
|
end
|
499
552
|
|
500
553
|
def missing_attribute(attr_name, stack)
|
501
|
-
raise ActiveModel::MissingAttributeError, "missing attribute
|
554
|
+
raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
|
502
555
|
end
|
503
556
|
|
504
557
|
def _read_attribute(attr)
|
@@ -43,8 +43,8 @@ module ActiveModel
|
|
43
43
|
|
44
44
|
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
|
45
45
|
attribute_changed?(attr_name) &&
|
46
|
-
(OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
|
47
|
-
(OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
|
46
|
+
(OPTION_NOT_GIVEN == from || original_value(attr_name) == type_cast(attr_name, from)) &&
|
47
|
+
(OPTION_NOT_GIVEN == to || fetch_value(attr_name) == type_cast(attr_name, to))
|
48
48
|
end
|
49
49
|
|
50
50
|
def changed_in_place?(attr_name)
|
@@ -82,6 +82,10 @@ module ActiveModel
|
|
82
82
|
def fetch_value(attr_name)
|
83
83
|
attributes.fetch_value(attr_name)
|
84
84
|
end
|
85
|
+
|
86
|
+
def type_cast(attr_name, value)
|
87
|
+
attributes[attr_name].type_cast(value)
|
88
|
+
end
|
85
89
|
end
|
86
90
|
|
87
91
|
class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
|
@@ -143,6 +147,10 @@ module ActiveModel
|
|
143
147
|
rescue TypeError, NoMethodError
|
144
148
|
value
|
145
149
|
end
|
150
|
+
|
151
|
+
def type_cast(attr_name, value)
|
152
|
+
value
|
153
|
+
end
|
146
154
|
end
|
147
155
|
|
148
156
|
class NullMutationTracker # :nodoc:
|
@@ -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
|