Skip to content

Commit 76d41ee

Browse files
authored
Merge pull request rails#54437 from byroot/hwia-dirty-raw-set
Allow to skip HashWithIndifferentAccess value conversion
2 parents db4215a + 97df37b commit 76d41ee

File tree

5 files changed

+84
-25
lines changed

5 files changed

+84
-25
lines changed

activemodel/lib/active_model/attribute_mutation_tracker.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ def changed_attribute_names
1616
end
1717

1818
def changed_values
19-
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
19+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
2020
if changed?(attr_name)
21-
result[attr_name] = original_value(attr_name)
21+
result.store(attr_name, original_value(attr_name), convert_value: false)
2222
end
2323
end
2424
end
2525

2626
def changes
27-
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
27+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
2828
if change = change_to_attribute(attr_name)
29-
result[attr_name] = change
29+
result.store(attr_name, change, convert_value: false)
3030
end
3131
end
3232
end

activerecord/lib/active_record/store.rb

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -146,37 +146,43 @@ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
146146
define_method("#{accessor_key}_changed?") do
147147
return false unless attribute_changed?(store_attribute)
148148
prev_store, new_store = changes[store_attribute]
149-
prev_store&.dig(key) != new_store&.dig(key)
149+
accessor = store_accessor_for(store_attribute)
150+
accessor.get(prev_store, key) != accessor.get(new_store, key)
150151
end
151152

152153
define_method("#{accessor_key}_change") do
153154
return unless attribute_changed?(store_attribute)
154155
prev_store, new_store = changes[store_attribute]
155-
[prev_store&.dig(key), new_store&.dig(key)]
156+
accessor = store_accessor_for(store_attribute)
157+
[accessor.get(prev_store, key), accessor.get(new_store, key)]
156158
end
157159

158160
define_method("#{accessor_key}_was") do
159161
return unless attribute_changed?(store_attribute)
160162
prev_store, _new_store = changes[store_attribute]
161-
prev_store&.dig(key)
163+
accessor = store_accessor_for(store_attribute)
164+
accessor.get(prev_store, key)
162165
end
163166

164167
define_method("saved_change_to_#{accessor_key}?") do
165168
return false unless saved_change_to_attribute?(store_attribute)
166169
prev_store, new_store = saved_changes[store_attribute]
167-
prev_store&.dig(key) != new_store&.dig(key)
170+
accessor = store_accessor_for(store_attribute)
171+
accessor.get(prev_store, key) != accessor.get(new_store, key)
168172
end
169173

170174
define_method("saved_change_to_#{accessor_key}") do
171175
return unless saved_change_to_attribute?(store_attribute)
172176
prev_store, new_store = saved_changes[store_attribute]
173-
[prev_store&.dig(key), new_store&.dig(key)]
177+
accessor = store_accessor_for(store_attribute)
178+
[accessor.get(prev_store, key), accessor.get(new_store, key)]
174179
end
175180

176181
define_method("#{accessor_key}_before_last_save") do
177182
return unless saved_change_to_attribute?(store_attribute)
178183
prev_store, _new_store = saved_changes[store_attribute]
179-
prev_store&.dig(key)
184+
accessor = store_accessor_for(store_attribute)
185+
accessor.get(prev_store, key)
180186
end
181187
end
182188
end
@@ -225,39 +231,58 @@ def store_accessor_for(store_attribute)
225231
end
226232

227233
class HashAccessor # :nodoc:
234+
def self.get(store_object, key)
235+
if store_object
236+
store_object[key]
237+
end
238+
end
239+
228240
def self.read(object, attribute, key)
229-
prepare(object, attribute)
230-
object.public_send(attribute)[key]
241+
store_object = prepare(object, attribute)
242+
store_object[key]
231243
end
232244

233245
def self.write(object, attribute, key, value)
234-
prepare(object, attribute)
235-
object.public_send(attribute)[key] = value if value != read(object, attribute, key)
246+
store_object = prepare(object, attribute)
247+
store_object[key] = value if value != store_object[key]
236248
end
237249

238250
def self.prepare(object, attribute)
239-
object.public_send :"#{attribute}=", {} unless object.send(attribute)
251+
store_object = object.public_send(attribute)
252+
253+
if store_object.nil?
254+
store_object = {}
255+
object.public_send(:"#{attribute}=", store_object)
256+
end
257+
258+
store_object
240259
end
241260
end
242261

