hashie 4.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -53
- data/CONTRIBUTING.md +38 -1
- data/LICENSE +1 -1
- data/README.md +920 -205
- data/Rakefile +32 -1
- data/UPGRADING.md +56 -0
- data/hashie.gemspec +10 -5
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +160 -32
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -26
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
- data/lib/hashie/extensions/dash/property_translation.rb +138 -15
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +16 -29
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
- data/lib/hashie/extensions/indifferent_access.rb +124 -5
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +94 -8
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +84 -17
- data/lib/hashie/hash.rb +18 -2
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +314 -82
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +46 -0
- metadata +22 -9
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# IndifferentAccess gives you the ability to not care
|
|
@@ -23,6 +25,13 @@ module Hashie
|
|
|
23
25
|
# h['baz'] # => 'blip'
|
|
24
26
|
#
|
|
25
27
|
module IndifferentAccess
|
|
28
|
+
# @api private
|
|
29
|
+
def self.convert_key(key)
|
|
30
|
+
return key.name if key.is_a?(Symbol)
|
|
31
|
+
|
|
32
|
+
key.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
def self.included(base)
|
|
27
36
|
Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)
|
|
28
37
|
|
|
@@ -54,6 +63,12 @@ module Hashie
|
|
|
54
63
|
# This will inject indifferent access into an instance of
|
|
55
64
|
# a hash without modifying the actual class. This is what
|
|
56
65
|
# allows IndifferentAccess to spread to sub-hashes.
|
|
66
|
+
#
|
|
67
|
+
# @example Convert an existing hash in place
|
|
68
|
+
# hash = { user: { name: 'Ada' } }
|
|
69
|
+
# Hashie::Extensions::IndifferentAccess.inject!(hash)
|
|
70
|
+
# hash['user'][:name] # => 'Ada'
|
|
71
|
+
# hash.class # => Hash
|
|
57
72
|
def self.inject!(hash)
|
|
58
73
|
(class << hash; self; end).send :include, IndifferentAccess
|
|
59
74
|
hash.convert!
|
|
@@ -61,24 +76,44 @@ module Hashie
|
|
|
61
76
|
|
|
62
77
|
# Injects indifferent access into a duplicate of the hash
|
|
63
78
|
# provided. See #inject!
|
|
79
|
+
#
|
|
80
|
+
# @example Leave the original hash's top-level keys unchanged
|
|
81
|
+
# original = { name: 'Ada' }
|
|
82
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject(original)
|
|
83
|
+
# hash['name'] # => 'Ada'
|
|
84
|
+
# original # => { name: 'Ada' }
|
|
64
85
|
def self.inject(hash)
|
|
65
86
|
inject!(hash.dup)
|
|
66
87
|
end
|
|
67
88
|
|
|
89
|
+
# @example Normalize symbol keys
|
|
90
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
91
|
+
# hash.convert_key(:name) # => 'name'
|
|
68
92
|
def convert_key(key)
|
|
69
|
-
key
|
|
93
|
+
IndifferentAccess.convert_key(key)
|
|
70
94
|
end
|
|
71
95
|
|
|
72
96
|
# Iterates through the keys and values, reconverting them to
|
|
73
97
|
# their proper indifferent state. Used when IndifferentAccess
|
|
74
98
|
# is injecting itself into member hashes.
|
|
99
|
+
#
|
|
100
|
+
# @example Normalize keys inserted through the original writer
|
|
101
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
102
|
+
# hash.regular_writer(:name, 'Ada')
|
|
103
|
+
# hash.convert!
|
|
104
|
+
# hash.keys # => ['name']
|
|
105
|
+
# hash[:name] # => 'Ada'
|
|
75
106
|
def convert!
|
|
76
107
|
keys.each do |k| # rubocop:disable Performance/HashEachMethods
|
|
77
|
-
|
|
108
|
+
indifferent_writer k, regular_delete(k)
|
|
78
109
|
end
|
|
79
110
|
self
|
|
80
111
|
end
|
|
81
112
|
|
|
113
|
+
# @example Make hashes nested in arrays indifferent
|
|
114
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
115
|
+
# value = hash.indifferent_value([{ name: 'Ada' }])
|
|
116
|
+
# value.first['name'] # => 'Ada'
|
|
82
117
|
def indifferent_value(value)
|
|
83
118
|
if hash_lacking_indifference?(value)
|
|
84
119
|
IndifferentAccess.inject!(value)
|
|
@@ -89,58 +124,142 @@ module Hashie
|
|
|
89
124
|
end
|
|
90
125
|
end
|
|
91
126
|
|
|
127
|
+
# @example Return an existing value or the configured default
|
|
128
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
129
|
+
# hash.default = 'unknown'
|
|
130
|
+
# hash.indifferent_default(:name) # => 'Ada'
|
|
131
|
+
# hash.indifferent_default(:missing) # => 'unknown'
|
|
92
132
|
def indifferent_default(key = nil)
|
|
93
133
|
return self[convert_key(key)] if key?(key)
|
|
134
|
+
|
|
94
135
|
regular_default(key)
|
|
95
136
|
end
|
|
96
137
|
|
|
138
|
+
# @example Normalize incoming keys and nested values
|
|
139
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
140
|
+
# hash.indifferent_update(user: { name: 'Ada' })
|
|
141
|
+
# hash['user']['name'] # => 'Ada'
|
|
97
142
|
def indifferent_update(other_hash)
|
|
98
143
|
return regular_update(other_hash) if hash_with_indifference?(other_hash)
|
|
144
|
+
|
|
99
145
|
other_hash.each_pair do |k, v|
|
|
100
146
|
self[k] = v
|
|
101
147
|
end
|
|
102
148
|
end
|
|
103
149
|
|
|
150
|
+
# @example Store a symbol key as a string
|
|
151
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
152
|
+
# hash.indifferent_writer(:name, 'Ada')
|
|
153
|
+
# hash # => { 'name' => 'Ada' }
|
|
104
154
|
def indifferent_writer(key, value)
|
|
105
155
|
regular_writer convert_key(key), indifferent_value(value)
|
|
106
156
|
end
|
|
107
157
|
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
# @example Fetch with either key type and support fallbacks
|
|
159
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
160
|
+
# hash.indifferent_fetch(:name) # => 'Ada'
|
|
161
|
+
# hash.indifferent_fetch(:missing, 'unknown') # => 'unknown'
|
|
162
|
+
def indifferent_fetch(key, ...)
|
|
163
|
+
regular_fetch(convert_key(key), ...)
|
|
110
164
|
end
|
|
111
165
|
|
|
166
|
+
# @example Delete a string key through a symbol
|
|
167
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
168
|
+
# hash.indifferent_delete(:name) # => 'Ada'
|
|
169
|
+
# hash # => {}
|
|
112
170
|
def indifferent_delete(key)
|
|
113
171
|
regular_delete convert_key(key)
|
|
114
172
|
end
|
|
115
173
|
|
|
174
|
+
# @example Recognize both key representations
|
|
175
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
176
|
+
# hash.indifferent_key?(:name) # => true
|
|
177
|
+
# hash.indifferent_key?('name') # => true
|
|
116
178
|
def indifferent_key?(key)
|
|
117
179
|
regular_key? convert_key(key)
|
|
118
180
|
end
|
|
119
181
|
|
|
182
|
+
# @example Retrieve values using mixed key types
|
|
183
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
|
|
184
|
+
# hash.indifferent_values_at(:name, 'age', :missing) # => ['Ada', 42, nil]
|
|
120
185
|
def indifferent_values_at(*indices)
|
|
121
186
|
indices.map { |i| self[i] }
|
|
122
187
|
end
|
|
123
188
|
|
|
189
|
+
# @example Identify an indifferent hash
|
|
190
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({})
|
|
191
|
+
# hash.indifferent_access? # => true
|
|
124
192
|
def indifferent_access?
|
|
125
193
|
true
|
|
126
194
|
end
|
|
127
195
|
|
|
196
|
+
# @example Replace existing data and convert incoming keys
|
|
197
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ old: true })
|
|
198
|
+
# hash.indifferent_replace(name: 'Ada')
|
|
199
|
+
# hash # => { 'name' => 'Ada' }
|
|
128
200
|
def indifferent_replace(other_hash)
|
|
129
201
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
130
202
|
other_hash.each { |key, value| self[key] = value }
|
|
131
203
|
self
|
|
132
204
|
end
|
|
133
205
|
|
|
206
|
+
# @example Merge without changing the original
|
|
207
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
208
|
+
# merged = hash.merge(age: 42)
|
|
209
|
+
# merged[:age] # => 42
|
|
210
|
+
# merged.indifferent_access? # => true
|
|
211
|
+
# hash.key?(:age) # => false
|
|
134
212
|
def merge(*args)
|
|
135
213
|
result = super
|
|
136
|
-
IndifferentAccess.inject!(result) if hash_lacking_indifference?(result)
|
|
214
|
+
return IndifferentAccess.inject!(result) if hash_lacking_indifference?(result)
|
|
215
|
+
|
|
137
216
|
result.convert!
|
|
138
217
|
end
|
|
139
218
|
|
|
219
|
+
# @example Merge and normalize in place
|
|
220
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
221
|
+
# hash.merge!(age: 42)
|
|
222
|
+
# hash['age'] # => 42
|
|
223
|
+
# hash.keys.sort # => ['age', 'name']
|
|
140
224
|
def merge!(*)
|
|
141
225
|
super.convert!
|
|
142
226
|
end
|
|
143
227
|
|
|
228
|
+
# @example Export a regular hash, preserving its default
|
|
229
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
|
|
230
|
+
# hash.default = 'unknown'
|
|
231
|
+
# plain = hash.to_hash
|
|
232
|
+
# plain # => { 'name' => 'Ada' }
|
|
233
|
+
# plain[:name] # => 'unknown'
|
|
234
|
+
# plain.respond_to?(:indifferent_access?) # => false
|
|
235
|
+
def to_hash
|
|
236
|
+
{}.tap do |result|
|
|
237
|
+
each_pair { |key, value| result[key] = value }
|
|
238
|
+
|
|
239
|
+
if default_proc
|
|
240
|
+
result.default_proc = default_proc
|
|
241
|
+
else
|
|
242
|
+
result.default = default
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# @example Select keys using either representation
|
|
248
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
|
|
249
|
+
# hash.slice(:name) # => { 'name' => 'Ada' }
|
|
250
|
+
def slice(*keys)
|
|
251
|
+
string_keys = keys.map { |key| convert_key(key) }
|
|
252
|
+
super(*string_keys)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @example Exclude keys using either representation
|
|
256
|
+
# hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
|
|
257
|
+
# hash.except(:age) # => { 'name' => 'Ada' }
|
|
258
|
+
def except(*keys)
|
|
259
|
+
string_keys = keys.map { |key| convert_key(key) }
|
|
260
|
+
super(*string_keys)
|
|
261
|
+
end
|
|
262
|
+
|
|
144
263
|
protected
|
|
145
264
|
|
|
146
265
|
def hash_lacking_indifference?(other)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
# @example Opt in to strict numeric coercion
|
|
6
|
+
# klass = Class.new(Hash) do
|
|
7
|
+
# include Hashie::Extensions::Coercion
|
|
8
|
+
# extend Hashie::Extensions::KernelCoercion
|
|
9
|
+
# coerce_key :age, Integer
|
|
10
|
+
# end
|
|
11
|
+
# hash = klass.new
|
|
12
|
+
# hash[:age] = '42'
|
|
13
|
+
# hash[:age] # => 42
|
|
14
|
+
# expect { hash[:age] = 'bad' }.to raise_error(Hashie::CoercionError)
|
|
15
|
+
module KernelCoercion
|
|
16
|
+
# Core types whose values can be coerced with a stricter
|
|
17
|
+
# +Kernel+ method (e.g. +Kernel#Integer+) instead of the more
|
|
18
|
+
# lenient +#to_i+-style conversion methods used by default.
|
|
19
|
+
#
|
|
20
|
+
# NOTE: There is no strict Kernel method equivalent for +Symbol+, so
|
|
21
|
+
# +Symbol+ coercion is left to the default, lenient +#to_sym+ behavior.
|
|
22
|
+
STRICT_CORE_TYPES = {
|
|
23
|
+
Integer => :Integer,
|
|
24
|
+
Float => :Float,
|
|
25
|
+
Complex => :Complex,
|
|
26
|
+
Rational => :Rational,
|
|
27
|
+
String => :String
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Overrides Hashie::Extensions::Coercion::ClassMethods#build_core_type_coercion
|
|
31
|
+
# to use a stricter Kernel method (e.g. Kernel#Integer) for coercing core
|
|
32
|
+
# types, instead of the default, more lenient conversion method (e.g. #to_i).
|
|
33
|
+
#
|
|
34
|
+
# "abcd".to_i #=> 0
|
|
35
|
+
# Integer("abcd") #=> ArgumentError: invalid value for Integer(): "abcd"
|
|
36
|
+
#
|
|
37
|
+
# @example Build a strict conversion and preserve Symbol fallback
|
|
38
|
+
# klass = Class.new(Hash) do
|
|
39
|
+
# include Hashie::Extensions::Coercion
|
|
40
|
+
# extend Hashie::Extensions::KernelCoercion
|
|
41
|
+
# end
|
|
42
|
+
# conversion = klass.build_core_type_coercion(Integer)
|
|
43
|
+
# conversion.call('42') # => 42
|
|
44
|
+
# expect { conversion.call('42 items') }.to raise_error(ArgumentError)
|
|
45
|
+
# klass.build_core_type_coercion(Symbol).call('name') # => :name
|
|
46
|
+
def build_core_type_coercion(type)
|
|
47
|
+
return super unless STRICT_CORE_TYPES.key?(type)
|
|
48
|
+
|
|
49
|
+
kernel_method = STRICT_CORE_TYPES[type]
|
|
50
|
+
lambda do |value|
|
|
51
|
+
next value if value.is_a?(type)
|
|
52
|
+
|
|
53
|
+
Kernel.public_send(kernel_method, value)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
5
|
+
# @example Disable conflict warnings on a Mash subclass
|
|
6
|
+
# klass = Class.new(Hashie::Mash)
|
|
7
|
+
# klass.disable_warnings(:size)
|
|
8
|
+
# klass.disable_warnings?(:size) # => true
|
|
9
|
+
# klass.disable_warnings?(:keys) # => false
|
|
3
10
|
module KeyConflictWarning
|
|
11
|
+
# @example Explain why base Mash warnings cannot be disabled
|
|
12
|
+
# require 'hashie/mash'
|
|
13
|
+
# error = Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings.new
|
|
14
|
+
# error.message.include?('please subclass the Mash') # => true
|
|
4
15
|
class CannotDisableMashWarnings < StandardError
|
|
16
|
+
# @example Construct the warning configuration error
|
|
17
|
+
# require 'hashie/mash'
|
|
18
|
+
# error = Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings.new
|
|
19
|
+
# error.message # => 'you cannot disable warnings on the base Mash class; please subclass the Mash and disable it in the subclass'
|
|
5
20
|
def initialize
|
|
6
21
|
super(
|
|
7
|
-
'
|
|
8
|
-
'
|
|
22
|
+
'you cannot disable warnings on the base Mash class; ' \
|
|
23
|
+
'please subclass the Mash and disable it in the subclass'
|
|
9
24
|
)
|
|
10
25
|
end
|
|
11
26
|
end
|
|
@@ -14,8 +29,13 @@ module Hashie
|
|
|
14
29
|
#
|
|
15
30
|
# @api semipublic
|
|
16
31
|
# @return [void]
|
|
32
|
+
# @example Disable warnings for selected methods
|
|
33
|
+
# klass = Class.new(Hashie::Mash)
|
|
34
|
+
# klass.disable_warnings(:size, :keys)
|
|
35
|
+
# klass.disabled_warnings # => [:size, :keys]
|
|
17
36
|
def disable_warnings(*method_keys)
|
|
18
37
|
raise CannotDisableMashWarnings if self == Hashie::Mash
|
|
38
|
+
|
|
19
39
|
if method_keys.any?
|
|
20
40
|
disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
|
|
21
41
|
else
|
|
@@ -29,23 +49,38 @@ module Hashie
|
|
|
29
49
|
#
|
|
30
50
|
# @api semipublic
|
|
31
51
|
# @return [Boolean]
|
|
52
|
+
# @example Disable all warnings when no methods are specified
|
|
53
|
+
# klass = Class.new(Hashie::Mash)
|
|
54
|
+
# klass.disable_warnings
|
|
55
|
+
# klass.disable_warnings?(:size) # => true
|
|
32
56
|
def disable_warnings?(method_key = nil)
|
|
33
57
|
return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
|
|
58
|
+
|
|
34
59
|
@disable_warnings ||= false
|
|
35
60
|
end
|
|
36
61
|
|
|
37
62
|
# Returns an array of methods that this class disables warnings for.
|
|
38
63
|
#
|
|
39
64
|
# @api semipublic
|
|
40
|
-
# @return [
|
|
65
|
+
# @return [Array<Symbol>]
|
|
66
|
+
# @example Inspect selected warning exclusions
|
|
67
|
+
# klass = Class.new(Hashie::Mash)
|
|
68
|
+
# klass.disable_warnings(:size)
|
|
69
|
+
# klass.disabled_warnings # => [:size]
|
|
41
70
|
def disabled_warnings
|
|
42
|
-
@
|
|
71
|
+
@disabled_warnings ||= []
|
|
43
72
|
end
|
|
44
73
|
|
|
45
74
|
# Inheritance hook that sets class configuration when inherited.
|
|
46
75
|
#
|
|
47
76
|
# @api semipublic
|
|
48
77
|
# @return [void]
|
|
78
|
+
# @example Inherit selected warning settings
|
|
79
|
+
# parent = Class.new(Hashie::Mash)
|
|
80
|
+
# parent.disable_warnings(:size)
|
|
81
|
+
# child = Class.new(parent)
|
|
82
|
+
# child.disabled_warnings # => [:size]
|
|
83
|
+
# child.disable_warnings?(:keys) # => false
|
|
49
84
|
def inherited(subclass)
|
|
50
85
|
super
|
|
51
86
|
subclass.disable_warnings(disabled_warnings) if disable_warnings?
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
5
|
+
# @example Include both key conversion extensions
|
|
6
|
+
# class ConvertibleHash < Hash
|
|
7
|
+
# include Hashie::Extensions::KeyConversion
|
|
8
|
+
# end
|
|
9
|
+
# hash = ConvertibleHash[:name, 'Ada']
|
|
10
|
+
# hash.stringify_keys # => { 'name' => 'Ada' }
|
|
11
|
+
# hash.stringify_keys!.symbolize_keys # => { name: 'Ada' }
|
|
3
12
|
module KeyConversion
|
|
4
13
|
def self.included(base)
|
|
5
14
|
base.send :include, SymbolizeKeys
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
6
|
+
# Generates reusable methods as Mash properties are accessed.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::DefineAccessors
|
|
11
|
+
# end
|
|
12
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
4
13
|
module DefineAccessors
|
|
14
|
+
# @example Install generated accessors on a subclass
|
|
15
|
+
# klass = Class.new(Hashie::Mash)
|
|
16
|
+
# klass.include(Hashie::Extensions::Mash::DefineAccessors)
|
|
17
|
+
# klass.new(quantity: 2).quantity # => 2
|
|
5
18
|
def self.included(klass)
|
|
6
19
|
klass.class_eval do
|
|
7
20
|
mod = Ext.new
|
|
@@ -9,23 +22,40 @@ module Hashie
|
|
|
9
22
|
end
|
|
10
23
|
end
|
|
11
24
|
|
|
25
|
+
# @example Install generated accessors on one Mash
|
|
26
|
+
# mash = Hashie::Mash.new(quantity: 2)
|
|
27
|
+
# mash.extend(Hashie::Extensions::Mash::DefineAccessors)
|
|
28
|
+
# mash.quantity # => 2
|
|
12
29
|
def self.extended(obj)
|
|
13
30
|
included(obj.singleton_class)
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# A module of lazily generated accessors for a Mash class.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
37
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
38
|
+
# mash = klass.new(name: 'Ada')
|
|
39
|
+
# accessors.define_reader('name', :name)
|
|
40
|
+
# mash.name # => 'Ada'
|
|
16
41
|
class Ext < Module
|
|
42
|
+
# @example Generate a method on first access
|
|
43
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
44
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
45
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
46
|
+
# accessors.instance_methods(false).include?(:name) # => true
|
|
17
47
|
def initialize
|
|
18
48
|
mod = self
|
|
19
49
|
define_method(:method_missing) do |method_name, *args, &block|
|
|
20
50
|
key, suffix = method_name_and_suffix(method_name)
|
|
21
51
|
case suffix
|
|
22
|
-
when '='
|
|
52
|
+
when '='
|
|
23
53
|
mod.define_writer(key, method_name)
|
|
24
|
-
when '?'
|
|
54
|
+
when '?'
|
|
25
55
|
mod.define_predicate(key, method_name)
|
|
26
|
-
when '!'
|
|
56
|
+
when '!'
|
|
27
57
|
mod.define_initializing_reader(key, method_name)
|
|
28
|
-
when '_'
|
|
58
|
+
when '_'
|
|
29
59
|
mod.define_underbang_reader(key, method_name)
|
|
30
60
|
else
|
|
31
61
|
mod.define_reader(key, method_name)
|
|
@@ -34,6 +64,12 @@ module Hashie
|
|
|
34
64
|
end
|
|
35
65
|
end
|
|
36
66
|
|
|
67
|
+
# @example
|
|
68
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
69
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
70
|
+
# mash = klass.new(name: 'Ada')
|
|
71
|
+
# accessors.define_reader('name', :name)
|
|
72
|
+
# mash.name # => 'Ada'
|
|
37
73
|
def define_reader(key, method_name)
|
|
38
74
|
define_method(method_name) do |&block|
|
|
39
75
|
if key? method_name
|
|
@@ -44,40 +80,67 @@ module Hashie
|
|
|
44
80
|
end
|
|
45
81
|
end
|
|
46
82
|
|
|
83
|
+
# @example
|
|
84
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
85
|
+
# accessors.define_writer('name', :name=)
|
|
86
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
87
|
+
# mash = klass.new
|
|
88
|
+
# mash.name = 'Ada'
|
|
89
|
+
# mash['name'] # => 'Ada'
|
|
47
90
|
def define_writer(key, method_name)
|
|
48
91
|
define_method(method_name) do |value = nil|
|
|
49
92
|
if key? method_name
|
|
50
|
-
self.[](method_name
|
|
93
|
+
self.[](method_name)
|
|
51
94
|
else
|
|
52
95
|
assign_property(key, value)
|
|
53
96
|
end
|
|
54
97
|
end
|
|
55
98
|
end
|
|
56
99
|
|
|
100
|
+
# @example
|
|
101
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
102
|
+
# accessors.define_predicate('name', :name?)
|
|
103
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
104
|
+
# klass.new(name: 'Ada').name? # => true
|
|
105
|
+
# klass.new.name? # => false
|
|
57
106
|
def define_predicate(key, method_name)
|
|
58
107
|
define_method(method_name) do
|
|
59
108
|
if key? method_name
|
|
60
|
-
self.[](method_name
|
|
109
|
+
self.[](method_name)
|
|
61
110
|
else
|
|
62
111
|
!!self[key]
|
|
63
112
|
end
|
|
64
113
|
end
|
|
65
114
|
end
|
|
66
115
|
|
|
116
|
+
# @example
|
|
117
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
118
|
+
# accessors.define_initializing_reader('address', :address!)
|
|
119
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
120
|
+
# mash = klass.new
|
|
121
|
+
# mash.address!.city = 'London'
|
|
122
|
+
# mash.address.city # => 'London'
|
|
67
123
|
def define_initializing_reader(key, method_name)
|
|
68
124
|
define_method(method_name) do
|
|
69
125
|
if key? method_name
|
|
70
|
-
self.[](method_name
|
|
126
|
+
self.[](method_name)
|
|
71
127
|
else
|
|
72
128
|
initializing_reader(key)
|
|
73
129
|
end
|
|
74
130
|
end
|
|
75
131
|
end
|
|
76
132
|
|
|
133
|
+
# @example
|
|
134
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
135
|
+
# accessors.define_underbang_reader('address', :address_)
|
|
136
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
137
|
+
# mash = klass.new
|
|
138
|
+
# mash.address_.city = 'London'
|
|
139
|
+
# mash.key?('address') # => false
|
|
77
140
|
def define_underbang_reader(key, method_name)
|
|
78
141
|
define_method(method_name) do
|
|
79
142
|
if key? method_name
|
|
80
|
-
self.[](key
|
|
143
|
+
self.[](key)
|
|
81
144
|
else
|
|
82
145
|
underbang_reader(key)
|
|
83
146
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
@@ -14,8 +16,13 @@ module Hashie
|
|
|
14
16
|
# mash['string_key'] == mash[:string_key] #=> true
|
|
15
17
|
# mash[:symbol_key] == mash['symbol_key'] #=> true
|
|
16
18
|
module KeepOriginalKeys
|
|
19
|
+
# @example Preserve symbol keys when including the extension
|
|
20
|
+
# klass = Class.new(Hashie::Mash) do
|
|
21
|
+
# include Hashie::Extensions::Mash::KeepOriginalKeys
|
|
22
|
+
# end
|
|
23
|
+
# klass.new(name: 'Ada').keys # => [:name]
|
|
17
24
|
def self.included(descendant)
|
|
18
|
-
error_message = "
|
|
25
|
+
error_message = "`#{descendant}` is not a kind of Hashie::Mash"
|
|
19
26
|
raise ArgumentError, error_message unless descendant <= Hashie::Mash
|
|
20
27
|
end
|
|
21
28
|
|
|
@@ -26,13 +33,10 @@ module Hashie
|
|
|
26
33
|
# @param [Object, String, Symbol] key the key to access.
|
|
27
34
|
# @return [Object] the value assigned to the key.
|
|
28
35
|
def convert_key(key)
|
|
29
|
-
if regular_key?(key)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
key
|
|
35
|
-
end
|
|
36
|
+
return key if regular_key?(key)
|
|
37
|
+
|
|
38
|
+
converted_key = __convert(key)
|
|
39
|
+
converted_key && regular_key?(converted_key) ? converted_key : key
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# Converts symbol/string keys to their alternative formats, but leaves
|
|
@@ -42,7 +46,7 @@ module Hashie
|
|
|
42
46
|
# @return [Object, String, Symbol] the converted key.
|
|
43
47
|
def __convert(key)
|
|
44
48
|
case key
|
|
45
|
-
when Symbol then key.
|
|
49
|
+
when Symbol then key.name
|
|
46
50
|
when String then key.to_sym
|
|
47
51
|
else key
|
|
48
52
|
end
|