activemodel 6.0.0.beta3 → 6.0.2.rc2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b034b3f54d2c82df8caa74bf97be978e102611c91075d49329e353ff317d187
4
- data.tar.gz: 6eedb13c89d8899c1f10584716a9c91d91628c8e7d7762bf2137f12f0d6a9165
3
+ metadata.gz: f53fd7a85f2c82e65474f83bfea395162728cb37c5a2b0e54a66558a192b31ef
4
+ data.tar.gz: d066c69bfc2aa4d87b599ab00a79f53cd052814b174f0f8777592e0e6859c85a
5
5
  SHA512:
6
- metadata.gz: 56c12136b80022c6fc9e0b2594ba87146b1f0b6a312ad4e389fec4069955edac0aed51c64fd58e91539dd53f74dd393bfcf35ca6fb42ba97f7492696da92dbff
7
- data.tar.gz: 2135d0a5bd6787134bb1bffd1602ec78ea45ac5d73109de2d6af625d8d3d56b2bef111abcf0cd96ef8a6f44d8ffcf68154bbfab5328b293c8e40a3e84e5341fd
6
+ metadata.gz: b35e989bdf9139fab9fac79123771c2338cc5919f3f096b58b2918b74b79a60fb86cff8c634666f76b95ef59e21f71210feec3b483376218b08348a487d37789
7
+ data.tar.gz: 1b524687a3256eca1b51d21f68fc28e79cf8e8583e17d51057667b97d504d64cd27a9de093b473985577717ab24de9bc469db2b411aaa385bc3bde76d33fee49
@@ -1,3 +1,55 @@
1
+ ## Rails 6.0.2.rc2 (December 09, 2019) ##
2
+
3
+ * No changes.
4
+
5
+
6
+ ## Rails 6.0.1 (November 5, 2019) ##
7
+
8
+ * No changes.
9
+
10
+
11
+ ## Rails 6.0.0 (August 16, 2019) ##
12
+
13
+ * No changes.
14
+
15
+
16
+ ## Rails 6.0.0.rc2 (July 22, 2019) ##
17
+
18
+ * No changes.
19
+
20
+
21
+ ## Rails 6.0.0.rc1 (April 24, 2019) ##
22
+
23
+ * Type cast falsy boolean symbols on boolean attribute as false.
24
+
25
+ Fixes #35676.
26
+
27
+ *Ryuta Kamizono*
28
+
29
+ * Change how validation error translation strings are fetched: The new behavior
30
+ will first try the more specific keys, including doing locale fallback, then try
31
+ the less specific ones.
32
+
33
+ For example, this is the order in which keys will now be tried for a `blank`
34
+ error on a `product`'s `title` attribute with current locale set to `en-US`:
35
+
36
+ en-US.activerecord.errors.models.product.attributes.title.blank
37
+ en-US.activerecord.errors.models.product.blank
38
+ en-US.activerecord.errors.messages.blank
39
+
40
+ en.activerecord.errors.models.product.attributes.title.blank
41
+ en.activerecord.errors.models.product.blank
42
+ en.activerecord.errors.messages.blank
43
+
44
+ en-US.errors.attributes.title.blank
45
+ en-US.errors.messages.blank
46
+
47
+ en.errors.attributes.title.blank
48
+ en.errors.messages.blank
49
+
50
+ *Hugo Vacher*
51
+
52
+
1
53
  ## Rails 6.0.0.beta3 (March 11, 2019) ##
2
54
 
3
55
  * No changes.
@@ -11,12 +63,12 @@
11
63
  Before:
12
64
 
13
65
  Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
14
- => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
66
+ # => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
15
67
 
16
68
  After:
17
69
 
18
70
  Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
19
- => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
71
+ # => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
20
72
 
21
73
  Fixes #28521.
22
74
 
@@ -52,6 +104,16 @@
52
104
 
53
105
  ## Rails 6.0.0.beta1 (January 18, 2019) ##
54
106
 
107
+ * Internal calls to `human_attribute_name` on an `Active Model` now pass attributes as strings instead of symbols
108
+ in some cases.
109
+
110
+ This is in line with examples in Rails docs and puts the code in line with the intention -
111
+ the potential use of strings or symbols.
112
+
113
+ It is recommended to cast the attribute input to your desired type as if you you are overriding that methid.
114
+
115
+ *Martin Larochelle*
116
+
55
117
  * Add `ActiveModel::Errors#of_kind?`.
56
118
 
57
119
  *bogdanvlviv*, *Rafael Mendonça França*
@@ -106,7 +168,7 @@
106
168
 
107
169
  *Unathi Chonco*
108
170
 
109
- * Add `config.active_model.i18n_full_message` in order to control whether
171
+ * Add `config.active_model.i18n_customize_full_message` in order to control whether
110
172
  the `full_message` error format can be overridden at the attribute or model
111
173
  level in the locale files. This is `false` by default.
112
174
 
@@ -5,6 +5,8 @@ They allow for Action Pack helpers to interact with non-Active Record models,
5
5
  for example. Active Model also helps with building custom ORMs for use outside of
6
6
  the Rails framework.
7
7
 
8
+ You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide.
9
+
8
10
  Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
9
11
  interact with Action Pack helpers, it was required to either copy chunks of
10
12
  code from Rails, or monkey patch entire helpers to make them handle objects
@@ -253,7 +255,7 @@ Active Model is released under the MIT license:
253
255
 
254
256
  API documentation is at:
255
257
 
256
- * http://api.rubyonrails.org
258
+ * https://api.rubyonrails.org
257
259
 
258
260
  Bug reports for the Ruby on Rails project can be filed here:
259
261
 
@@ -286,12 +286,12 @@ module ActiveModel
286
286
  method_name = matcher.method_name(attr_name)
287
287
 
288
288
  unless instance_method_already_implemented?(method_name)
289
- generate_method = "define_method_#{matcher.method_missing_target}"
289
+ generate_method = "define_method_#{matcher.target}"
290
290
 
291
291
  if respond_to?(generate_method, true)
292
292
  send(generate_method, attr_name.to_s)
293
293
  else
294
- define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
294
+ define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s
295
295
  end
296
296
  end
297
297
  end
@@ -352,17 +352,18 @@ module ActiveModel
352
352
 
353
353
  def attribute_method_matchers_matching(method_name)
354
354
  attribute_method_matchers_cache.compute_if_absent(method_name) do
355
- # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
356
- # will match every time.
355
+ # Bump plain matcher to last place so that only methods that do not
356
+ # match any other pattern match the actual attribute name.
357
+ # This is currently only needed to support legacy usage.
357
358
  matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
358
- matchers.map { |method| method.match(method_name) }.compact
359
+ matchers.map { |matcher| matcher.match(method_name) }.compact
359
360
  end
360
361
  end
361
362
 
362
363
  # Define a method `name` in `mod` that dispatches to `send`
363
364
  # using the given `extra` args. This falls back on `define_method`
364
365
  # and `send` if the given names cannot be compiled.
365
- def define_proxy_call(include_private, mod, name, send, *extra)
366
+ def define_proxy_call(include_private, mod, name, target, *extra)
366
367
  defn = if NAME_COMPILABLE_REGEXP.match?(name)
367
368
  "def #{name}(*args)"
368
369
  else
@@ -371,34 +372,34 @@ module ActiveModel
371
372
 
372
373
  extra = (extra.map!(&:inspect) << "*args").join(", ")
373
374
 
374
- target = if CALL_COMPILABLE_REGEXP.match?(send)
375
- "#{"self." unless include_private}#{send}(#{extra})"
375
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
376
+ "#{"self." unless include_private}#{target}(#{extra})"
376
377
  else
377
- "send(:'#{send}', #{extra})"
378
+ "send(:'#{target}', #{extra})"
378
379
  end
379
380
 
380
381
  mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
381
382
  #{defn}
382
- #{target}
383
+ #{body}
383
384
  end
384
385
  RUBY
385
386
  end
386
387
 
387
388
  class AttributeMethodMatcher #:nodoc:
388
- attr_reader :prefix, :suffix, :method_missing_target
389
+ attr_reader :prefix, :suffix, :target
389
390
 
390
- AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
391
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
391
392
 
392
393
  def initialize(options = {})
393
394
  @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
394
395
  @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
395
- @method_missing_target = "#{@prefix}attribute#{@suffix}"
396
+ @target = "#{@prefix}attribute#{@suffix}"
396
397
  @method_name = "#{prefix}%s#{suffix}"
397
398
  end
398
399
 
399
400
  def match(method_name)
400
401
  if @regex =~ method_name
401
- AttributeMethodMatch.new(method_missing_target, $1, method_name)
402
+ AttributeMethodMatch.new(target, $1)
402
403
  end
403
404
  end
404
405
 
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash/indifferent_access"
4
+ require "active_support/core_ext/object/duplicable"
4
5
 
5
6
  module ActiveModel
6
7
  class AttributeMutationTracker # :nodoc:
7
8
  OPTION_NOT_GIVEN = Object.new
8
9
 
9
- def initialize(attributes)
10
+ def initialize(attributes, forced_changes = Set.new)
10
11
  @attributes = attributes