243262
class StringKeyedHashAccessor < HashAccessor # :nodoc:
263+
def self.get(store_object, key)
264+
super store_object, Symbol === key ? key.name : key.to_s
265+
end
266+
244267
def self.read(object, attribute, key)
245-
super object, attribute, key.to_s
268+
super object, attribute, Symbol === key ? key.name : key.to_s
246269
end
247270

248271
def self.write(object, attribute, key, value)
249-
super object, attribute, key.to_s, value
272+
super object, attribute, Symbol === key ? key.name : key.to_s, value
250273
end
251274
end
252275

253276
class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
254-
def self.prepare(object, store_attribute)
255-
attribute = object.send(store_attribute)
256-
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
257-
attribute = IndifferentCoder.as_indifferent_hash(attribute)
258-
object.public_send :"#{store_attribute}=", attribute
277+
def self.prepare(object, attribute)
278+
store_object = object.public_send(attribute)
279+
280+
unless store_object.is_a?(ActiveSupport::HashWithIndifferentAccess)
281+
store_object = IndifferentCoder.as_indifferent_hash(store_object)
282+
object.public_send :"#{attribute}=", store_object
259283
end
260-
attribute
284+
285+
store_object
261286
end
262287
end
263288

activerecord/test/cases/store_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ class StoreTest < ActiveRecord::TestCase
3131
assert_equal "37signals.com", @john.homepage
3232
end
3333

34+
test "reading store attributes with indifferent access" do
35+
@john.preferences = { "remember_login" => "yes" }
36+
assert_equal "yes", @john.remember_login
37+
38+
@john.preferences = { remember_login: "no" }
39+
assert_equal "no", @john.remember_login
40+
end
41+
3442
test "writing store attributes does not update unchanged value" do
3543
admin_user = Admin::User.new(homepage: nil)
3644
admin_user.homepage = nil

activerecord/test/models/admin/user.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,28 @@ def load(s)
1616
end
1717

1818
belongs_to :account
19+
1920
store :params, accessors: [ :token ], coder: YAML
21+
2022
store :settings, accessors: [ :color, :homepage ]
2123
store_accessor :settings, :favorite_food
24+
2225
store :parent, accessors: [:birthday, :name], prefix: true
26+
2327
store :spouse, accessors: [:birthday], prefix: :partner
2428
store_accessor :spouse, :name, prefix: :partner
29+
2530
store :configs, accessors: [ :secret_question ]
2631
store :configs, accessors: [ :two_factor_auth ], suffix: true
2732
store_accessor :configs, :login_retry, suffix: :config
28-
store :preferences, accessors: [ :remember_login ]
33+
34+
serialize :preferences, type: Hash, coder: Coder.new
35+
store_accessor :preferences, :remember_login
36+
2937
store :json_data, accessors: [ :height, :weight ], coder: Coder.new
38+
3039
store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new
40+
3141
store_accessor :json_options, :enable_friend_requests
3242

3343
def phone_number

activesupport/lib/active_support/hash_with_indifferent_access.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,27 @@ def self.[](*args)
9595
# hash[:key] = 'value'
9696
#
9797
# This value can be later fetched using either +:key+ or <tt>'key'</tt>.
98+
#
99+
# If the value is a Hash or contains one or multiple Hashes, they will be
100+
# converted to +HashWithIndifferentAccess+.
98101
def []=(key, value)
99102
regular_writer(convert_key(key), convert_value(value, conversion: :assignment))
100103
end
101104

102-
alias_method :store, :[]=
105+
# Assigns a new value to the hash:
106+
#
107+
# hash = ActiveSupport::HashWithIndifferentAccess.new
108+
# hash[:key] = 'value'
109+
#
110+
# This value can be later fetched using either +:key+ or <tt>'key'</tt>.
111+
#
112+
# If the value is a Hash or contains one or multiple Hashes, they will be
113+
# converted to +HashWithIndifferentAccess+. unless `convert_value: false`
114+
# is set.
115+
def store(key, value, convert_value: true)
116+
value = convert_value(value, conversion: :assignment) if convert_value
117+
regular_writer(convert_key(key), value)
118+
end
103119

104120
# Updates the receiver in-place, merging in the hashes passed as arguments:
105121
#

0 commit comments

Comments
 (0)