Skip to content

Commit

Permalink
Convert Hash to HashWithIndifferentAccess in ActiveRecord::Store.
Browse files Browse the repository at this point in the history
In order to make migration from 3.x apps easier, we should try to
convert
Hash instances to HashWithIndifferentAccess, to allow accessing values
with both symbol and a string. This is follow up to changes in 3c0bf04.
  • Loading branch information
Andrey Voronkov committed May 22, 2012
1 parent f491c6a commit 940c135
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 10 deletions.
37 changes: 27 additions & 10 deletions activerecord/lib/active_record/store.rb
@@ -1,3 +1,5 @@
require 'active_support/core_ext/hash/indifferent_access'

module ActiveRecord module ActiveRecord
# Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
# It's like a simple key/value store backed into your record when you don't care about being able to # It's like a simple key/value store backed into your record when you don't care about being able to
Expand All @@ -13,18 +15,19 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats. # You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
# #
# String keys should be used for direct access to virtual attributes because of most of the coders do not
# distinguish symbols and strings as keys.
#
# Examples: # Examples:
# #
# class User < ActiveRecord::Base # class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON # store :settings, accessors: [ :color, :homepage ], coder: JSON
# end # end
# #
# u = User.new(color: 'black', homepage: '37signals.com') # u = User.new(color: 'black', homepage: '37signals.com')
# u.color # Accessor stored attribute # u.color # Accessor stored attribute
# u.settings['country'] = 'Denmark' # Any attribute, even if not specified with an accessor # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
# #
# # Add additional accessors to an existing store through store_accessor # # Add additional accessors to an existing store through store_accessor
# class SuperUser < User # class SuperUser < User
Expand All @@ -35,24 +38,38 @@ module Store


module ClassMethods module ClassMethods
def store(store_attribute, options = {}) def store(store_attribute, options = {})
serialize store_attribute, options.fetch(:coder, Hash) serialize store_attribute, options.fetch(:coder, ActiveSupport::HashWithIndifferentAccess)
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end end


def store_accessor(store_attribute, *keys) def store_accessor(store_attribute, *keys)
keys.flatten.each do |key| keys.flatten.each do |key|
define_method("#{key}=") do |value| define_method("#{key}=") do |value|
send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) initialize_store_attribute(store_attribute)
send(store_attribute)[key.to_s] = value send(store_attribute)[key] = value
send("#{store_attribute}_will_change!") send("#{store_attribute}_will_change!")
end end


define_method(key) do define_method(key) do
send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) initialize_store_attribute(store_attribute)
send(store_attribute)[key.to_s] send(store_attribute)[key]
end end
end end
end end
end end

private
def initialize_store_attribute(store_attribute)
case attribute = send(store_attribute)
when ActiveSupport::HashWithIndifferentAccess
# Already initialized. Do nothing.
when Hash
# Initialized as a Hash. Convert to indifferent access.
send :"#{store_attribute}=", attribute.with_indifferent_access
else
# Uninitialized. Set to an indifferent hash.
send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new
end
end
end end
end end
34 changes: 34 additions & 0 deletions activerecord/test/cases/store_test.rb
Expand Up @@ -41,6 +41,40 @@ class StoreTest < ActiveRecord::TestCase
assert_equal false, @john.remember_login assert_equal false, @john.remember_login
end end


test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do
@john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
@john.height = 'low'
assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal 'heavy', @john.json_data[:weight]
assert_equal 'heavy', @john.json_data['weight']
end

test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do
@john.json_data = { :height => 'tall', 'weight' => 'heavy' }
assert_equal true, @john.json_data.instance_of?(Hash)
assert_equal 'tall', @john.json_data[:height]
assert_equal nil, @john.json_data['height']
assert_equal nil, @john.json_data[:weight]
assert_equal 'heavy', @john.json_data['weight']
@john.height = 'low'
assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal 'heavy', @john.json_data[:weight]
assert_equal 'heavy', @john.json_data['weight']
end

test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do
@john.json_data = "somedata"
@john.height = 'low'
assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any?
end

test "reading store attributes through accessors encoded with JSON" do test "reading store attributes through accessors encoded with JSON" do
assert_equal 'tall', @john.height assert_equal 'tall', @john.height
assert_nil @john.weight assert_nil @john.weight
Expand Down

0 comments on commit 940c135

Please sign in to comment.