11
- @forced_changes = Set.new
12
+ @forced_changes = forced_changes
12
13
  end
13
14
 
14
15
  def changed_attribute_names
@@ -18,24 +19,22 @@ module ActiveModel
18
19
  def changed_values
19
20
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
20
21
  if changed?(attr_name)
21
- result[attr_name] = attributes[attr_name].original_value
22
+ result[attr_name] = original_value(attr_name)
22
23
  end
23
24
  end
24
25
  end
25
26
 
26
27
  def changes
27
28
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
28
- change = change_to_attribute(attr_name)
29
- if change
29
+ if change = change_to_attribute(attr_name)
30
30
  result.merge!(attr_name => change)
31
31
  end
32
32
  end
33
33
  end
34
34
 
35
35
  def change_to_attribute(attr_name)
36
- attr_name = attr_name.to_s
37
36
  if changed?(attr_name)
38
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
37
+ [original_value(attr_name), fetch_value(attr_name)]
39
38
  end
40
39
  end
41
40
 
@@ -44,29 +43,26 @@ module ActiveModel
44
43
  end
45
44
 
46
45
  def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
47
- attr_name = attr_name.to_s
48
- forced_changes.include?(attr_name) ||
49
- attributes[attr_name].changed? &&
50
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
51
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
46
+ attribute_changed?(attr_name) &&
47
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
48
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
52
49
  end
53
50
 
54
51
  def changed_in_place?(attr_name)
55
- attributes[attr_name.to_s].changed_in_place?
52
+ attributes[attr_name].changed_in_place?
56
53
  end
57
54
 
58
55
  def forget_change(attr_name)
59
- attr_name = attr_name.to_s
60
56
  attributes[attr_name] = attributes[attr_name].forgetting_assignment
61
57
  forced_changes.delete(attr_name)
62
58
  end
63
59
 
64
60
  def original_value(attr_name)
65
- attributes[attr_name.to_s].original_value
61
+ attributes[attr_name].original_value
66
62
  end
67
63
 
68
64
  def force_change(attr_name)
69
- forced_changes << attr_name.to_s
65
+ forced_changes << attr_name
70
66
  end
71
67
 
72
68
  private
@@ -75,45 +71,108 @@ module ActiveModel
75
71
  def attr_names
76
72
  attributes.keys
77
73
  end
74
+
75
+ def attribute_changed?(attr_name)
76
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
77
+ end
78
+
79
+ def fetch_value(attr_name)
80
+ attributes.fetch_value(attr_name)
81
+ end
82
+ end
83
+
84
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
85
+ def initialize(attributes, forced_changes = {})
86
+ super
87
+ @finalized_changes = nil
88
+ end
89
+
90
+ def changed_in_place?(attr_name)
91
+ false
92
+ end
93
+
94
+ def change_to_attribute(attr_name)
95
+ if finalized_changes&.include?(attr_name)
96
+ finalized_changes[attr_name].dup
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ def forget_change(attr_name)
103
+ forced_changes.delete(attr_name)
104
+ end
105
+
106
+ def original_value(attr_name)
107
+ if changed?(attr_name)
108
+ forced_changes[attr_name]
109
+ else
110
+ fetch_value(attr_name)
111
+ end
112
+ end
113
+
114
+ def force_change(attr_name)
115
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
116
+ end
117
+
118
+ def finalize_changes
119
+ @finalized_changes = changes
120
+ end
121
+
122
+ private
123
+ attr_reader :finalized_changes
124
+
125
+ def attr_names
126
+ forced_changes.keys
127
+ end
128
+
129
+ def attribute_changed?(attr_name)
130
+ forced_changes.include?(attr_name)
131
+ end
132
+
133
+ def fetch_value(attr_name)
134
+ attributes.send(:_read_attribute, attr_name)
135
+ end
136
+
137
+ def clone_value(attr_name)
138
+ value = fetch_value(attr_name)
139
+ value.duplicable? ? value.clone : value
140
+ rescue TypeError, NoMethodError
141
+ value
142
+ end
78
143
  end
79
144
 
80
145
  class NullMutationTracker # :nodoc:
81
146
  include Singleton
82
147
 
83
- def changed_attribute_names(*)
148
+ def changed_attribute_names
84
149
  []
85
150
  end
86
151
 
87
- def changed_values(*)
152
+ def changed_values
88
153
  {}
89
154
  end
90
155
 
91
- def changes(*)
156
+ def changes
92
157
  {}
93
158
  end
94
159
 
95
160
  def change_to_attribute(attr_name)
96
161
  end
97
162
 
