require 'hanami/utils/duplicable' require 'transproc' module Hanami module Utils # Hash on steroids # @since 0.1.0 # # rubocop:disable Metrics/ClassLength class Hash # @since 0.6.0 # @api private # # @see Hanami::Utils::Hash#deep_dup # @see Hanami::Utils::Duplicable DUPLICATE_LOGIC = proc do |value| case value when Hash value.deep_dup when ::Hash Hash.new(value).deep_dup.to_h end end.freeze extend Transproc::Registry import Transproc::HashTransformations # Symbolize the given hash # # @param input [::Hash] the input # # @return [::Hash] the symbolized hash # # @since 1.0.1 # # @see .deep_symbolize # # @example Basic Usage # require 'hanami/utils/hash' # # hash = Hanami::Utils::Hash.symbolize("foo" => "bar", "baz" => {"a" => 1}) # # => {:foo=>"bar", :baz=>{"a"=>1}} # # hash.class # # => Hash def self.symbolize(input) self[:symbolize_keys].call(input) end # Performs deep symbolize on the given hash # # @param input [::Hash] the input # # @return [::Hash] the deep symbolized hash # # @since 1.0.1 # # @see .symbolize # # @example Basic Usage # require 'hanami/utils/hash' # # hash = Hanami::Utils::Hash.deep_symbolize("foo" => "bar", "baz" => {"a" => 1}) # # => {:foo=>"bar", :baz=>{a:=>1}} # # hash.class # # => Hash def self.deep_symbolize(input) self[:deep_symbolize_keys].call(input) end # Stringifies the given hash # # @param input [::Hash] the input # # @return [::Hash] the stringified hash # # @since 1.0.1 # # @example Basic Usage # require 'hanami/utils/hash' # # hash = Hanami::Utils::Hash.stringify(foo: "bar", baz: {a: 1}) # # => {"foo"=>"bar", "baz"=>{:a=>1}} # # hash.class # # => Hash def self.stringify(input) self[:stringify_keys].call(input) end # Deeply stringifies the given hash # # @param input [::Hash] the input # # @return [::Hash] the deep stringified hash # # @since 1.1.1 # # @example Basic Usage # require "hanami/utils/hash" # # hash = Hanami::Utils::Hash.deep_stringify(foo: "bar", baz: {a: 1}) # # => {"foo"=>"bar", "baz"=>{"a"=>1}} # # hash.class # # => Hash def self.deep_stringify(input) # rubocop:disable Metrics/MethodLength input.each_with_object({}) do |(key, value), output| output[key.to_s] = case value when ::Hash deep_stringify(value) when Array value.map do |item| item.is_a?(::Hash) ? deep_stringify(item) : item end else value end end end # Deep duplicates hash values # # The output of this function is a deep duplicate of the input. # Any further modification on the input, won't be reflected on the output # and viceversa. # # @param input [::Hash] the input # # @return [::Hash] the deep duplicate of input # # @since 1.0.1 # # @example Basic Usage # require 'hanami/utils/hash' # # input = { "a" => { "b" => { "c" => [1, 2, 3] } } } # output = Hanami::Utils::Hash.deep_dup(input) # # => {"a"=>{"b"=>{"c"=>[1,2,3]}}} # # output.class # # => Hash # # # # # mutations on input aren't reflected on output # # input["a"]["b"]["c"] << 4 # output.dig("a", "b", "c") # # => [1, 2, 3] # # # # # mutations on output aren't reflected on input # # output["a"].delete("b") # input # # => {"a"=>{"b"=>{"c"=>[1,2,3,4]}}} def self.deep_dup(input) input.each_with_object({}) do |(k, v), result| result[k] = case v when ::Hash deep_dup(v) else Duplicable.dup(v) end end end # Deep serializes given object into a `Hash` # # Please note that the returning `Hash` will use symbols as keys. # # @param input [#to_hash] the input # # @return [::Hash] the deep serialized hash # # @since 1.1.0 # # @example Basic Usage # require 'hanami/utils/hash' # require 'ostruct' # # class Data < OpenStruct # def to_hash # to_h # end # end # # input = Data.new("foo" => "bar", baz => [Data.new(hello: "world")]) # # Hanami::Utils::Hash.deep_serialize(input) # # => {:foo=>"bar", :baz=>[{:hello=>"world"}]} def self.deep_serialize(input) # rubocop:disable Metrics/MethodLength input.to_hash.each_with_object({}) do |(key, value), output| output[key.to_sym] = case value when ->(h) { h.respond_to?(:to_hash) } deep_serialize(value) when Array value.map do |item| item.respond_to?(:to_hash) ? deep_serialize(item) : item end else value end end end # 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 # @deprecated # # @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_hash @hash.default_proc = blk if blk end # Converts in-place all the keys to Symbol instances. # # @return [Hash] self # # @since 0.1.0 # @deprecated Use {Hanami::Utils::Hash.symbolize} # # @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) self[k.to_sym] = v end self end # Converts in-place all the keys to Symbol instances, nested hashes are converted too. # # @return [Hash] self # # @since 1.0.0 # @deprecated Use {Hanami::Utils::Hash.deep_symbolize} # # @example # require 'hanami/utils/hash' # # hash = Hanami::Utils::Hash.new 'a' => 23, 'b' => { 'c' => ['x','y','z'] } # hash.deep_symbolize! # # hash.keys # => [:a, :b] # hash.inspect # => {:a=>23, :b=>{:c=>["x", "y", "z"]}} def deep_symbolize! keys.each do |k| v = delete(k) v = self.class.new(v).deep_symbolize! if v.respond_to?(:to_hash) self[k.to_sym] = v end self end # Converts in-place all the keys to Symbol instances, nested hashes are converted too. # # @return [Hash] self # # @since 0.3.2 # @deprecated Use {Hanami::Utils::Hash.stringify} # # @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 = self.class.new(v).stringify! if v.respond_to?(:to_hash) self[k.to_s] = v end self end # Returns a deep copy of the current Hanami::Utils::Hash # # @return [Hash] a deep duplicated self # # @since 0.3.1 # @deprecated Use {Hanami::Utils::Hash.deep_dup} # # @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('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'] # => # # 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 self.class.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 # @deprecated # # @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 # @deprecated # # @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 # @deprecated # # @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 # @deprecated # # @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 # @deprecated # # @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.respond_to?(:to_hash) result[k] = v end end alias to_hash to_h # Converts into a nested array of [ key, value ] arrays. # # @return [::Array] the array # # @since 0.3.0 # @deprecated # # @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 # @deprecated def ==(other) @hash == other.to_h end alias eql? == # Returns the hash of the internal @hash # # @return [Fixnum] # # @since 0.3.0 # @deprecated def hash @hash.hash end # Returns a string describing the internal @hash # # @return [String] # # @since 0.3.0 # @deprecated def inspect @hash.inspect end # Overrides 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(method_name, *args, &blk) raise NoMethodError.new(%(undefined method `#{method_name}' for #{@hash}:#{self.class})) unless respond_to?(method_name) h = @hash.__send__(method_name, *args, &blk) h = self.class.new(h) if h.is_a?(::Hash) h end # Overrides Ruby's respond_to_missing? in order to support ::Hash interface # # @api private # @since 0.3.0 def respond_to_missing?(method_name, include_private = false) @hash.respond_to?(method_name, include_private) end end # rubocop:enable Metrics/ClassLength end end