hanami-utils/lib/hanami/utils/hash.rb

300 lines
8.0 KiB
Ruby

require 'hanami/utils/duplicable'
module Hanami
module Utils
# Hash on steroids
# @since 0.1.0
class Hash
# @since 0.6.0
# @api private
#
# @see Hanami::Utils::Hash#deep_dup
# @see Hanami::Utils::Duplicable
DUPLICATE_LOGIC = Proc.new do |value|
case value
when Hash
value.deep_dup
when ::Hash
Hash.new(value).deep_dup.to_h
end
end.freeze
# Initialize the hash
#
# @param hash [#to_h] the value we want to use to initialize this instance
# @param blk [Proc] define the default value
#
# @return [Hanami::Utils::Hash] self
#
# @since 0.1.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-c-5B-5D
#
# @example Passing a Hash
# require 'hanami/utils/hash'
#
# hash = Hanami::Utils::Hash.new('l' => 23)
# hash['l'] # => 23
#
# @example Passing a block for default
# require 'hanami/utils/hash'
#
# hash = Hanami::Utils::Hash.new {|h,k| h[k] = [] }
# hash['foo'].push 'bar'
#
# hash.to_h # => { 'foo' => ['bar'] }
def initialize(hash = {}, &blk)
@hash = hash.to_h
@hash.default_proc = blk
end
# Convert in-place all the keys to Symbol instances, nested hashes are converted too.
#
# @return [Hash] self
#
# @since 0.1.0
#
# @example
# require 'hanami/utils/hash'
#
# hash = Hanami::Utils::Hash.new 'a' => 23, 'b' => { 'c' => ['x','y','z'] }
# hash.symbolize!
#
# hash.keys # => [:a, :b]
# hash.inspect # => {:a=>23, :b=>{:c=>["x", "y", "z"]}}
def symbolize!
keys.each do |k|
v = delete(k)
v = Hash.new(v).symbolize! if v.is_a?(::Hash)
self[k.to_sym] = v
end
self
end
# Convert in-place all the keys to Symbol instances, nested hashes are converted too.
#
# @return [Hash] self
#
# @since 0.3.2
#
# @example
# require 'hanami/utils/hash'
#
# hash = Hanami::Utils::Hash.new a: 23, b: { c: ['x','y','z'] }
# hash.stringify!
#
# hash.keys # => [:a, :b]
# hash.inspect # => {"a"=>23, "b"=>{"c"=>["x", "y", "z"]}}
def stringify!
keys.each do |k|
v = delete(k)
v = Hash.new(v).stringify! if v.is_a?(::Hash)
self[k.to_s] = v
end
self
end
# Return a deep copy of the current Hanami::Utils::Hash
#
# @return [Hash] a deep duplicated self
#
# @since 0.3.1
#
# @example
# require 'hanami/utils/hash'
#
# hash = Hanami::Utils::Hash.new(
# 'nil' => nil,
# 'false' => false,
# 'true' => true,
# 'symbol' => :foo,
# 'fixnum' => 23,
# 'bignum' => 13289301283 ** 2,
# 'float' => 1.0,
# 'complex' => Complex(0.3),
# 'bigdecimal' => BigDecimal.new('12.0001'),
# 'rational' => Rational(0.3),
# 'string' => 'foo bar',
# 'hash' => { a: 1, b: 'two', c: :three },
# 'u_hash' => Hanami::Utils::Hash.new({ a: 1, b: 'two', c: :three })
# )
#
# duped = hash.deep_dup
#
# hash.class # => Hanami::Utils::Hash
# duped.class # => Hanami::Utils::Hash
#
# hash.object_id # => 70147385937100
# duped.object_id # => 70147385950620
#
# # unduplicated values
# duped['nil'] # => nil
# duped['false'] # => false
# duped['true'] # => true
# duped['symbol'] # => :foo
# duped['fixnum'] # => 23
# duped['bignum'] # => 176605528590345446089
# duped['float'] # => 1.0
# duped['complex'] # => (0.3+0i)
# duped['bigdecimal'] # => #<BigDecimal:7f9ffe6e2fd0,'0.120001E2',18(18)>
# duped['rational'] # => 5404319552844595/18014398509481984)
#
# # it duplicates values
# duped['string'].reverse!
# duped['string'] # => "rab oof"
# hash['string'] # => "foo bar"
#
# # it deeply duplicates Hash, by preserving the class
# duped['hash'].class # => Hash
# duped['hash'].delete(:a)
# hash['hash'][:a] # => 1
#
# duped['hash'][:b].upcase!
# duped['hash'][:b] # => "TWO"
# hash['hash'][:b] # => "two"
#
# # it deeply duplicates Hanami::Utils::Hash, by preserving the class
# duped['u_hash'].class # => Hanami::Utils::Hash
def deep_dup
Hash.new.tap do |result|
@hash.each {|k, v| result[k] = Duplicable.dup(v, &DUPLICATE_LOGIC) }
end
end
# Returns a new array populated with the keys from this hash
#
# @return [Array] the keys
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-keys
def keys
@hash.keys
end
# Deletes the key-value pair and returns the value from hsh whose key is
# equal to key.
#
# @param key [Object] the key to remove
#
# @return [Object,nil] the value hold by the given key, if present
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-keys
def delete(key)
@hash.delete(key)
end
# Retrieves the value object corresponding to the key object.
#
# @param key [Object] the key
#
# @return [Object,nil] the correspoding value, if present
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-5B-5D
def [](key)
@hash[key]
end
# Associates the value given by value with the key given by key.
#
# @param key [Object] the key to assign
# @param value [Object] the value to assign
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-5B-5D-3D
def []=(key, value)
@hash[key] = value
end
# Returns a Ruby Hash as duplicated version of self
#
# @return [::Hash] the hash
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-to_h
def to_h
@hash.each_with_object({}) do |(k, v), result|
v = v.to_h if v.is_a?(self.class)
result[k] = v
end
end
alias_method :to_hash, :to_h
# Converts into a nested array of [ key, value ] arrays.
#
# @return [::Array] the array
#
# @since 0.3.0
#
# @see http://www.ruby-doc.org/core/Hash.html#method-i-to_a
def to_a
@hash.to_a
end
# Equality
#
# @return [TrueClass,FalseClass]
#
# @since 0.3.0
def ==(other)
@hash == other.to_h
end
alias_method :eql?, :==
# Returns the hash of the internal @hash
#
# @return [Fixnum]
#
# @since 0.3.0
def hash
@hash.hash
end
# Returns a string describing the internal @hash
#
# @return [String]
#
# @since 0.3.0
def inspect
@hash.inspect
end
# Override Ruby's method_missing in order to provide ::Hash interface
#
# @api private
# @since 0.3.0
#
# @raise [NoMethodError] If doesn't respond to the given method
def method_missing(m, *args, &blk)
if respond_to?(m)
h = @hash.__send__(m, *args, &blk)
h = self.class.new(h) if h.is_a?(::Hash)
h
else
raise NoMethodError.new(%(undefined method `#{ m }' for #{ @hash }:#{ self.class }))
end
end
# Override Ruby's respond_to_missing? in order to support ::Hash interface
#
# @api private
# @since 0.3.0
def respond_to_missing?(m, include_private=false)
@hash.respond_to?(m, include_private)
end
end
end
end