98
- def any_changes?(*)
163
+ def any_changes?
99
164
  false
100
165
  end
101
166
 
102
- def changed?(*)
167
+ def changed?(attr_name, **)
103
168
  false
104
169
  end
105
170
 
106
- def changed_in_place?(*)
171
+ def changed_in_place?(attr_name)
107
172
  false
108
173
  end
109
174
 
110
- def forget_change(*)
111
- end
112
-
113
- def original_value(*)
114
- end
115
-
116
- def force_change(*)
175
+ def original_value(attr_name)
117
176
  end
118
177
  end
119
178
  end
@@ -26,6 +26,21 @@ module ActiveModel
26
26
  define_attribute_method(name)
27
27
  end
28
28
 
29
+ # Returns an array of attribute names as strings
30
+ #
31
+ # class Person
32
+ # include ActiveModel::Attributes
33
+ #
34
+ # attribute :name, :string
35
+ # attribute :age, :integer
36
+ # end
37
+ #
38
+ # Person.attribute_names
39
+ # # => ["name", "age"]
40
+ def attribute_names
41
+ attribute_types.keys
42
+ end
43
+
29
44
  private
30
45
 
31
46
  def define_method_attribute=(name)
@@ -65,33 +80,57 @@ module ActiveModel
65
80
  super
66
81
  end
67
82
 
83
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
84
+ #
85
+ # class Person
86
+ # include ActiveModel::Model
87
+ # include ActiveModel::Attributes
88
+ #
89
+ # attribute :name, :string
90
+ # attribute :age, :integer
91
+ # end
92
+ #
93
+ # person = Person.new(name: 'Francesco', age: 22)
94
+ # person.attributes
95
+ # # => {"name"=>"Francesco", "age"=>22}
68
96
  def attributes
69
97
  @attributes.to_hash
70
98
  end
71
99
 
100
+ # Returns an array of attribute names as strings
101
+ #
102
+ # class Person
103
+ # include ActiveModel::Attributes
104
+ #
105
+ # attribute :name, :string
106
+ # attribute :age, :integer
107
+ # end
108
+ #
109
+ # person = Person.new
110
+ # person.attribute_names
111
+ # # => ["name", "age"]
112
+ def attribute_names
113
+ @attributes.keys
114
+ end
115
+
72
116
  private
73
117
 
74
118
  def write_attribute(attr_name, value)
75
- name = if self.class.attribute_alias?(attr_name)
76
- self.class.attribute_alias(attr_name).to_s
77
- else
78
- attr_name.to_s
79
- end
119
+ name = attr_name.to_s
120
+ name = self.class.attribute_aliases[name] || name
80
121
 
81
122
  @attributes.write_from_user(name, value)
82
123
  value
83
124
  end
84
125
 
85
126
  def attribute(attr_name)
86
- name = if self.class.attribute_alias?(attr_name)
87
- self.class.attribute_alias(attr_name).to_s
88
- else
89
- attr_name.to_s
90
- end
127
+ name = attr_name.to_s
128
+ name = self.class.attribute_aliases[name] || name
129
+
91
130
  @attributes.fetch_value(name)
92
131
  end
93
132
 
94
- # Handle *= for method_missing.
133
+ # Dispatch target for <tt>*=</tt> attribute methods.
95
134
  def attribute=(attribute_name, value)
96
135
  write_attribute(attribute_name, value)
97
136
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/hash_with_indifferent_access"
4
- require "active_support/core_ext/object/duplicable"
5
3
  require "active_model/attribute_mutation_tracker"
6
4
 
7
5
  module ActiveModel
@@ -122,9 +120,6 @@ module ActiveModel
122
120
  extend ActiveSupport::Concern
123
121
  include ActiveModel::AttributeMethods
124
122
 
125
- OPTION_NOT_GIVEN = Object.new # :nodoc:
126
- private_constant :OPTION_NOT_GIVEN
127
-
128
123
  included do
129
124
  attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
130
125
  attribute_method_suffix "_previously_changed?", "_previous_change"
@@ -145,10 +140,9 @@ module ActiveModel
145
140
  # +mutations_from_database+ to +mutations_before_last_save+ respectively.
146
141
  def changes_applied
147
142
  unless defined?(@attributes)
148
- @previously_changed = changes
143
+ mutations_from_database.finalize_changes
149
144
  end
150
145
  @mutations_before_last_save = mutations_from_database
151
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
152
146
  forget_attribute_assignments
153
147
  @mutations_from_database = nil
154
148
  end
@@ -159,7 +153,7 @@ module ActiveModel
159
153
  # person.name = 'bob'
