/
indifferent_hash.rb
209 lines (169 loc) · 5.22 KB
/
indifferent_hash.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# frozen_string_literal: true
if !Hash.method_defined?(:slice) && !$LOAD_PATH.grep(%r{gems/activesupport}).empty?
if ENV['APP_ENV'] == 'test' && File.basename(File.dirname(ENV['BUNDLE_GEMFILE'])) == 'sinatra'
# Some extensions get loaded during internal testing (e.g. by RABL and our
# RABL test) that we have no control over, but we need them to load
# *before* IndifferentHash, so we'll do it preemptively here.
#
# Newer Rubies have these methods built-in, so the extensions are no-ops.
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/keys'
elsif ENV['SINATRA_ACTIVESUPPORT_WARNING'] != 'false'
$stderr.puts <<-EOF
WARNING: If you plan to load any of ActiveSupport's core extensions to Hash, be
sure to do so *before* loading Sinatra::Application or Sinatra::Base. If not,
you may disregard this warning.
Set SINATRA_ACTIVESUPPORT_WARNING=false in the environment to hide this warning.
EOF
end
end
module Sinatra
# A poor man's ActiveSupport::HashWithIndifferentAccess, with all the Rails-y
# stuff removed.
#
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> 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. <tt>[]=</tt>, <tt>merge</tt>). 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)
args.map!(&method(:convert_value))
super(*args)
end
def default(*args)
args.map!(&method(:convert_key))
super(*args)
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)
args.map!(&method(:convert_value))
super(convert_key(key), *args)
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)
keys.map!(&method(:convert_key))
super(*keys)
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)
keys.map!(&method(:convert_key))
super(*keys)
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
if method_defined?(:transform_values!) # Added in Ruby 2.4
def transform_values(&block)
dup.transform_values!(&block)
end
def transform_values!
super
super(&method(:convert_value))
end
end
if method_defined?(:transform_keys!) # Added in Ruby 2.5
def transform_keys(&block)
dup.transform_keys!(&block)
end
def transform_keys!
super
super(&method(:convert_key))
end
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