# frozen_string_literal: true module Apiculture # A poor man's ActiveSupport::HashWithIndifferentAccess, with all the Rails-y # stuff removed. # # Implements a hash where keys :foo and "foo" are # considered to be the same. # # rgb = Sinatra::IndifferentHash.new # # rgb[:black] = '#000000' # symbol assignment # rgb[:black] # => '#000000' # symbol retrieval # rgb['black'] # => '#000000' # string retrieval # # rgb['white'] = '#FFFFFF' # string assignment # rgb[:white] # => '#FFFFFF' # symbol retrieval # rgb['white'] # => '#FFFFFF' # string retrieval # # Internally, symbols are mapped to strings when used as keys in the entire # writing interface (calling e.g. []=, merge). This mapping # belongs to the public interface. For example, given: # # hash = Sinatra::IndifferentHash.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 = Sinatra::IndifferentHash.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 Sinatra. class IndifferentHash < Hash def self.[](*args) new.merge!(Hash[*args]) end def initialize(*args) super(*args.map(&method(:convert_value))) end def default(*args) super(*args.map(&method(:convert_key))) end def default=(value) super(convert_value(value)) end def assoc(key) super(convert_key(key)) end def rassoc(value) super(convert_value(value)) end def fetch(key, *args) super(convert_key(key), *args.map(&method(:convert_value))) end def [](key) super(convert_key(key)) end def []=(key, value) super(convert_key(key), convert_value(value)) end alias_method :store, :[]= def key(value) super(convert_value(value)) end def key?(key) super(convert_key(key)) end alias_method :has_key?, :key? alias_method :include?, :key? alias_method :member?, :key? def value?(value) super(convert_value(value)) end alias_method :has_value?, :value? def delete(key) super(convert_key(key)) end def dig(key, *other_keys) super(convert_key(key), *other_keys) end if method_defined?(:dig) # Added in Ruby 2.3 def fetch_values(*keys) super(*keys.map(&method(:convert_key))) end if method_defined?(:fetch_values) # Added in Ruby 2.3 def slice(*keys) keys.map!(&method(:convert_key)) self.class[super(*keys)] end if method_defined?(:slice) # Added in Ruby 2.5 def values_at(*keys) super(*keys.map(&method(:convert_key))) end def merge!(other_hash) return super if other_hash.is_a?(self.class) other_hash.each_pair do |key, value| key = convert_key(key) value = yield(key, self[key], value) if block_given? && key?(key) self[key] = convert_value(value) end self end alias_method :update, :merge! def merge(other_hash, &block) dup.merge!(other_hash, &block) end def replace(other_hash) super(other_hash.is_a?(self.class) ? other_hash : self.class[other_hash]) end private def convert_key(key) key.is_a?(Symbol) ? key.to_s : key end def convert_value(value) case value when Hash value.is_a?(self.class) ? value : self.class[value] when Array value.map(&method(:convert_value)) else value end end end end