160
154
  # person.changed? # => true
161
155
  def changed?
162
- changed_attributes.present?
156
+ mutations_from_database.any_changes?
163
157
  end
164
158
 
165
159
  # Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +162,37 @@ module ActiveModel
168
162
  # person.name = 'bob'
169
163
  # person.changed # => ["name"]
170
164
  def changed
171
- changed_attributes.keys
165
+ mutations_from_database.changed_attribute_names
172
166
  end
173
167
 
174
- # Handles <tt>*_changed?</tt> for +method_missing+.
175
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
176
- !!changes_include?(attr) &&
177
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
178
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
168
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
169
+ def attribute_changed?(attr_name, **options) # :nodoc:
170
+ mutations_from_database.changed?(attr_name.to_s, options)
179
171
  end
180
172
 
181
- # Handles <tt>*_was</tt> for +method_missing+.
182
- def attribute_was(attr) # :nodoc:
183
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
173
+ # Dispatch target for <tt>*_was</tt> attribute methods.
174
+ def attribute_was(attr_name) # :nodoc:
175
+ mutations_from_database.original_value(attr_name.to_s)
184
176
  end
185
177
 
186
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
187
- def attribute_previously_changed?(attr) #:nodoc:
188
- previous_changes_include?(attr)
178
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
179
+ def attribute_previously_changed?(attr_name) # :nodoc:
180
+ mutations_before_last_save.changed?(attr_name.to_s)
189
181
  end
190
182
 
191
183
  # Restore all previous data of the provided attributes.
192
- def restore_attributes(attributes = changed)
193
- attributes.each { |attr| restore_attribute! attr }
184
+ def restore_attributes(attr_names = changed)
185
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
194
186
  end
195
187
 
196
188
  # Clears all dirty data: current changes and previous changes.
197
189
  def clear_changes_information
198
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
199
190
  @mutations_before_last_save = nil
200
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
201
191
  forget_attribute_assignments
202
192
  @mutations_from_database = nil
203
193
  end
204
194
 
205
195
  def clear_attribute_changes(attr_names)
206
- attributes_changed_by_setter.except!(*attr_names)
207
196
  attr_names.each do |attr_name|
208
197
  clear_attribute_change(attr_name)
209
198
  end
@@ -216,13 +205,7 @@ module ActiveModel
216
205
  # person.name = 'robert'
217
206
  # person.changed_attributes # => {"name" => "bob"}
218
207
  def changed_attributes
219
- # This should only be set by methods which will call changed_attributes
220
- # multiple times when it is known that the computed value cannot change.
221
- if defined?(@cached_changed_attributes)
222
- @cached_changed_attributes
223
- else
224
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
225
- end
208
+ mutations_from_database.changed_values
226
209
  end
227
210
 
228
211
  # Returns a hash of changed attributes indicating their original
@@ -232,9 +215,7 @@ module ActiveModel
232
215
  # person.name = 'bob'
233
216
  # person.changes # => { "name" => ["bill", "bob"] }
234
217
  def changes
235
- cache_changed_attributes do
236
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
237
- end
218
+ mutations_from_database.changes
238
219
  end
239
220
 
240
221
  # Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +225,23 @@ module ActiveModel
244
225
  # person.save
245
226
  # person.previous_changes # => {"name" => ["bob", "robert"]}
246
227
  def previous_changes
247
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
248
- @previously_changed.merge(mutations_before_last_save.changes)
228
+ mutations_before_last_save.changes
249
229
  end
250
230
 
251
231
  def attribute_changed_in_place?(attr_name) # :nodoc:
252
- mutations_from_database.changed_in_place?(attr_name)
232
+ mutations_from_database.changed_in_place?(attr_name.to_s)
253
233
  end
254
234
 
255
235
  private
256
236
  def clear_attribute_change(attr_name)
257
- mutations_from_database.forget_change(attr_name)
237
+ mutations_from_database.forget_change(attr_name.to_s)
258
238
  end
259
239
 
260
240
  def mutations_from_database
261
- unless defined?(@mutations_from_database)
262
- @mutations_from_database = nil
263
- end
264
241
  @mutations_from_database ||= if defined?(@attributes)
265
242
  ActiveModel::AttributeMutationTracker.new(@attributes)
266
243
  else
267
- NullMutationTracker.instance
244
+ ActiveModel::ForcedMutationTracker.new(self)
268
245
  end
269
246
  end
270
247
 
@@ -276,68 +253,28 @@ module ActiveModel
276
253
  @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
277
254
  end
278
255
 
