class Hash
# Returns a new hash with all keys converted using the block operation.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.transform_keys{ |key| key.to_s.upcase }
# # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
result = {}
each_key do |key|
result[yield(key)] = self[key]
end
result
end
# Destructively convert all keys using the block operations.
# Same as transform_keys but modifies +self+.
def transform_keys!
keys.each do |key|
self[yield(key)] = delete(key)
end
self
end
# Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
# # => { "name" => "Rob", "age" => "28" }
def stringify_keys
transform_keys{ |key| key.to_s }
end
# Destructively convert all keys to strings. Same as
# +stringify_keys+, but modifies +self+.
def stringify_keys!
transform_keys!{ |key| key.to_s }
end
# Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+.
#
# hash = { 'name' => 'Rob', 'age' => '28' }
#
# hash.symbolize_keys
# # => { name: "Rob", age: "28" }
def symbolize_keys
transform_keys{ |key| key.to_sym rescue key }
end
alias_method :to_options, :symbolize_keys
# Destructively convert all keys to symbols, as long as they respond
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
def symbolize_keys!
transform_keys!{ |key| key.to_sym rescue key }
end
alias_method :to_options!, :symbolize_keys!
# Validate all keys in a hash match *valid_keys, raising ArgumentError
# on a mismatch. Note that keys are NOT treated indifferently, meaning if you
# use strings for keys but assert symbols as keys, this will fail.
#
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
def assert_valid_keys(*valid_keys)
valid_keys.flatten!
each_key do |k|
unless valid_keys.include?(k)
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
end
end
end
# Returns a new hash with all keys converted by the block operation.
# This includes the keys from the root hash and from all
# nested hashes.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
def deep_transform_keys(&block)
result = {}
each do |key, value|
result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
end
result
end
# Destructively convert all keys by using the block operation.
# This includes the keys from the root hash and from all
# nested hashes.
def deep_transform_keys!(&block)
keys.each do |key|
value = delete(key)
self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
end
self
end
# Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
# nested hashes.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
def deep_stringify_keys
deep_transform_keys{ |key| key.to_s }
end
# Destructively convert all keys to strings.
# This includes the keys from the root hash and from all
# nested hashes.
def deep_stringify_keys!
deep_transform_keys!{ |key| key.to_s }
end
# Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+. This includes the keys from the root hash
# and from all nested hashes.
#
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
# hash.deep_symbolize_keys
# # => {:person=>{:name=>"Rob", :age=>"28"}}
def deep_symbolize_keys
deep_transform_keys{ |key| key.to_sym rescue key }
end
# Destructively convert all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
# nested hashes.
def deep_symbolize_keys!
deep_transform_keys!{ |key| key.to_sym rescue key }
end
end
module ActiveSupport
# Implements a hash where keys :foo and "foo" are considered
# to be the same.
#
# rgb = ActiveSupport::HashWithIndifferentAccess.new
#
# rgb[:black] = '#000000'
# rgb[:black] # => '#000000'
# rgb['black'] # => '#000000'
#
# rgb['white'] = '#FFFFFF'
# rgb[:white] # => '#FFFFFF'
# rgb['white'] # => '#FFFFFF'
#
# Internally symbols are mapped to strings when used as keys in the entire
# writing interface (calling []=, merge, etc). This
# mapping belongs to the public interface. For example, given:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
#
# You are guaranteed that the key is returned as a string:
#
# hash.keys # => ["a"]
#
# Technically other types of keys are accepted:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
# hash[0] = 0
# hash # => {"a"=>1, 0=>0}
#
# but this class is intended for use cases where strings or symbols are the
# expected keys and it is convenient to understand both as the same. For
# example the +params+ hash in Ruby on Rails.
#
# Note that core extensions define Hash#with_indifferent_access:
#
# rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
#
# which may be handy.
class HashWithIndifferentAccess < Hash
# Returns +true+ so that Array#extract_options! finds members of
# this class.
def extractable_options?
true
end
def with_indifferent_access
dup
end
def nested_under_indifferent_access
self
end
def initialize(constructor = {})
if constructor.is_a?(Hash)
super()
update(constructor)
else
super(constructor)
end
end
def default(key = nil)
if key.is_a?(Symbol) && include?(key = key.to_s)
self[key]
else
super
end
end
def self.new_from_hash_copying_default(hash)
hash = hash.to_hash
new(hash).tap do |new_hash|
new_hash.default = hash.default
end
end
def self.[](*args)
new.merge!(Hash[*args])
end
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
alias_method :regular_update, :update unless method_defined?(:regular_update)
# Assigns a new value to the hash:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:key] = 'value'
#
# This value can be later fetched using either +:key+ or +'key'+.
def []=(key, value)
regular_writer(convert_key(key), convert_value(value, for: :assignment))
end
alias_method :store, :[]=
# Updates the receiver in-place, merging in the hash passed as argument:
#
# hash_1 = ActiveSupport::HashWithIndifferentAccess.new
# hash_1[:key] = 'value'
#
# hash_2 = ActiveSupport::HashWithIndifferentAccess.new
# hash_2[:key] = 'New Value!'
#
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
#
# The argument can be either an
# ActiveSupport::HashWithIndifferentAccess or a regular +Hash+.
# In either case the merge respects the semantics of indifferent access.
#
# If the argument is a regular hash with keys +:key+ and +"key"+ only one
# of the values end up in the receiver, but which one is unspecified.
#
# When given a block, the value for duplicated keys will be determined
# by the result of invoking the block with the duplicated key, the value
# in the receiver, and the value in +other_hash+. The rules for duplicated
# keys follow the semantics of indifferent access:
#
# hash_1[:key] = 10
# hash_2['key'] = 12
# hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
def update(other_hash)
if other_hash.is_a? HashWithIndifferentAccess
super(other_hash)
else
other_hash.to_hash.each_pair do |key, value|
if block_given? && key?(key)
value = yield(convert_key(key), self[key], value)
end
regular_writer(convert_key(key), convert_value(value))
end
self
end
end
alias_method :merge!, :update
# Checks the hash for a key matching the argument passed in:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash['key'] = 'value'
# hash.key?(:key) # => true
# hash.key?('key') # => true
def key?(key)
super(convert_key(key))
end
alias_method :include?, :key?
alias_method :has_key?, :key?
alias_method :member?, :key?
# Same as Hash#fetch where the key passed as argument can be
# either a string or a symbol:
#
# counters = ActiveSupport::HashWithIndifferentAccess.new
# counters[:foo] = 1
#
# counters.fetch('foo') # => 1
# counters.fetch(:bar, 0) # => 0
# counters.fetch(:bar) { |key| 0 } # => 0
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
def fetch(key, *extras)
super(convert_key(key), *extras)
end
# Returns an array of the values at the specified indices:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:a] = 'x'
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
def values_at(*indices)
indices.collect { |key| self[convert_key(key)] }
end
# Returns an exact copy of the hash.
def dup
self.class.new(self).tap do |new_hash|
new_hash.default = default
end
end
# This method has the same semantics of +update+, except it does not
# modify the receiver but rather returns a new hash with indifferent
# access with the result of the merge.
def merge(hash, &block)
self.dup.update(hash, &block)
end
# Like +merge+ but the other way around: Merges the receiver into the
# argument and returns a new hash with indifferent access as result:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash['a'] = nil
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
def reverse_merge(other_hash)
super(self.class.new_from_hash_copying_default(other_hash))
end
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
def reverse_merge!(other_hash)
replace(reverse_merge( other_hash ))
end
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
def replace(other_hash)
super(self.class.new_from_hash_copying_default(other_hash))
end
# Removes the specified key from the hash.
def delete(key)
super(convert_key(key))
end
def stringify_keys!; self end
def deep_stringify_keys!; self end
def stringify_keys; dup end
def deep_stringify_keys; dup end
undef :symbolize_keys!
undef :deep_symbolize_keys!
def symbolize_keys; to_hash.symbolize_keys! end
def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
def to_options!; self end
def select(*args, &block)
dup.tap { |hash| hash.select!(*args, &block) }
end
def reject(*args, &block)
dup.tap { |hash| hash.reject!(*args, &block) }
end
# Convert to a regular hash with string keys.
def to_hash
_new_hash= {}
each do |key, value|
_new_hash[convert_key(key)] = convert_value(value, for: :to_hash)
end
Hash.new(default).merge!(_new_hash)
end
protected
def convert_key(key)
key.kind_of?(Symbol) ? key.to_s : key
end
def convert_value(value, options = {})
if value.is_a? Hash
if options[:for] == :to_hash
value.to_hash
else
value.nested_under_indifferent_access
end
elsif value.is_a?(Array)
unless options[:for] == :assignment
value = value.dup
end
value.map! { |e| convert_value(e, options) }
else
value
end
end
end
end
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess