activemodel 4.2.0 → 6.1.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 +5 -5
- data/CHANGELOG.md +49 -37
- data/MIT-LICENSE +1 -1
- data/README.rdoc +16 -22
- data/lib/active_model/attribute/user_provided_default.rb +51 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute_assignment.rb +55 -0
- data/lib/active_model/attribute_methods.rb +150 -73
- data/lib/active_model/attribute_mutation_tracker.rb +181 -0
- data/lib/active_model/attribute_set/builder.rb +191 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +106 -0
- data/lib/active_model/attributes.rb +132 -0
- data/lib/active_model/callbacks.rb +31 -25
- data/lib/active_model/conversion.rb +20 -9
- data/lib/active_model/dirty.rb +142 -116
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +436 -202
- data/lib/active_model/forbidden_attributes_protection.rb +6 -3
- data/lib/active_model/gem_version.rb +5 -3
- data/lib/active_model/lint.rb +47 -42
- data/lib/active_model/locale/en.yml +2 -1
- data/lib/active_model/model.rb +7 -7
- data/lib/active_model/naming.rb +36 -18
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +8 -0
- data/lib/active_model/secure_password.rb +61 -67
- data/lib/active_model/serialization.rb +48 -17
- data/lib/active_model/serializers/json.rb +22 -13
- data/lib/active_model/translation.rb +5 -4
- data/lib/active_model/type/big_integer.rb +14 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +46 -0
- data/lib/active_model/type/date.rb +52 -0
- data/lib/active_model/type/date_time.rb +46 -0
- data/lib/active_model/type/decimal.rb +69 -0
- data/lib/active_model/type/float.rb +35 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +48 -0
- data/lib/active_model/type/helpers/time_value.rb +90 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +35 -0
- data/lib/active_model/type/integer.rb +67 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +35 -0
- data/lib/active_model/type/time.rb +46 -0
- data/lib/active_model/type/value.rb +133 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/validations/absence.rb +6 -4
- data/lib/active_model/validations/acceptance.rb +72 -14
- data/lib/active_model/validations/callbacks.rb +23 -19
- data/lib/active_model/validations/clusivity.rb +18 -12
- data/lib/active_model/validations/confirmation.rb +27 -14
- data/lib/active_model/validations/exclusion.rb +7 -4
- data/lib/active_model/validations/format.rb +27 -27
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +8 -7
- data/lib/active_model/validations/length.rb +35 -32
- data/lib/active_model/validations/numericality.rb +72 -34
- data/lib/active_model/validations/presence.rb +3 -3
- data/lib/active_model/validations/validates.rb +17 -15
- data/lib/active_model/validations/with.rb +6 -12
- data/lib/active_model/validations.rb +58 -23
- data/lib/active_model/validator.rb +23 -17
- data/lib/active_model/version.rb +4 -2
- data/lib/active_model.rb +18 -11
- metadata +44 -25
- data/lib/active_model/serializers/xml.rb +0 -238
- data/lib/active_model/test_case.rb +0 -4
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/map"
|
3
4
|
|
4
5
|
module ActiveModel
|
5
6
|
# Raised when an attribute is not defined.
|
@@ -23,7 +24,7 @@ module ActiveModel
|
|
23
24
|
# The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
|
24
25
|
#
|
25
26
|
# * <tt>include ActiveModel::AttributeMethods</tt> in your class.
|
26
|
-
# * Call each of its
|
27
|
+
# * Call each of its methods you want to add, such as +attribute_method_suffix+
|
27
28
|
# or +attribute_method_prefix+.
|
28
29
|
# * Call +define_attribute_methods+ after the other methods are called.
|
29
30
|
# * Define the various generic +_attribute+ methods that you have declared.
|
@@ -68,9 +69,8 @@ module ActiveModel
|
|
68
69
|
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
|
69
70
|
|
70
71
|
included do
|
71
|
-
class_attribute :attribute_aliases, :
|
72
|
-
|
73
|
-
self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
|
72
|
+
class_attribute :attribute_aliases, instance_writer: false, default: {}
|
73
|
+
class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
|
74
74
|
end
|
75
75
|
|
76
76
|
module ClassMethods
|
@@ -207,10 +207,12 @@ module ActiveModel
|
|
207
207
|
# person.nickname_short? # => true
|
208
208
|
def alias_attribute(new_name, old_name)
|
209
209
|
self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
210
|
+
CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
|
211
|
+
attribute_method_matchers.each do |matcher|
|
212
|
+
matcher_new = matcher.method_name(new_name).to_s
|
213
|
+
matcher_old = matcher.method_name(old_name).to_s
|
214
|
+
define_proxy_call false, owner, matcher_new, matcher_old
|
215
|
+
end
|
214
216
|
end
|
215
217
|
end
|
216
218
|
|
@@ -225,9 +227,9 @@ module ActiveModel
|
|
225
227
|
end
|
226
228
|
|
227
229
|
# Declares the attributes that should be prefixed and suffixed by
|
228
|
-
# ActiveModel::AttributeMethods
|
230
|
+
# <tt>ActiveModel::AttributeMethods</tt>.
|
229
231
|
#
|
230
|
-
# To use, pass attribute names (as strings or symbols)
|
232
|
+
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
231
233
|
# +define_attribute_methods+ after you define any prefix, suffix or affix
|
232
234
|
# methods, or they will not hook in.
|
233
235
|
#
|
@@ -239,7 +241,7 @@ module ActiveModel
|
|
239
241
|
#
|
240
242
|
# # Call to define_attribute_methods must appear after the
|
241
243
|
# # attribute_method_prefix, attribute_method_suffix or
|
242
|
-
# # attribute_method_affix
|
244
|
+
# # attribute_method_affix declarations.
|
243
245
|
# define_attribute_methods :name, :age, :address
|
244
246
|
#
|
245
247
|
# private
|
@@ -249,13 +251,15 @@ module ActiveModel
|
|
249
251
|
# end
|
250
252
|
# end
|
251
253
|
def define_attribute_methods(*attr_names)
|
252
|
-
|
254
|
+
CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
255
|
+
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
|
256
|
+
end
|
253
257
|
end
|
254
258
|
|
255
259
|
# Declares an attribute that should be prefixed and suffixed by
|
256
|
-
# ActiveModel::AttributeMethods
|
260
|
+
# <tt>ActiveModel::AttributeMethods</tt>.
|
257
261
|
#
|
258
|
-
# To use, pass an attribute name (as string or symbol)
|
262
|
+
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
259
263
|
# +define_attribute_method+ after you define any prefix, suffix or affix
|
260
264
|
# method, or they will not hook in.
|
261
265
|
#
|
@@ -267,7 +271,7 @@ module ActiveModel
|
|
267
271
|
#
|
268
272
|
# # Call to define_attribute_method must appear after the
|
269
273
|
# # attribute_method_prefix, attribute_method_suffix or
|
270
|
-
# # attribute_method_affix
|
274
|
+
# # attribute_method_affix declarations.
|
271
275
|
# define_attribute_method :name
|
272
276
|
#
|
273
277
|
# private
|
@@ -281,21 +285,23 @@ module ActiveModel
|
|
281
285
|
# person.name = 'Bob'
|
282
286
|
# person.name # => "Bob"
|
283
287
|
# person.name_short? # => true
|
284
|
-
def define_attribute_method(attr_name)
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
288
|
+
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
289
|
+
CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
290
|
+
attribute_method_matchers.each do |matcher|
|
291
|
+
method_name = matcher.method_name(attr_name)
|
292
|
+
|
293
|
+
unless instance_method_already_implemented?(method_name)
|
294
|
+
generate_method = "define_method_#{matcher.target}"
|
295
|
+
|
296
|
+
if respond_to?(generate_method, true)
|
297
|
+
send(generate_method, attr_name.to_s, owner: owner)
|
298
|
+
else
|
299
|
+
define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
|
300
|
+
end
|
295
301
|
end
|
296
302
|
end
|
303
|
+
attribute_method_matchers_cache.clear
|
297
304
|
end
|
298
|
-
attribute_method_matchers_cache.clear
|
299
305
|
end
|
300
306
|
|
301
307
|
# Removes all the previously dynamically defined methods from the class.
|
@@ -323,50 +329,84 @@ module ActiveModel
|
|
323
329
|
# person.name_short? # => NoMethodError
|
324
330
|
def undefine_attribute_methods
|
325
331
|
generated_attribute_methods.module_eval do
|
326
|
-
|
332
|
+
undef_method(*instance_methods)
|
327
333
|
end
|
328
334
|
attribute_method_matchers_cache.clear
|
329
335
|
end
|
330
336
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
337
|
+
private
|
338
|
+
class CodeGenerator
|
339
|
+
class << self
|
340
|
+
def batch(owner, path, line)
|
341
|
+
if owner.is_a?(CodeGenerator)
|
342
|
+
yield owner
|
343
|
+
else
|
344
|
+
instance = new(owner, path, line)
|
345
|
+
result = yield instance
|
346
|
+
instance.execute
|
347
|
+
result
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def initialize(owner, path, line)
|
353
|
+
@owner = owner
|
354
|
+
@path = path
|
355
|
+
@line = line
|
356
|
+
@sources = ["# frozen_string_literal: true\n"]
|
357
|
+
@renames = {}
|
358
|
+
end
|
359
|
+
|
360
|
+
def <<(source_line)
|
361
|
+
@sources << source_line
|
362
|
+
end
|
336
363
|
|
337
|
-
|
338
|
-
|
364
|
+
def rename_method(old_name, new_name)
|
365
|
+
@renames[old_name] = new_name
|
366
|
+
end
|
367
|
+
|
368
|
+
def execute
|
369
|
+
@owner.module_eval(@sources.join(";"), @path, @line - 1)
|
370
|
+
@renames.each do |old_name, new_name|
|
371
|
+
@owner.alias_method new_name, old_name
|
372
|
+
@owner.undef_method old_name
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
private_constant :CodeGenerator
|
377
|
+
|
378
|
+
def generated_attribute_methods
|
379
|
+
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
380
|
+
end
|
381
|
+
|
382
|
+
def instance_method_already_implemented?(method_name)
|
339
383
|
generated_attribute_methods.method_defined?(method_name)
|
340
384
|
end
|
341
385
|
|
342
|
-
private
|
343
386
|
# The methods +method_missing+ and +respond_to?+ of this module are
|
344
387
|
# invoked often in a typical rails, both of which invoke the method
|
345
|
-
# +
|
388
|
+
# +matched_attribute_method+. The latter method iterates through an
|
346
389
|
# array doing regular expression matches, which results in a lot of
|
347
390
|
# object creations. Most of the time it returns a +nil+ match. As the
|
348
391
|
# match result is always the same given a +method_name+, this cache is
|
349
392
|
# used to alleviate the GC, which ultimately also speeds up the app
|
350
393
|
# significantly (in our case our test suite finishes 10% faster with
|
351
394
|
# this cache).
|
352
|
-
def attribute_method_matchers_cache
|
353
|
-
@attribute_method_matchers_cache ||=
|
395
|
+
def attribute_method_matchers_cache
|
396
|
+
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
354
397
|
end
|
355
398
|
|
356
|
-
def attribute_method_matchers_matching(method_name)
|
399
|
+
def attribute_method_matchers_matching(method_name)
|
357
400
|
attribute_method_matchers_cache.compute_if_absent(method_name) do
|
358
|
-
|
359
|
-
# will match every time.
|
360
|
-
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
|
361
|
-
matchers.map { |method| method.match(method_name) }.compact
|
401
|
+
attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
|
362
402
|
end
|
363
403
|
end
|
364
404
|
|
365
405
|
# Define a method `name` in `mod` that dispatches to `send`
|
366
|
-
# using the given `extra` args. This
|
406
|
+
# using the given `extra` args. This falls back on `define_method`
|
367
407
|
# and `send` if the given names cannot be compiled.
|
368
|
-
def define_proxy_call(include_private,
|
369
|
-
defn = if name
|
408
|
+
def define_proxy_call(include_private, code_generator, name, target, *extra)
|
409
|
+
defn = if NAME_COMPILABLE_REGEXP.match?(name)
|
370
410
|
"def #{name}(*args)"
|
371
411
|
else
|
372
412
|
"define_method(:'#{name}') do |*args|"
|
@@ -374,44 +414,40 @@ module ActiveModel
|
|
374
414
|
|
375
415
|
extra = (extra.map!(&:inspect) << "*args").join(", ")
|
376
416
|
|
377
|
-
|
378
|
-
"#{"self." unless include_private}#{
|
417
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target)
|
418
|
+
"#{"self." unless include_private}#{target}(#{extra})"
|
379
419
|
else
|
380
|
-
"send(:'#{
|
420
|
+
"send(:'#{target}', #{extra})"
|
381
421
|
end
|
382
422
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
end
|
387
|
-
|
423
|
+
code_generator <<
|
424
|
+
defn <<
|
425
|
+
body <<
|
426
|
+
"end" <<
|
427
|
+
"ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
|
388
428
|
end
|
389
429
|
|
390
430
|
class AttributeMethodMatcher #:nodoc:
|
391
|
-
attr_reader :prefix, :suffix, :
|
431
|
+
attr_reader :prefix, :suffix, :target
|
392
432
|
|
393
|
-
AttributeMethodMatch = Struct.new(:target, :attr_name
|
433
|
+
AttributeMethodMatch = Struct.new(:target, :attr_name)
|
394
434
|
|
395
435
|
def initialize(options = {})
|
396
|
-
@prefix, @suffix = options.fetch(:prefix,
|
436
|
+
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
|
397
437
|
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
|
398
|
-
@
|
438
|
+
@target = "#{@prefix}attribute#{@suffix}"
|
399
439
|
@method_name = "#{prefix}%s#{suffix}"
|
400
440
|
end
|
401
441
|
|
402
442
|
def match(method_name)
|
403
443
|
if @regex =~ method_name
|
404
|
-
AttributeMethodMatch.new(
|
444
|
+
AttributeMethodMatch.new(target, $1)
|
405
445
|
end
|
406
446
|
end
|
407
447
|
|
408
448
|
def method_name(attr_name)
|
409
449
|
@method_name % attr_name
|
410
450
|
end
|
411
|
-
|
412
|
-
def plain?
|
413
|
-
prefix.empty? && suffix.empty?
|
414
|
-
end
|
415
451
|
end
|
416
452
|
end
|
417
453
|
|
@@ -419,7 +455,7 @@ module ActiveModel
|
|
419
455
|
# returned by <tt>attributes</tt>, as though they were first-class
|
420
456
|
# methods. So a +Person+ class with a +name+ attribute can for example use
|
421
457
|
# <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
|
422
|
-
# the attributes hash -- except for multiple
|
458
|
+
# the attributes hash -- except for multiple assignments with
|
423
459
|
# <tt>ActiveRecord::Base#attributes=</tt>.
|
424
460
|
#
|
425
461
|
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
@@ -429,10 +465,11 @@ module ActiveModel
|
|
429
465
|
if respond_to_without_attributes?(method, true)
|
430
466
|
super
|
431
467
|
else
|
432
|
-
match =
|
468
|
+
match = matched_attribute_method(method.to_s)
|
433
469
|
match ? attribute_missing(match, *args, &block) : super
|
434
470
|
end
|
435
471
|
end
|
472
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
436
473
|
|
437
474
|
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
438
475
|
# +method_missing+ is called we check to see if there is a matching
|
@@ -454,19 +491,18 @@ module ActiveModel
|
|
454
491
|
# but found among all methods. Which means that the given method is private.
|
455
492
|
false
|
456
493
|
else
|
457
|
-
!
|
494
|
+
!matched_attribute_method(method.to_s).nil?
|
458
495
|
end
|
459
496
|
end
|
460
497
|
|
461
|
-
|
462
|
-
def attribute_method?(attr_name)
|
498
|
+
private
|
499
|
+
def attribute_method?(attr_name)
|
463
500
|
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
|
464
501
|
end
|
465
502
|
|
466
|
-
private
|
467
503
|
# Returns a struct representing the matching attribute method.
|
468
504
|
# The struct's attributes are prefix, base and suffix.
|
469
|
-
def
|
505
|
+
def matched_attribute_method(method_name)
|
470
506
|
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
471
507
|
matches.detect { |match| attribute_method?(match.attr_name) }
|
472
508
|
end
|
@@ -474,5 +510,46 @@ module ActiveModel
|
|
474
510
|
def missing_attribute(attr_name, stack)
|
475
511
|
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
476
512
|
end
|
513
|
+
|
514
|
+
def _read_attribute(attr)
|
515
|
+
__send__(attr)
|
516
|
+
end
|
517
|
+
|
518
|
+
module AttrNames # :nodoc:
|
519
|
+
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
|
520
|
+
|
521
|
+
# We want to generate the methods via module_eval rather than
|
522
|
+
# define_method, because define_method is slower on dispatch.
|
523
|
+
# Evaluating many similar methods may use more memory as the instruction
|
524
|
+
# sequences are duplicated and cached (in MRI). define_method may
|
525
|
+
# be slower on dispatch, but if you're careful about the closure
|
526
|
+
# created, then define_method will consume much less memory.
|
527
|
+
#
|
528
|
+
# But sometimes the database might return columns with
|
529
|
+
# characters that are not allowed in normal method names (like
|
530
|
+
# 'my_column(omg)'. So to work around this we first define with
|
531
|
+
# the __temp__ identifier, and then use alias method to rename
|
532
|
+
# it to what we want.
|
533
|
+
#
|
534
|
+
# We are also defining a constant to hold the frozen string of
|
535
|
+
# the attribute name. Using a constant means that we do not have
|
536
|
+
# to allocate an object on each call to the attribute method.
|
537
|
+
# Making it frozen means that it doesn't get duped when used to
|
538
|
+
# key the @attributes in read_attribute.
|
539
|
+
def self.define_attribute_accessor_method(owner, attr_name, writer: false)
|
540
|
+
method_name = "#{attr_name}#{'=' if writer}"
|
541
|
+
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
|
542
|
+
yield method_name, "'#{attr_name}'"
|
543
|
+
else
|
544
|
+
safe_name = attr_name.unpack1("h*")
|
545
|
+
const_name = "ATTR_#{safe_name}"
|
546
|
+
const_set(const_name, attr_name) unless const_defined?(const_name)
|
547
|
+
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
|
548
|
+
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
|
549
|
+
yield temp_method_name, attr_name_expr
|
550
|
+
owner.rename_method(temp_method_name, method_name)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
477
554
|
end
|
478
555
|
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
4
|
+
require "active_support/core_ext/object/duplicable"
|
5
|
+
|
6
|
+
module ActiveModel
|
7
|
+
class AttributeMutationTracker # :nodoc:
|
8
|
+
OPTION_NOT_GIVEN = Object.new
|
9
|
+
|
10
|
+
def initialize(attributes)
|
11
|
+
@attributes = attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def changed_attribute_names
|
15
|
+
attr_names.select { |attr_name| changed?(attr_name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def changed_values
|
19
|
+
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
20
|
+
if changed?(attr_name)
|
21
|
+
result[attr_name] = original_value(attr_name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def changes
|
27
|
+
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
28
|
+
if change = change_to_attribute(attr_name)
|
29
|
+
result.merge!(attr_name => change)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def change_to_attribute(attr_name)
|
35
|
+
if changed?(attr_name)
|
36
|
+
[original_value(attr_name), fetch_value(attr_name)]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def any_changes?
|
41
|
+
attr_names.any? { |attr| changed?(attr) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
|
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)
|
48
|
+
end
|
49
|
+
|
50
|
+
def changed_in_place?(attr_name)
|
51
|
+
attributes[attr_name].changed_in_place?
|
52
|
+
end
|
53
|
+
|
54
|
+
def forget_change(attr_name)
|
55
|
+
attributes[attr_name] = attributes[attr_name].forgetting_assignment
|
56
|
+
forced_changes.delete(attr_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def original_value(attr_name)
|
60
|
+
attributes[attr_name].original_value
|
61
|
+
end
|
62
|
+
|
63
|
+
def force_change(attr_name)
|
64
|
+
forced_changes[attr_name] = fetch_value(attr_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
attr_reader :attributes
|
69
|
+
|
70
|
+
def forced_changes
|
71
|
+
@forced_changes ||= {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def attr_names
|
75
|
+
attributes.keys
|
76
|
+
end
|
77
|
+
|
78
|
+
def attribute_changed?(attr_name)
|
79
|
+
forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
|
80
|
+
end
|
81
|
+
|
82
|
+
def fetch_value(attr_name)
|
83
|
+
attributes.fetch_value(attr_name)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
|
88
|
+
def initialize(attributes)
|
89
|
+
super
|
90
|
+
@finalized_changes = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def changed_in_place?(attr_name)
|
94
|
+
false
|
95
|
+
end
|
96
|
+
|
97
|
+
def change_to_attribute(attr_name)
|
98
|
+
if finalized_changes&.include?(attr_name)
|
99
|
+
finalized_changes[attr_name].dup
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def forget_change(attr_name)
|
106
|
+
forced_changes.delete(attr_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def original_value(attr_name)
|
110
|
+
if changed?(attr_name)
|
111
|
+
forced_changes[attr_name]
|
112
|
+
else
|
113
|
+
fetch_value(attr_name)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def force_change(attr_name)
|
118
|
+
forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
|
119
|
+
end
|
120
|
+
|
121
|
+
def finalize_changes
|
122
|
+
@finalized_changes = changes
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
attr_reader :finalized_changes
|
127
|
+
|
128
|
+
def attr_names
|
129
|
+
forced_changes.keys
|
130
|
+
end
|
131
|
+
|
132
|
+
def attribute_changed?(attr_name)
|
133
|
+
forced_changes.include?(attr_name)
|
134
|
+
end
|
135
|
+
|
136
|
+
def fetch_value(attr_name)
|
137
|
+
attributes.send(:_read_attribute, attr_name)
|
138
|
+
end
|
139
|
+
|
140
|
+
def clone_value(attr_name)
|
141
|
+
value = fetch_value(attr_name)
|
142
|
+
value.duplicable? ? value.clone : value
|
143
|
+
rescue TypeError, NoMethodError
|
144
|
+
value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class NullMutationTracker # :nodoc:
|
149
|
+
include Singleton
|
150
|
+
|
151
|
+
def changed_attribute_names
|
152
|
+
[]
|
153
|
+
end
|
154
|
+
|
155
|
+
def changed_values
|
156
|
+
{}
|
157
|
+
end
|
158
|
+
|
159
|
+
def changes
|
160
|
+
{}
|
161
|
+
end
|
162
|
+
|
163
|
+
def change_to_attribute(attr_name)
|
164
|
+
end
|
165
|
+
|
166
|
+
def any_changes?
|
167
|
+
false
|
168
|
+
end
|
169
|
+
|
170
|
+
def changed?(attr_name, **)
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def changed_in_place?(attr_name)
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
def original_value(attr_name)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|