279
- def cache_changed_attributes
280
- @cached_changed_attributes = changed_attributes
281
- yield
282
- ensure
283
- clear_changed_attributes_cache
284
- end
285
-
286
- def clear_changed_attributes_cache
287
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
288
- end
289
-
290
- # Returns +true+ if attr_name is changed, +false+ otherwise.
291
- def changes_include?(attr_name)
292
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
293
- end
294
- alias attribute_changed_by_setter? changes_include?
295
-
296
- # Returns +true+ if attr_name were changed before the model was saved,
297
- # +false+ otherwise.
298
- def previous_changes_include?(attr_name)
299
- previous_changes.include?(attr_name)
300
- end
301
-
302
- # Handles <tt>*_change</tt> for +method_missing+.
303
- def attribute_change(attr)
304
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
256
+ # Dispatch target for <tt>*_change</tt> attribute methods.
257
+ def attribute_change(attr_name)
258
+ mutations_from_database.change_to_attribute(attr_name.to_s)
305
259
  end
306
260
 
307
- # Handles <tt>*_previous_change</tt> for +method_missing+.
308
- def attribute_previous_change(attr)
309
- previous_changes[attr]
261
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
262
+ def attribute_previous_change(attr_name)
263
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
310
264
  end
311
265
 
312
- # Handles <tt>*_will_change!</tt> for +method_missing+.
313
- def attribute_will_change!(attr)
314
- unless attribute_changed?(attr)
315
- begin
316
- value = _read_attribute(attr)
317
- value = value.duplicable? ? value.clone : value
318
- rescue TypeError, NoMethodError
319
- end
320
-
321
- set_attribute_was(attr, value)
322
- end
323
- mutations_from_database.force_change(attr)
266
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
267
+ def attribute_will_change!(attr_name)
268
+ mutations_from_database.force_change(attr_name.to_s)
324
269
  end
325
270
 
326
- # Handles <tt>restore_*!</tt> for +method_missing+.
327
- def restore_attribute!(attr)
328
- if attribute_changed?(attr)
329
- __send__("#{attr}=", changed_attributes[attr])
330
- clear_attribute_changes([attr])
271
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
272
+ def restore_attribute!(attr_name)
273
+ attr_name = attr_name.to_s
274
+ if attribute_changed?(attr_name)
275
+ __send__("#{attr_name}=", attribute_was(attr_name))
276
+ clear_attribute_change(attr_name)
331
277
  end
332
278
  end
333
-
334
- def attributes_changed_by_setter
335
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
336
- end
337
-
338
- # Force an attribute to have a particular "before" value
339
- def set_attribute_was(attr, old_value)
340
- attributes_changed_by_setter[attr] = old_value
341
- end
342
279
  end
343
280
  end
@@ -63,9 +63,9 @@ module ActiveModel
63
63
  MESSAGE_OPTIONS = [:message]
64
64
 
65
65
  class << self
66
- attr_accessor :i18n_full_message # :nodoc:
66
+ attr_accessor :i18n_customize_full_message # :nodoc:
67
67
  end
68
- self.i18n_full_message = false
68
+ self.i18n_customize_full_message = false
69
69
 
70
70
  attr_reader :messages, :details
71
71
 
@@ -413,7 +413,7 @@ module ActiveModel
413
413
  return message if attribute == :base
414
414
  attribute = attribute.to_s
415
415
 
416
- if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
416
+ if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
417
417
  attribute = attribute.remove(/\[\d\]/)
418
418
  parts = attribute.split(".")
419
419
  attribute_name = parts.pop
@@ -479,6 +479,14 @@ module ActiveModel
479
479
  # * <tt>errors.messages.blank</tt>
480
480
  def generate_message(attribute, type = :invalid, options = {})
481
481
  type = options.delete(:message) if options[:message].is_a?(Symbol)
482
+ value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
483
+
484
+ options = {
485
+ model: @base.model_name.human,
486
+ attribute: @base.class.human_attribute_name(attribute),
487
+ value: value,
488
+ object: @base
489
+ }.merge!(options)
482
490
 
483
491
  if @base.class.respond_to?(:i18n_scope)
484
492
  i18n_scope = @base.class.i18n_scope.to_s
@@ -487,6 +495,11 @@ module ActiveModel
487
495
  :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
488
496
  end
489
497
  defaults << :"#{i18n_scope}.errors.messages.#{type}"
498
+
499
+ catch(:exception) do
500
+ translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
501
+ return translation unless translation.nil?
502
+ end unless options[:message]
490
503
  else
491
504
  defaults = []
492
505
  end
@@ -496,15 +509,7 @@ module ActiveModel
496
509
 
497
510
  key = defaults.shift
