activemodel 7.1.5.2 → 8.1.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 +21 -281
- data/README.rdoc +10 -10
- data/lib/active_model/attribute/user_provided_default.rb +10 -0
- data/lib/active_model/attribute.rb +10 -2
- data/lib/active_model/attribute_assignment.rb +26 -3
- data/lib/active_model/attribute_methods.rb +42 -43
- data/lib/active_model/attribute_mutation_tracker.rb +5 -5
- data/lib/active_model/attribute_registration.rb +62 -22
- data/lib/active_model/attribute_set.rb +1 -1
- data/lib/active_model/attributes/normalization.rb +192 -0
- data/lib/active_model/attributes.rb +13 -0
- data/lib/active_model/callbacks.rb +1 -1
- data/lib/active_model/conversion.rb +1 -1
- data/lib/active_model/dirty.rb +16 -9
- data/lib/active_model/error.rb +3 -2
- data/lib/active_model/errors.rb +1 -4
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +7 -3
- data/lib/active_model/model.rb +2 -2
- data/lib/active_model/naming.rb +0 -1
- data/lib/active_model/nested_error.rb +1 -3
- data/lib/active_model/railtie.rb +8 -4
- data/lib/active_model/secure_password.rb +62 -5
- data/lib/active_model/serialization.rb +21 -21
- data/lib/active_model/translation.rb +22 -5
- data/lib/active_model/type/big_integer.rb +21 -0
- data/lib/active_model/type/boolean.rb +1 -0
- data/lib/active_model/type/date.rb +1 -0
- data/lib/active_model/type/date_time.rb +8 -0
- data/lib/active_model/type/decimal.rb +1 -0
- data/lib/active_model/type/float.rb +9 -8
- data/lib/active_model/type/helpers/immutable.rb +13 -0
- data/lib/active_model/type/helpers/time_value.rb +12 -15
- data/lib/active_model/type/helpers/timezone.rb +5 -1
- data/lib/active_model/type/helpers.rb +1 -0
- data/lib/active_model/type/immutable_string.rb +2 -0
- data/lib/active_model/type/integer.rb +31 -19
- data/lib/active_model/type/registry.rb +2 -3
- data/lib/active_model/type/string.rb +4 -0
- data/lib/active_model/type/value.rb +4 -4
- data/lib/active_model/validations/acceptance.rb +1 -1
- data/lib/active_model/validations/callbacks.rb +11 -1
- data/lib/active_model/validations/comparison.rb +1 -1
- data/lib/active_model/validations/validates.rb +8 -3
- data/lib/active_model/validations.rb +57 -32
- data/lib/active_model.rb +6 -0
- metadata +10 -8
|
@@ -66,7 +66,6 @@ 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: {}
|
|
@@ -228,28 +227,13 @@ module ActiveModel
|
|
|
228
227
|
method_name = pattern.method_name(new_name).to_s
|
|
229
228
|
target_name = pattern.method_name(old_name).to_s
|
|
230
229
|
parameters = pattern.parameters
|
|
231
|
-
mangled_name = target_name
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
mangled_name = "__temp__#{target_name.unpack1("h*")}"
|
|
235
|
-
end
|
|
231
|
+
mangled_name = build_mangled_name(target_name)
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"self.#{target_name}(#{parameters || ''})"
|
|
240
|
-
else
|
|
241
|
-
call_args = [":'#{target_name}'"]
|
|
242
|
-
call_args << parameters if parameters
|
|
243
|
-
"send(#{call_args.join(", ")})"
|
|
244
|
-
end
|
|
233
|
+
call_args = []
|
|
234
|
+
call_args << parameters if parameters
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
batch <<
|
|
249
|
-
"#{modifier}def #{mangled_name}(#{parameters || ''})" <<
|
|
250
|
-
body <<
|
|
251
|
-
"end"
|
|
252
|
-
end
|
|
236
|
+
define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
|
|
253
237
|
end
|
|
254
238
|
|
|
255
239
|
# Is +new_name+ an alias?
|
|
@@ -337,8 +321,8 @@ module ActiveModel
|
|
|
337
321
|
canonical_method_name = pattern.method_name(attr_name)
|
|
338
322
|
public_method_name = pattern.method_name(as)
|
|
339
323
|
|
|
340
|
-
# If defining a regular attribute method, we don't override methods that are
|
|
341
|
-
# defined in
|
|
324
|
+
# If defining a regular attribute method, we don't override methods that are explicitly
|
|
325
|
+
# defined in parent classes.
|
|
342
326
|
if instance_method_already_implemented?(public_method_name)
|
|
343
327
|
# However, for `alias_attribute`, we always define the method.
|
|
344
328
|
# We check for override second because `instance_method_already_implemented?`
|
|
@@ -347,6 +331,7 @@ module ActiveModel
|
|
|
347
331
|
end
|
|
348
332
|
|
|
349
333
|
generate_method = "define_method_#{pattern.proxy_target}"
|
|
334
|
+
|
|
350
335
|
if respond_to?(generate_method, true)
|
|
351
336
|
send(generate_method, attr_name.to_s, owner: owner, as: as)
|
|
352
337
|
else
|
|
@@ -357,7 +342,7 @@ module ActiveModel
|
|
|
357
342
|
pattern.parameters,
|
|
358
343
|
attr_name.to_s,
|
|
359
344
|
namespace: :active_model_proxy,
|
|
360
|
-
as: public_method_name
|
|
345
|
+
as: public_method_name
|
|
361
346
|
)
|
|
362
347
|
end
|
|
363
348
|
end
|
|
@@ -388,7 +373,7 @@ module ActiveModel
|
|
|
388
373
|
# person.name_short? # => NoMethodError
|
|
389
374
|
# person.first_name # => NoMethodError
|
|
390
375
|
def undefine_attribute_methods
|
|
391
|
-
generated_attribute_methods
|
|
376
|
+
@generated_attribute_methods&.module_eval do
|
|
392
377
|
undef_method(*instance_methods)
|
|
393
378
|
end
|
|
394
379
|
attribute_method_patterns_cache.clear
|
|
@@ -403,6 +388,8 @@ module ActiveModel
|
|
|
403
388
|
super
|
|
404
389
|
base.class_eval do
|
|
405
390
|
@attribute_method_patterns_cache = nil
|
|
391
|
+
@aliases_by_attribute_name = nil
|
|
392
|
+
@generated_attribute_methods = nil
|
|
406
393
|
end
|
|
407
394
|
end
|
|
408
395
|
|
|
@@ -415,7 +402,7 @@ module ActiveModel
|
|
|
415
402
|
end
|
|
416
403
|
|
|
417
404
|
def instance_method_already_implemented?(method_name)
|
|
418
|
-
generated_attribute_methods
|
|
405
|
+
@generated_attribute_methods&.method_defined?(method_name)
|
|
419
406
|
end
|
|
420
407
|
|
|
421
408
|
# The methods +method_missing+ and +respond_to?+ of this module are
|
|
@@ -441,27 +428,41 @@ module ActiveModel
|
|
|
441
428
|
# using the given `extra` args. This falls back on `send`
|
|
442
429
|
# if the called name cannot be compiled.
|
|
443
430
|
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
|
|
444
|
-
mangled_name = name
|
|
445
|
-
unless NAME_COMPILABLE_REGEXP.match?(name)
|
|
446
|
-
mangled_name = "__temp__#{name.unpack1("h*")}"
|
|
447
|
-
end
|
|
431
|
+
mangled_name = build_mangled_name(name)
|
|
448
432
|
|
|
449
433
|
call_args.map!(&:inspect)
|
|
450
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
|
|
451
440
|
namespace = :"#{namespace}_#{proxy_target}"
|
|
452
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)
|
|
446
|
+
mangled_name = name
|
|
447
|
+
|
|
448
|
+
unless NAME_COMPILABLE_REGEXP.match?(name)
|
|
449
|
+
mangled_name = :"__temp__#{name.unpack1("h*")}"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
mangled_name
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
|
|
453
456
|
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
|
|
454
|
-
body = if CALL_COMPILABLE_REGEXP.match?(
|
|
455
|
-
"self.#{
|
|
457
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
|
|
458
|
+
"self.#{target_name}(#{call_args.join(", ")})"
|
|
456
459
|
else
|
|
457
|
-
call_args.unshift(":'#{
|
|
460
|
+
call_args.unshift(":'#{target_name}'")
|
|
458
461
|
"send(#{call_args.join(", ")})"
|
|
459
462
|
end
|
|
460
463
|
|
|
461
|
-
modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
|
|
462
|
-
|
|
463
464
|
batch <<
|
|
464
|
-
"
|
|
465
|
+
"def #{mangled_name}(#{parameters || ''})" <<
|
|
465
466
|
body <<
|
|
466
467
|
"end"
|
|
467
468
|
end
|
|
@@ -475,7 +476,7 @@ module ActiveModel
|
|
|
475
476
|
def initialize(prefix: "", suffix: "", parameters: nil)
|
|
476
477
|
@prefix = prefix
|
|
477
478
|
@suffix = suffix
|
|
478
|
-
@parameters = parameters.nil? ?
|
|
479
|
+
@parameters = parameters.nil? ? "..." : parameters
|
|
479
480
|
@regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
|
|
480
481
|
@proxy_target = "#{@prefix}attribute#{@suffix}"
|
|
481
482
|
@method_name = "#{prefix}%s#{suffix}"
|
|
@@ -503,24 +504,22 @@ module ActiveModel
|
|
|
503
504
|
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
|
504
505
|
# class belonging to the +clients+ table with a +master_id+ foreign key
|
|
505
506
|
# can instantiate master through <tt>Client#master</tt>.
|
|
506
|
-
def method_missing(method,
|
|
507
|
+
def method_missing(method, ...)
|
|
507
508
|
if respond_to_without_attributes?(method, true)
|
|
508
509
|
super
|
|
509
510
|
else
|
|
510
|
-
match = matched_attribute_method(method.
|
|
511
|
-
match ? attribute_missing(match,
|
|
511
|
+
match = matched_attribute_method(method.name)
|
|
512
|
+
match ? attribute_missing(match, ...) : super
|
|
512
513
|
end
|
|
513
514
|
end
|
|
514
|
-
ruby2_keywords(:method_missing)
|
|
515
515
|
|
|
516
516
|
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
|
517
517
|
# +method_missing+ is called we check to see if there is a matching
|
|
518
518
|
# attribute method. If so, we tell +attribute_missing+ to dispatch the
|
|
519
519
|
# attribute. This method can be overloaded to customize the behavior.
|
|
520
|
-
def attribute_missing(match,
|
|
521
|
-
__send__(match.proxy_target, match.attr_name,
|
|
520
|
+
def attribute_missing(match, ...)
|
|
521
|
+
__send__(match.proxy_target, match.attr_name, ...)
|
|
522
522
|
end
|
|
523
|
-
ruby2_keywords(:attribute_missing)
|
|
524
523
|
|
|
525
524
|
# A +Person+ instance with a +name+ attribute can ask
|
|
526
525
|
# <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
|
|
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/duplicable"
|
|
|
5
5
|
|
|
6
6
|
module ActiveModel
|
|
7
7
|
class AttributeMutationTracker # :nodoc:
|
|
8
|
-
OPTION_NOT_GIVEN = Object.new
|
|
8
|
+
OPTION_NOT_GIVEN = Object.new.freeze
|
|
9
9
|
|
|
10
10
|
def initialize(attributes)
|
|
11
11
|
@attributes = attributes
|
|
@@ -16,17 +16,17 @@ module ActiveModel
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def changed_values
|
|
19
|
-
attr_names.each_with_object(
|
|
19
|
+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
|
|
20
20
|
if changed?(attr_name)
|
|
21
|
-
result
|
|
21
|
+
result.store(attr_name, original_value(attr_name), convert_value: false)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def changes
|
|
27
|
-
attr_names.each_with_object(
|
|
27
|
+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
|
|
28
28
|
if change = change_to_attribute(attr_name)
|
|
29
|
-
result.
|
|
29
|
+
result.store(attr_name, change, convert_value: false)
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -10,17 +10,28 @@ module ActiveModel
|
|
|
10
10
|
|
|
11
11
|
module ClassMethods # :nodoc:
|
|
12
12
|
def attribute(name, type = nil, default: (no_default = true), **options)
|
|
13
|
+
name = resolve_attribute_name(name)
|
|
13
14
|
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
|
|
15
|
+
type = hook_attribute_type(name, type) if type
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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)
|
|
18
27
|
|
|
19
28
|
reset_default_attributes
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def _default_attributes # :nodoc:
|
|
23
|
-
@default_attributes ||=
|
|
32
|
+
@default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
|
|
33
|
+
apply_pending_attribute_modifications(attribute_set)
|
|
34
|
+
end
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
def attribute_types # :nodoc:
|
|
@@ -29,40 +40,62 @@ module ActiveModel
|
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
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
|
+
|
|
32
53
|
private
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
attribute
|
|
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)
|
|
40
64
|
end
|
|
41
65
|
end
|
|
42
66
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
75
|
end
|
|
47
76
|
|
|
48
|
-
def
|
|
49
|
-
|
|
77
|
+
def pending_attribute_modifications
|
|
78
|
+
@pending_attribute_modifications ||= []
|
|
79
|
+
end
|
|
50
80
|
|
|
51
|
-
|
|
52
|
-
|
|
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)
|
|
53
84
|
end
|
|
54
85
|
|
|
55
|
-
|
|
86
|
+
pending_attribute_modifications.each do |modification|
|
|
87
|
+
modification.apply_to(attribute_set)
|
|
88
|
+
end
|
|
56
89
|
end
|
|
57
90
|
|
|
58
|
-
def
|
|
59
|
-
|
|
91
|
+
def reset_default_attributes
|
|
92
|
+
reset_default_attributes!
|
|
93
|
+
subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
|
|
60
94
|
end
|
|
61
95
|
|
|
62
|
-
def reset_default_attributes
|
|
96
|
+
def reset_default_attributes!
|
|
63
97
|
@default_attributes = nil
|
|
64
98
|
@attribute_types = nil
|
|
65
|
-
subclasses.each { |subclass| subclass.send(__method__) }
|
|
66
99
|
end
|
|
67
100
|
|
|
68
101
|
def resolve_attribute_name(name)
|
|
@@ -72,6 +105,13 @@ module ActiveModel
|
|
|
72
105
|
def resolve_type_name(name, **options)
|
|
73
106
|
Type.lookup(name, **options)
|
|
74
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
|
|
75
115
|
end
|
|
76
116
|
end
|
|
77
117
|
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveModel
|
|
4
|
+
module Attributes
|
|
5
|
+
module Normalization
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
include ActiveModel::Dirty
|
|
10
|
+
include ActiveModel::Validations::Callbacks
|
|
11
|
+
|
|
12
|
+
class_attribute :normalized_attributes, default: Set.new
|
|
13
|
+
|
|
14
|
+
before_validation :normalize_changed_in_place_attributes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Normalizes a specified attribute using its declared normalizations.
|
|
18
|
+
#
|
|
19
|
+
# ==== Examples
|
|
20
|
+
#
|
|
21
|
+
# class User
|
|
22
|
+
# include ActiveModel::Attributes
|
|
23
|
+
# include ActiveModel::Attributes::Normalization
|
|
24
|
+
#
|
|
25
|
+
# attribute :email, :string
|
|
26
|
+
#
|
|
27
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# legacy_user = User.load_from_legacy_data(...)
|
|
31
|
+
# legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
|
|
32
|
+
# legacy_user.normalize_attribute(:email)
|
|
33
|
+
# legacy_user.email # => "cruise-control@example.com"
|
|
34
|
+
#
|
|
35
|
+
# ==== Behavior with Active Record
|
|
36
|
+
#
|
|
37
|
+
# To prevent confusion, normalization will not be applied
|
|
38
|
+
# when the attribute is fetched from the database. This means that if a
|
|
39
|
+
# record was persisted before the normalization was declared, the record's
|
|
40
|
+
# attribute will not be normalized until either it is assigned a new
|
|
41
|
+
# value, or it is explicitly migrated via Normalization#normalize_attribute.
|
|
42
|
+
#
|
|
43
|
+
# Be aware that if your app was created before Rails 7.1, and your app
|
|
44
|
+
# marshals instances of the targeted model (for example, when caching),
|
|
45
|
+
# then you should set ActiveRecord.marshalling_format_version to +7.1+ or
|
|
46
|
+
# higher via either <tt>config.load_defaults 7.1</tt> or
|
|
47
|
+
# <tt>config.active_record.marshalling_format_version = 7.1</tt>.
|
|
48
|
+
# Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
|
|
49
|
+
# and raise +TypeError+.
|
|
50
|
+
#
|
|
51
|
+
# class User < ActiveRecord::Base
|
|
52
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
|
53
|
+
# normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
|
|
57
|
+
# user.email # => "cruise-control@example.com"
|
|
58
|
+
#
|
|
59
|
+
# user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
|
|
60
|
+
# user.email # => "cruise-control@example.com"
|
|
61
|
+
# user.email_before_type_cast # => "cruise-control@example.com"
|
|
62
|
+
#
|
|
63
|
+
# User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
|
|
64
|
+
# User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
|
|
65
|
+
#
|
|
66
|
+
# User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
|
|
67
|
+
# User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
|
|
68
|
+
#
|
|
69
|
+
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
|
|
70
|
+
def normalize_attribute(name)
|
|
71
|
+
# Treat the value as a new, unnormalized value.
|
|
72
|
+
send(:"#{name}=", send(name))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module ClassMethods
|
|
76
|
+
# Declares a normalization for one or more attributes. The normalization
|
|
77
|
+
# is applied when the attribute is assigned or validated.
|
|
78
|
+
#
|
|
79
|
+
# Because the normalization may be applied multiple times, it should be
|
|
80
|
+
# _idempotent_. In other words, applying the normalization more than once
|
|
81
|
+
# should have the same result as applying it only once.
|
|
82
|
+
#
|
|
83
|
+
# By default, the normalization will not be applied to +nil+ values. This
|
|
84
|
+
# behavior can be changed with the +:apply_to_nil+ option.
|
|
85
|
+
#
|
|
86
|
+
# ==== Options
|
|
87
|
+
#
|
|
88
|
+
# * +:with+ - Any callable object that accepts the attribute's value as
|
|
89
|
+
# its sole argument, and returns it normalized.
|
|
90
|
+
# * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
|
|
91
|
+
# Defaults to +false+.
|
|
92
|
+
#
|
|
93
|
+
# ==== Examples
|
|
94
|
+
#
|
|
95
|
+
# class User
|
|
96
|
+
# include ActiveModel::Attributes
|
|
97
|
+
# include ActiveModel::Attributes::Normalization
|
|
98
|
+
#
|
|
99
|
+
# attribute :email, :string
|
|
100
|
+
# attribute :phone, :string
|
|
101
|
+
#
|
|
102
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
|
103
|
+
# normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# user = User.new
|
|
107
|
+
# user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
|
|
108
|
+
# user.email # => "cruise-control@example.com"
|
|
109
|
+
#
|
|
110
|
+
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
|
|
111
|
+
def normalizes(*names, with:, apply_to_nil: false)
|
|
112
|
+
decorate_attributes(names) do |name, cast_type|
|
|
113
|
+
NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
self.normalized_attributes += names.map(&:to_sym)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Normalizes a given +value+ using normalizations declared for +name+.
|
|
120
|
+
#
|
|
121
|
+
# ==== Examples
|
|
122
|
+
#
|
|
123
|
+
# class User
|
|
124
|
+
# include ActiveModel::Attributes
|
|
125
|
+
# include ActiveModel::Attributes::Normalization
|
|
126
|
+
#
|
|
127
|
+
# attribute :email, :string
|
|
128
|
+
#
|
|
129
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
# User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
|
|
133
|
+
# # => "cruise-control@example.com"
|
|
134
|
+
def normalize_value_for(name, value)
|
|
135
|
+
type_for_attribute(name).cast(value)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
def normalize_changed_in_place_attributes
|
|
141
|
+
self.class.normalized_attributes.each do |name|
|
|
142
|
+
normalize_attribute(name) if attribute_changed_in_place?(name)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
|
|
147
|
+
include ActiveModel::Type::SerializeCastValue
|
|
148
|
+
|
|
149
|
+
attr_reader :cast_type, :normalizer, :normalize_nil
|
|
150
|
+
alias :normalize_nil? :normalize_nil
|
|
151
|
+
|
|
152
|
+
def initialize(cast_type:, normalizer:, normalize_nil:)
|
|
153
|
+
@cast_type = cast_type
|
|
154
|
+
@normalizer = normalizer
|
|
155
|
+
@normalize_nil = normalize_nil
|
|
156
|
+
super(cast_type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def cast(value)
|
|
160
|
+
normalize(super(value))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def serialize(value)
|
|
164
|
+
serialize_cast_value(cast(value))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def serialize_cast_value(value)
|
|
168
|
+
ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def ==(other)
|
|
172
|
+
self.class == other.class &&
|
|
173
|
+
normalize_nil? == other.normalize_nil? &&
|
|
174
|
+
normalizer == other.normalizer &&
|
|
175
|
+
cast_type == other.cast_type
|
|
176
|
+
end
|
|
177
|
+
alias eql? ==
|
|
178
|
+
|
|
179
|
+
def hash
|
|
180
|
+
[self.class, cast_type, normalizer, normalize_nil?].hash
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
define_method(:inspect, Kernel.instance_method(:inspect))
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
def normalize(value)
|
|
187
|
+
normalizer.call(value) unless value.nil? && !normalize_nil?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -75,6 +75,19 @@ module ActiveModel
|
|
|
75
75
|
attribute_types.keys
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
##
|
|
79
|
+
# :method: type_for_attribute
|
|
80
|
+
# :call-seq: type_for_attribute(attribute_name, &block)
|
|
81
|
+
#
|
|
82
|
+
# Returns the type of the specified attribute after applying any
|
|
83
|
+
# modifiers. This method is the only valid source of information for
|
|
84
|
+
# anything related to the types of a model's attributes. The return value
|
|
85
|
+
# of this method will implement the interface described by
|
|
86
|
+
# ActiveModel::Type::Value (though the object itself may not subclass it).
|
|
87
|
+
#--
|
|
88
|
+
# Implemented by ActiveModel::AttributeRegistration::ClassMethods#type_for_attribute.
|
|
89
|
+
|
|
90
|
+
##
|
|
78
91
|
private
|
|
79
92
|
def define_method_attribute=(canonical_name, owner:, as: canonical_name)
|
|
80
93
|
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
|
|
@@ -60,7 +60,7 @@ module ActiveModel
|
|
|
60
60
|
# Would only create the +after_create+ and +before_create+ callback methods in
|
|
61
61
|
# your class.
|
|
62
62
|
#
|
|
63
|
-
# NOTE:
|
|
63
|
+
# NOTE: Defining the same callback multiple times will overwrite previous callback definitions.
|
|
64
64
|
#
|
|
65
65
|
module Callbacks
|
|
66
66
|
def self.extended(base) # :nodoc:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ActiveModel
|
|
4
4
|
# = Active \Model \Conversion
|
|
5
5
|
#
|
|
6
|
-
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
|
|
6
|
+
# Handles default conversions: #to_model, #to_key, #to_param, and #to_partial_path.
|
|
7
7
|
#
|
|
8
8
|
# Let's take for example this non-persisted object.
|
|
9
9
|
#
|
data/lib/active_model/dirty.rb
CHANGED
|
@@ -108,7 +108,7 @@ module ActiveModel
|
|
|
108
108
|
# person.changes # => {"name" => ["Bill", "Bob"]}
|
|
109
109
|
#
|
|
110
110
|
# If an attribute is modified in-place then make use of
|
|
111
|
-
# {*_will_change!}[rdoc-
|
|
111
|
+
# {*_will_change!}[rdoc-ref:#*_will_change!] to mark that the attribute is changing.
|
|
112
112
|
# Otherwise \Active \Model can't track changes to in-place attributes. Note
|
|
113
113
|
# that Active Record can detect in-place modifications automatically. You do
|
|
114
114
|
# not need to call <tt>*_will_change!</tt> on Active Record models.
|
|
@@ -247,16 +247,23 @@ module ActiveModel
|
|
|
247
247
|
|
|
248
248
|
def initialize_dup(other) # :nodoc:
|
|
249
249
|
super
|
|
250
|
+
@mutations_from_database = nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def init_attributes(other) # :nodoc:
|
|
254
|
+
attrs = super
|
|
250
255
|
if self.class.respond_to?(:_default_attributes)
|
|
251
|
-
|
|
252
|
-
attr.with_value_from_user(
|
|
256
|
+
self.class._default_attributes.map do |attr|
|
|
257
|
+
attr.with_value_from_user(attrs.fetch_value(attr.name))
|
|
253
258
|
end
|
|
259
|
+
else
|
|
260
|
+
attrs
|
|
254
261
|
end
|
|
255
|
-
@mutations_from_database = nil
|
|
256
262
|
end
|
|
257
263
|
|
|
258
264
|
def as_json(options = {}) # :nodoc:
|
|
259
|
-
|
|
265
|
+
except = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
|
|
266
|
+
options = options.merge except: except
|
|
260
267
|
super(options)
|
|
261
268
|
end
|
|
262
269
|
|
|
@@ -289,22 +296,22 @@ module ActiveModel
|
|
|
289
296
|
mutations_from_database.changed_attribute_names
|
|
290
297
|
end
|
|
291
298
|
|
|
292
|
-
# Dispatch target for {*_changed?}[rdoc-
|
|
299
|
+
# Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
|
|
293
300
|
def attribute_changed?(attr_name, **options)
|
|
294
301
|
mutations_from_database.changed?(attr_name.to_s, **options)
|
|
295
302
|
end
|
|
296
303
|
|
|
297
|
-
# Dispatch target for {*_was}[rdoc-
|
|
304
|
+
# Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
|
|
298
305
|
def attribute_was(attr_name)
|
|
299
306
|
mutations_from_database.original_value(attr_name.to_s)
|
|
300
307
|
end
|
|
301
308
|
|
|
302
|
-
# Dispatch target for {*_previously_changed?}[rdoc-
|
|
309
|
+
# Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
|
|
303
310
|
def attribute_previously_changed?(attr_name, **options)
|
|
304
311
|
mutations_before_last_save.changed?(attr_name.to_s, **options)
|
|
305
312
|
end
|
|
306
313
|
|
|
307
|
-
# Dispatch target for {*_previously_was}[rdoc-
|
|
314
|
+
# Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
|
|
308
315
|
def attribute_previously_was(attr_name)
|
|
309
316
|
mutations_before_last_save.original_value(attr_name.to_s)
|
|
310
317
|
end
|
data/lib/active_model/error.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/core_ext/class/attribute"
|
|
4
3
|
|
|
5
4
|
module ActiveModel
|
|
6
5
|
# = Active \Model \Error
|
|
@@ -83,7 +82,7 @@ module ActiveModel
|
|
|
83
82
|
defaults << :"#{i18n_scope}.errors.messages.#{type}"
|
|
84
83
|
|
|
85
84
|
catch(:exception) do
|
|
86
|
-
translation = I18n.translate(defaults.first, **options
|
|
85
|
+
translation = I18n.translate(defaults.first, **options, default: defaults.drop(1), throw: true)
|
|
87
86
|
return translation unless translation.nil?
|
|
88
87
|
end unless options[:message]
|
|
89
88
|
else
|
|
@@ -205,4 +204,6 @@ module ActiveModel
|
|
|
205
204
|
[@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
|
|
206
205
|
end
|
|
207
206
|
end
|
|
207
|
+
|
|
208
|
+
ActiveSupport.run_load_hooks(:active_model_error, Error)
|
|
208
209
|
end
|