498
511
  defaults = options.delete(:message) if options[:message]
499
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
500
-
501
- options = {
502
- default: defaults,
503
- model: @base.model_name.human,
504
- attribute: @base.class.human_attribute_name(attribute),
505
- value: value,
506
- object: @base
507
- }.merge!(options)
512
+ options[:default] = defaults
508
513
 
509
514
  I18n.translate(key, options)
510
515
  end
@@ -9,8 +9,8 @@ module ActiveModel
9
9
  module VERSION
10
10
  MAJOR = 6
11
11
  MINOR = 0
12
- TINY = 0
13
- PRE = "beta3"
12
+ TINY = 2
13
+ PRE = "rc2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -13,8 +13,8 @@ module ActiveModel
13
13
  ActiveModel::SecurePassword.min_cost = Rails.env.test?
14
14
  end
15
15
 
16
- initializer "active_model.i18n_full_message" do
17
- ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
16
+ initializer "active_model.i18n_customize_full_message" do
17
+ ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
18
18
  end
19
19
  end
20
20
  end
@@ -69,6 +69,27 @@ module ActiveModel
69
69
  raise
70
70
  end
71
71
 
72
+ include InstanceMethodsOnActivation.new(attribute)
73
+
74
+ if validations
75
+ include ActiveModel::Validations
76
+
77
+ # This ensures the model has a password by checking whether the password_digest
78
+ # is present, so that this works with both new and existing records. However,
79
+ # when there is an error, the message is added to the password attribute instead
80
+ # so that the error message will make sense to the end-user.
81
+ validate do |record|
82
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
83
+ end
84
+
85
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
86
+ validates_confirmation_of attribute, allow_blank: true
87
+ end
88
+ end
89
+ end
90
+
91
+ class InstanceMethodsOnActivation < Module
92
+ def initialize(attribute)
72
93
  attr_reader attribute
73
94
 
74
95
  define_method("#{attribute}=") do |unencrypted_password|
@@ -101,21 +122,6 @@ module ActiveModel
101
122
  end
102
123
 
103
124
  alias_method :authenticate, :authenticate_password if attribute == :password
104
-
105
- if validations
106
- include ActiveModel::Validations
107
-
108
- # This ensures the model has a password by checking whether the password_digest
109
- # is present, so that this works with both new and existing records. However,
110
- # when there is an error, the message is added to the password attribute instead
111
- # so that the error message will make sense to the end-user.
112
- validate do |record|
113
- record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
114
- end
115
-
116
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
117
- validates_confirmation_of attribute, allow_blank: true
118
- end
119
125
  end
120
126
  end
121
127
  end
@@ -14,7 +14,16 @@ module ActiveModel
14
14
  # - Empty strings are coerced to +nil+
15
15
  # - All other values will be coerced to +true+
16
16
  class Boolean < Value
17
- FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
17
+ FALSE_VALUES = [
18
+ false, 0,
19
+ "0", :"0",
20
+ "f", :f,
21
+ "F", :F,
22
+ "false", :false,
23
+ "FALSE", :FALSE,
24
+ "off", :off,
25
+ "OFF", :OFF,
26
+ ].to_set.freeze
18
27
 
19
28
  def type # :nodoc:
20
29
  :boolean
@@ -22,10 +22,17 @@ module ActiveModel
22
22
  end
23
23
 
24
24
  def apply_seconds_precision(value)
25
- return value unless precision && value.respond_to?(:usec)
26
- number_of_insignificant_digits = 6 - precision
25
+ return value unless precision && value.respond_to?(:nsec)
26
+
27
+ number_of_insignificant_digits = 9 - precision
27
28
  round_power = 10**number_of_insignificant_digits
28
- value.change(usec: value.usec - value.usec % round_power)
29
+ rounded_off_nsec = value.nsec % round_power
30
+
31
+ if rounded_off_nsec > 0
32
+ value.change(nsec: value.nsec - rounded_off_nsec)
33
+ else
34
+ value
35
+ end
29
36
  end
30
37
 
31
38
  def type_cast_for_schema(value)
@@ -18,6 +18,11 @@ module ActiveModel
18
18
  :integer
19
19
  end
20
20
 
21
+ def deserialize(value)
22
+ return if value.blank?
23
+ value.to_i
24
+ end
25
+
21
26
  def serialize(value)
22
27
  return if value.is_a?(::String) && non_numeric_string?(value)
23
28
  ensure_in_range(super)
@@ -17,7 +17,8 @@ module ActiveModel
17
17
  private
18
18
 
19
19
  def setup!(klass)
20
- klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
20
+ define_attributes = LazilyDefineAttributes.new(attributes)
21
+ klass.include(define_attributes) unless klass.included_modules.include?(define_attributes)
21
22
  end
22
23
 
23
24
  def acceptable_option?(value)
@@ -25,46 +26,57 @@ module ActiveModel
25
26
  end
26
27
 
27
28
  class LazilyDefineAttributes < Module
28
- def initialize(attribute_definition)
29
+ def initialize(attributes)
30
+ @attributes = attributes.map(&:to_s)
31
+ end
32
+
33
+ def included(klass)
34
+ @lock = Mutex.new
35
+ mod = self
36
+
29
37
  define_method(:respond_to_missing?) do |method_name, include_private = false|
30
- super(method_name, include_private) || attribute_definition.matches?(method_name)
38
+ mod.define_on(klass)
39
+ super(method_name, include_private) || mod.matches?(method_name)
31
40
  end
32
41
 
33
42
  define_method(:method_missing) do |method_name, *args, &block|
34
- if attribute_definition.matches?(method_name)
35
- attribute_definition.define_on(self.class)
43
+ mod.define_on(klass)
44
+ if mod.matches?(method_name)
36
45
  send(method_name, *args, &block)
37
46
  else
38
47
  super(method_name, *args, &block)
39
48
  end
40
49
  end
41
50
  end
42
- end
43
-
44
- class AttributeDefinition
45
- def initialize(attributes)
46
- @attributes = attributes.map(&:to_s)
47
- end
48
51
 
49
52
  def matches?(method_name)
50
- attr_name = convert_to_reader_name(method_name)
51
- attributes.include?(attr_name)
53
+ attr_name = method_name.to_s.chomp("=")
54
+ attributes.any? { |name| name == attr_name }
52
55
  end
53
56
 
54
57
  def define_on(klass)
55
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
56
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
57
- klass.define_attribute_methods
58
- klass.attr_reader(*attr_readers)
59
- klass.attr_writer(*attr_writers)
60
- end
58
+ @lock&.synchronize do
59
+ return unless @lock
61
60
 
62
- private
63
- attr_reader :attributes
61
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
62
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
63
+
64
+ attr_reader(*attr_readers)
65
+ attr_writer(*attr_writers)
64
66
 
65
- def convert_to_reader_name(method_name)
66
- method_name.to_s.chomp("=")
67
+ remove_method :respond_to_missing?
68
+ remove_method :method_missing
69
+
70
+ @lock = nil
67
71
  end
72
+ end
73
+
74
+ def ==(other)
75
+ self.class == other.class && attributes == other.attributes
76
+ end
77
+
78
+ protected
79
+ attr_reader :attributes
68
80
  end
69
81
  end
70
82
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.beta3
4
+ version: 6.0.2.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-13 00:00:00.000000000 Z
11
+ date: 2019-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.0.beta3
19
+ version: 6.0.2.rc2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.0.beta3
26
+ version: 6.0.2.rc2
27
27
  description: A toolkit for building modeling frameworks like Active Record. Rich support
28
28
  for attributes, callbacks, validations, serialization, internationalization, and
29
29
  testing.
@@ -97,12 +97,15 @@ files:
97
97
  - lib/active_model/validations/with.rb
98
98
  - lib/active_model/validator.rb
99
99
  - lib/active_model/version.rb
100
- homepage: http://rubyonrails.org
100
+ homepage: https://rubyonrails.org
101
101
  licenses:
102
102
  - MIT
103
103
  metadata:
104
- source_code_uri: https://github.com/rails/rails/tree/v6.0.0.beta3/activemodel
105
- changelog_uri: https://github.com/rails/rails/blob/v6.0.0.beta3/activemodel/CHANGELOG.md
104
+ bug_tracker_uri: https://github.com/rails/rails/issues
105
+ changelog_uri: https://github.com/rails/rails/blob/v6.0.2.rc2/activemodel/CHANGELOG.md
106
+ documentation_uri: https://api.rubyonrails.org/v6.0.2.rc2/
107
+ mailing_list_uri: https://groups.google.com/forum/#!forum/rubyonrails-talk
108
+ source_code_uri: https://github.com/rails/rails/tree/v6.0.2.rc2/activemodel
106
109
  post_install_message:
107
110
  rdoc_options: []
108
111
  require_paths:
@@ -118,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
121
  - !ruby/object:Gem::Version
119
122
  version: 1.3.1
120
123
  requirements: []
121
- rubygems_version: 3.0.1
124
+ rubygems_version: 3.0.3
122
125
  signing_key:
123
126
  specification_version: 4
124
127
  summary: A toolkit for building modeling frameworks (part of Rails).