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
data/lib/hashie/mash.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'hashie/hash'
|
|
2
4
|
require 'hashie/array'
|
|
3
5
|
require 'hashie/utils'
|
|
@@ -15,13 +17,13 @@ module Hashie
|
|
|
15
17
|
# based on the following rules:
|
|
16
18
|
#
|
|
17
19
|
# * No punctuation: Returns the value of the hash for that key, or nil if none exists.
|
|
18
|
-
# * Assignment (
|
|
19
|
-
# * Truthiness (
|
|
20
|
+
# * Assignment (`=`): Sets the attribute of the given method name.
|
|
21
|
+
# * Truthiness (`?`): Returns true or false depending on the truthiness of
|
|
20
22
|
# the attribute, or false if the key is not set.
|
|
21
|
-
# * Bang (
|
|
23
|
+
# * Bang (`!`): Forces the existence of this key, used for deep Mashes. Think of it
|
|
22
24
|
# as "touch" for mashes.
|
|
23
|
-
# * Under Bang (
|
|
24
|
-
# Used to test
|
|
25
|
+
# * Under Bang (`_`): Like Bang, but returns a new Mash rather than creating a key.
|
|
26
|
+
# Used to test existence in deep Mashes.
|
|
25
27
|
#
|
|
26
28
|
# == Basic Example
|
|
27
29
|
#
|
|
@@ -61,24 +63,47 @@ module Hashie
|
|
|
61
63
|
# mash.author_.name = "Michael Bleigh" (assigned to temp object)
|
|
62
64
|
# mash.author # => <Mash>
|
|
63
65
|
#
|
|
66
|
+
# @example Read nested hashes through method access
|
|
67
|
+
# mash = Hashie::Mash.new(author: { name: 'Ada' })
|
|
68
|
+
# mash.author.name # => 'Ada'
|
|
69
|
+
# mash.missing? # => false
|
|
64
70
|
class Mash < Hash
|
|
65
|
-
include Hashie::Extensions::PrettyInspect
|
|
66
|
-
include Hashie::Extensions::RubyVersionCheck
|
|
67
71
|
extend Hashie::Extensions::KeyConflictWarning
|
|
68
72
|
|
|
69
73
|
ALLOWED_SUFFIXES = %w[? ! = _].freeze
|
|
70
74
|
|
|
75
|
+
# Loads a YAML file. The default parser evaluates ERB templates, so only
|
|
76
|
+
# use this default with trusted files. Pass YamlParser to avoid ERB.
|
|
77
|
+
#
|
|
78
|
+
# @example Load and cache a frozen configuration from a YAML file
|
|
79
|
+
# require 'tempfile'
|
|
80
|
+
# file = Tempfile.new(['hashie-example', '.yml'], Dir.pwd)
|
|
81
|
+
# begin
|
|
82
|
+
# file.write("name: Ada\n")
|
|
83
|
+
# file.close
|
|
84
|
+
# settings = Hashie::Mash.load(file.path, parser: Hashie::Extensions::Parsers::YamlParser)
|
|
85
|
+
# settings.name # => 'Ada'
|
|
86
|
+
# settings.frozen? # => true
|
|
87
|
+
# File.write(file.path, "name: Grace\n")
|
|
88
|
+
# Hashie::Mash.load(file.path).to_hash # => { 'name' => 'Ada' }
|
|
89
|
+
# ensure
|
|
90
|
+
# file.close!
|
|
91
|
+
# end
|
|
71
92
|
def self.load(path, options = {})
|
|
72
93
|
@_mashes ||= new
|
|
73
94
|
|
|
74
95
|
return @_mashes[path] if @_mashes.key?(path)
|
|
75
|
-
raise ArgumentError, "
|
|
96
|
+
raise ArgumentError, "file not found: `#{path}`" unless File.file?(path)
|
|
76
97
|
|
|
77
98
|
options = options.dup
|
|
78
99
|
parser = options.delete(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
|
|
79
100
|
@_mashes[path] = new(parser.perform(path, options)).freeze
|
|
80
101
|
end
|
|
81
102
|
|
|
103
|
+
# @example Expose a Mash from an included module
|
|
104
|
+
# settings = Hashie::Mash.new(name: 'Ada')
|
|
105
|
+
# klass = Class.new { include settings.to_module(:configuration) }
|
|
106
|
+
# klass.new.configuration.name # => 'Ada'
|
|
82
107
|
def to_module(mash_method_name = :settings)
|
|
83
108
|
mash = self
|
|
84
109
|
Module.new do |m|
|
|
@@ -88,16 +113,28 @@ module Hashie
|
|
|
88
113
|
end
|
|
89
114
|
end
|
|
90
115
|
|
|
116
|
+
# @example Generate real methods for existing keys
|
|
117
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
118
|
+
# mash.with_accessors!
|
|
119
|
+
# mash.name # => 'Ada'
|
|
120
|
+
# mash.method(:name).owner.instance_methods(false).include?(:name) # => true
|
|
91
121
|
def with_accessors!
|
|
92
122
|
extend Hashie::Extensions::Mash::DefineAccessors
|
|
93
123
|
end
|
|
94
124
|
|
|
125
|
+
# @example Inspect the Mash's attributes
|
|
126
|
+
# Hashie::Mash.new(name: 'Ada').to_s # => '#<Hashie::Mash name="Ada">'
|
|
95
127
|
alias to_s inspect
|
|
96
128
|
|
|
97
129
|
# If you pass in an existing hash, it will
|
|
98
130
|
# convert it to a Mash including recursively
|
|
99
131
|
# descending into arrays and hashes, converting
|
|
100
132
|
# them as well.
|
|
133
|
+
#
|
|
134
|
+
# @example Recursively convert hashes nested in arrays
|
|
135
|
+
# mash = Hashie::Mash.new(users: [{ name: 'Ada' }])
|
|
136
|
+
# mash.users.first.name # => 'Ada'
|
|
137
|
+
# mash.users.class # => Hashie::Array
|
|
101
138
|
def initialize(source_hash = nil, default = nil, &blk)
|
|
102
139
|
deep_update(source_hash) if source_hash
|
|
103
140
|
default ? super(default) : super(&blk)
|
|
@@ -109,6 +146,11 @@ module Hashie
|
|
|
109
146
|
# Hashie::Mash.quiet.new(hash) all warnings disabled.
|
|
110
147
|
# Hashie::Mash.quiet(:zip).new(hash) only zip warning
|
|
111
148
|
# is disabled.
|
|
149
|
+
#
|
|
150
|
+
# @example Disable warnings for a conflicting key on a subclass
|
|
151
|
+
# klass = Hashie::Mash.quiet(:zip)
|
|
152
|
+
# klass.new(zip: '12345')[:zip] # => '12345'
|
|
153
|
+
# klass.disable_warnings?(:zip) # => true
|
|
112
154
|
def self.quiet(*method_keys)
|
|
113
155
|
@memoized_classes ||= {}
|
|
114
156
|
@memoized_classes[method_keys] ||= Class.new(self) do
|
|
@@ -116,13 +158,33 @@ module Hashie
|
|
|
116
158
|
end
|
|
117
159
|
end
|
|
118
160
|
|
|
119
|
-
class << self
|
|
161
|
+
class << self
|
|
162
|
+
# @example Construct a Mash with bracket notation
|
|
163
|
+
# Hashie::Mash[name: 'Ada'].name # => 'Ada'
|
|
164
|
+
alias [] new
|
|
165
|
+
end
|
|
120
166
|
|
|
167
|
+
# @example Read stored string keys without key conversion
|
|
168
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
169
|
+
# mash.regular_reader('name') # => 'Ada'
|
|
170
|
+
# mash.regular_reader(:name) # => nil
|
|
121
171
|
alias regular_reader []
|
|
172
|
+
|
|
173
|
+
# @example Store a value without recursively converting it
|
|
174
|
+
# mash = Hashie::Mash.new
|
|
175
|
+
# mash.regular_writer('user', { name: 'Ada' })
|
|
176
|
+
# mash['user'].class # => Hash
|
|
177
|
+
# mash['user'] # => { name: 'Ada' }
|
|
122
178
|
alias regular_writer []=
|
|
123
179
|
|
|
124
|
-
# Retrieves an attribute set in the Mash. Will convert
|
|
125
|
-
#
|
|
180
|
+
# Retrieves an attribute set in the Mash. Will convert a key passed in
|
|
181
|
+
# as a symbol to a string before retrieving.
|
|
182
|
+
#
|
|
183
|
+
# @example Read a symbol key and yield the result
|
|
184
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
185
|
+
# observed = []
|
|
186
|
+
# mash.custom_reader(:name) { |value| observed << value } # => 'Ada'
|
|
187
|
+
# observed # => ['Ada']
|
|
126
188
|
def custom_reader(key)
|
|
127
189
|
default_proc.call(self, key) if default_proc && !key?(key)
|
|
128
190
|
value = regular_reader(convert_key(key))
|
|
@@ -130,21 +192,39 @@ module Hashie
|
|
|
130
192
|
value
|
|
131
193
|
end
|
|
132
194
|
|
|
133
|
-
# Sets an attribute in the Mash.
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
# Sets an attribute in the Mash. Symbol keys will be converted to
|
|
196
|
+
# strings before being set, and Hashes will be converted into Mashes
|
|
197
|
+
# for nesting purposes.
|
|
198
|
+
#
|
|
199
|
+
# @example Normalize assigned keys and recursively convert values
|
|
200
|
+
# mash = Hashie::Mash.new
|
|
201
|
+
# mash.custom_writer(:user, { name: 'Ada' })
|
|
202
|
+
# mash.keys # => ['user']
|
|
203
|
+
# mash.user.name # => 'Ada'
|
|
204
|
+
def custom_writer(key, value, convert = true) # :nodoc:
|
|
205
|
+
log_built_in_message(key) if key.respond_to?(:to_sym) && log_collision?(key.to_sym)
|
|
206
|
+
regular_writer(convert_key(key), convert ? convert_value(value) : value)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# @example Read symbol and string keys interchangeably
|
|
210
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
211
|
+
# mash[:name] # => 'Ada'
|
|
212
|
+
# mash['name'] # => 'Ada'
|
|
143
213
|
alias [] custom_reader
|
|
214
|
+
|
|
215
|
+
# @example Convert assigned hashes to nested Mashes
|
|
216
|
+
# mash = Hashie::Mash.new
|
|
217
|
+
# mash[:user] = { name: 'Ada' }
|
|
218
|
+
# mash.user.name # => 'Ada'
|
|
144
219
|
alias []= custom_writer
|
|
145
220
|
|
|
146
221
|
# This is the bang method reader, it will return a new Mash
|
|
147
222
|
# if there isn't a value already assigned to the key requested.
|
|
223
|
+
#
|
|
224
|
+
# @example Persist an initially empty nested Mash
|
|
225
|
+
# mash = Hashie::Mash.new
|
|
226
|
+
# mash.initializing_reader(:user).name = 'Ada'
|
|
227
|
+
# mash.user.name # => 'Ada'
|
|
148
228
|
def initializing_reader(key)
|
|
149
229
|
ck = convert_key(key)
|
|
150
230
|
regular_writer(ck, self.class.new) unless key?(ck)
|
|
@@ -153,6 +233,11 @@ module Hashie
|
|
|
153
233
|
|
|
154
234
|
# This is the under bang method reader, it will return a temporary new Mash
|
|
155
235
|
# if there isn't a value already assigned to the key requested.
|
|
236
|
+
#
|
|
237
|
+
# @example Read an absent branch without storing it
|
|
238
|
+
# mash = Hashie::Mash.new
|
|
239
|
+
# mash.underbang_reader(:user).empty? # => true
|
|
240
|
+
# mash.key?(:user) # => false
|
|
156
241
|
def underbang_reader(key)
|
|
157
242
|
ck = convert_key(key)
|
|
158
243
|
if key?(ck)
|
|
@@ -162,14 +247,24 @@ module Hashie
|
|
|
162
247
|
end
|
|
163
248
|
end
|
|
164
249
|
|
|
250
|
+
# @example Fetch symbol keys and supply a fallback for missing keys
|
|
251
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
252
|
+
# mash.fetch(:name) # => 'Ada'
|
|
253
|
+
# mash.fetch(:missing, 'unknown') # => 'unknown'
|
|
165
254
|
def fetch(key, *args)
|
|
166
255
|
super(convert_key(key), *args)
|
|
167
256
|
end
|
|
168
257
|
|
|
258
|
+
# @example Delete a string-backed key using a symbol
|
|
259
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
260
|
+
# mash.delete(:name) # => 'Ada'
|
|
261
|
+
# mash.empty? # => true
|
|
169
262
|
def delete(key)
|
|
170
263
|
super(convert_key(key))
|
|
171
264
|
end
|
|
172
265
|
|
|
266
|
+
# @example Retrieve multiple keys using either key form
|
|
267
|
+
# Hashie::Mash.new(name: 'Ada', age: 36).values_at(:name, 'age', :missing) # => ['Ada', 36, nil]
|
|
173
268
|
def values_at(*keys)
|
|
174
269
|
super(*keys.map { |key| convert_key(key) })
|
|
175
270
|
end
|
|
@@ -177,71 +272,139 @@ module Hashie
|
|
|
177
272
|
# Returns a new instance of the class it was called on, using its keys as
|
|
178
273
|
# values, and its values as keys. The new values and keys will always be
|
|
179
274
|
# strings.
|
|
275
|
+
#
|
|
276
|
+
# @example Swap keys and values while retaining Mash behavior
|
|
277
|
+
# inverted = Hashie::Mash.new(name: :author).invert
|
|
278
|
+
# inverted.author # => 'name'
|
|
279
|
+
# inverted.class # => Hashie::Mash
|
|
180
280
|
def invert
|
|
181
281
|
self.class.new(super)
|
|
182
282
|
end
|
|
183
283
|
|
|
184
284
|
# Returns a new instance of the class it was called on, containing elements
|
|
185
285
|
# for which the given block returns false.
|
|
286
|
+
#
|
|
287
|
+
# @example Remove selected entries from a copy
|
|
288
|
+
# mash = Hashie::Mash.new(name: 'Ada', age: 36)
|
|
289
|
+
# mash.reject { |key, _value| key == 'age' }.to_hash # => { 'name' => 'Ada' }
|
|
290
|
+
# mash.age # => 36
|
|
186
291
|
def reject(&blk)
|
|
187
|
-
self.class.new(super
|
|
292
|
+
self.class.new(super)
|
|
188
293
|
end
|
|
189
294
|
|
|
190
295
|
# Returns a new instance of the class it was called on, containing elements
|
|
191
296
|
# for which the given block returns true.
|
|
297
|
+
#
|
|
298
|
+
# @example Keep matching entries in a new Mash
|
|
299
|
+
# selected = Hashie::Mash.new(name: 'Ada', age: 36).select { |key, _value| key == 'name' }
|
|
300
|
+
# selected.name # => 'Ada'
|
|
301
|
+
# selected.key?(:age) # => false
|
|
192
302
|
def select(&blk)
|
|
193
|
-
self.class.new(super
|
|
303
|
+
self.class.new(super)
|
|
194
304
|
end
|
|
195
305
|
|
|
306
|
+
# @example Make a shallow copy using the original Hash duplication
|
|
307
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
308
|
+
# copy = mash.regular_dup
|
|
309
|
+
# copy.equal?(mash) # => false
|
|
310
|
+
# copy.user.equal?(mash.user) # => true
|
|
196
311
|
alias regular_dup dup
|
|
312
|
+
|
|
313
|
+
# Hash's own readers, since a key like "default" can override them.
|
|
314
|
+
HASH_DEFAULT = ::Hash.instance_method(:default)
|
|
315
|
+
HASH_DEFAULT_PROC = ::Hash.instance_method(:default_proc)
|
|
316
|
+
private_constant :HASH_DEFAULT, :HASH_DEFAULT_PROC
|
|
317
|
+
|
|
197
318
|
# Duplicates the current mash as a new mash.
|
|
319
|
+
#
|
|
320
|
+
# @example Duplicate nested Mashes independently
|
|
321
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
322
|
+
# copy = mash.dup
|
|
323
|
+
# copy.user.name = 'Grace'
|
|
324
|
+
# mash.user.name # => 'Ada'
|
|
325
|
+
# copy.user.name # => 'Grace'
|
|
198
326
|
def dup
|
|
327
|
+
default = HASH_DEFAULT.bind_call(self)
|
|
328
|
+
default_proc = HASH_DEFAULT_PROC.bind_call(self)
|
|
199
329
|
self.class.new(self, default, &default_proc)
|
|
200
330
|
end
|
|
201
331
|
|
|
332
|
+
# @example Check the exact stored key without symbol conversion
|
|
333
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
334
|
+
# mash.regular_key?('name') # => true
|
|
335
|
+
# mash.regular_key?(:name) # => false
|
|
202
336
|
alias regular_key? key?
|
|
337
|
+
|
|
338
|
+
# @example Recognize symbol and string versions of a key
|
|
339
|
+
# mash = Hashie::Mash.new(name: nil)
|
|
340
|
+
# mash.key?(:name) # => true
|
|
341
|
+
# mash.key?('name') # => true
|
|
203
342
|
def key?(key)
|
|
204
343
|
super(convert_key(key))
|
|
205
344
|
end
|
|
345
|
+
# @example Test key presence even for a nil value
|
|
346
|
+
# Hashie::Mash.new(name: nil).has_key?(:name) # => true
|
|
206
347
|
alias has_key? key?
|
|
207
|
-
alias include? key?
|
|
208
|
-
alias member? key?
|
|
209
348
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def deep_merge(*other_hashes, &blk)
|
|
214
|
-
dup.deep_update(*other_hashes, &blk)
|
|
215
|
-
end
|
|
349
|
+
# @example Test membership with a symbol key
|
|
350
|
+
# Hashie::Mash.new(name: 'Ada').include?(:name) # => true
|
|
351
|
+
alias include? key?
|
|
216
352
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
other_hashes.each do |other_hash|
|
|
221
|
-
_deep_update(other_hash, &blk)
|
|
222
|
-
end
|
|
223
|
-
self
|
|
224
|
-
end
|
|
225
|
-
else
|
|
226
|
-
# Performs a deep_update on a duplicate of the
|
|
227
|
-
# current mash.
|
|
228
|
-
def deep_merge(other_hash, &blk)
|
|
229
|
-
dup.deep_update(other_hash, &blk)
|
|
230
|
-
end
|
|
353
|
+
# @example Test membership with a string key
|
|
354
|
+
# Hashie::Mash.new(name: 'Ada').member?('name') # => true
|
|
355
|
+
alias member? key?
|
|
231
356
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
357
|
+
# Performs a deep_update on a duplicate of the
|
|
358
|
+
# current mash.
|
|
359
|
+
#
|
|
360
|
+
# @example Recursively merge without modifying the receiver
|
|
361
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
362
|
+
# merged = mash.deep_merge(user: { age: 36 })
|
|
363
|
+
# merged.user.to_hash # => { 'name' => 'Ada', 'age' => 36 }
|
|
364
|
+
# mash.user.key?(:age) # => false
|
|
365
|
+
def deep_merge(*other_hashes, &blk)
|
|
366
|
+
dup.deep_update(*other_hashes, &blk)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Recursively merges this mash with the passed
|
|
370
|
+
# in hash, merging each hash in the hierarchy.
|
|
371
|
+
#
|
|
372
|
+
# @example Update nested values in place with conflict resolution
|
|
373
|
+
# mash = Hashie::Mash.new(user: { visits: 2 })
|
|
374
|
+
# mash.deep_update(user: { visits: 3 }) { |_key, old, new| old + new }
|
|
375
|
+
# mash.user.visits # => 5
|
|
376
|
+
def deep_update(*other_hashes, &blk)
|
|
377
|
+
other_hashes.each do |other_hash|
|
|
235
378
|
_deep_update(other_hash, &blk)
|
|
236
|
-
self
|
|
237
379
|
end
|
|
380
|
+
self
|
|
238
381
|
end
|
|
239
382
|
|
|
240
383
|
# Alias these lexically so they get the correctly defined
|
|
241
384
|
# #deep_merge and #deep_update based on ruby version.
|
|
385
|
+
#
|
|
386
|
+
# @example Merge nested hashes into a copy
|
|
387
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
388
|
+
# mash.merge(user: { age: 36 }).user.to_hash # => { 'name' => 'Ada', 'age' => 36 }
|
|
242
389
|
alias merge deep_merge
|
|
390
|
+
|
|
391
|
+
# @example Recursively merge into the receiver
|
|
392
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
393
|
+
# mash.deep_merge!(user: { age: 36 })
|
|
394
|
+
# mash.user.to_hash # => { 'name' => 'Ada', 'age' => 36 }
|
|
243
395
|
alias deep_merge! deep_update
|
|
396
|
+
|
|
397
|
+
# @example Update the receiver with nested keys
|
|
398
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
399
|
+
# mash.update(user: { age: 36 })
|
|
400
|
+
# mash.user.age # => 36
|
|
401
|
+
# mash.user.name # => 'Ada'
|
|
244
402
|
alias update deep_update
|
|
403
|
+
|
|
404
|
+
# @example Merge multiple hashes into the receiver
|
|
405
|
+
# mash = Hashie::Mash.new
|
|
406
|
+
# mash.merge!({ name: 'Ada' }, { age: 36 })
|
|
407
|
+
# mash.to_hash # => { 'name' => 'Ada', 'age' => 36 }
|
|
245
408
|
alias merge! update
|
|
246
409
|
|
|
247
410
|
def _deep_update(other_hash, &blk)
|
|
@@ -259,17 +422,32 @@ module Hashie
|
|
|
259
422
|
private :_deep_update
|
|
260
423
|
|
|
261
424
|
# Assigns a value to a key
|
|
425
|
+
#
|
|
426
|
+
# @example Assign a property by name
|
|
427
|
+
# mash = Hashie::Mash.new
|
|
428
|
+
# mash.assign_property(:name, 'Ada')
|
|
429
|
+
# mash.name # => 'Ada'
|
|
262
430
|
def assign_property(name, value)
|
|
263
431
|
self[name] = value
|
|
264
432
|
end
|
|
265
433
|
|
|
266
434
|
# Performs a shallow_update on a duplicate of the current mash
|
|
435
|
+
#
|
|
436
|
+
# @example Replace a nested hash in a copy rather than merging it
|
|
437
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada', age: 36 })
|
|
438
|
+
# mash.shallow_merge(user: { name: 'Grace' }).user.to_hash # => { 'name' => 'Grace' }
|
|
439
|
+
# mash.user.age # => 36
|
|
267
440
|
def shallow_merge(other_hash)
|
|
268
441
|
dup.shallow_update(other_hash)
|
|
269
442
|
end
|
|
270
443
|
|
|
271
444
|
# Merges (non-recursively) the hash from the argument,
|
|
272
445
|
# changing the receiving hash
|
|
446
|
+
#
|
|
447
|
+
# @example Replace a nested value in place
|
|
448
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada', age: 36 })
|
|
449
|
+
# mash.shallow_update(user: { name: 'Grace' })
|
|
450
|
+
# mash.user.to_hash # => { 'name' => 'Grace' }
|
|
273
451
|
def shallow_update(other_hash)
|
|
274
452
|
other_hash.each_pair do |k, v|
|
|
275
453
|
regular_writer(convert_key(k), convert_value(v, true))
|
|
@@ -277,14 +455,24 @@ module Hashie
|
|
|
277
455
|
self
|
|
278
456
|
end
|
|
279
457
|
|
|
458
|
+
# @example Discard omitted attributes and convert replacement keys
|
|
459
|
+
# mash = Hashie::Mash.new(name: 'Ada', age: 36)
|
|
460
|
+
# mash.replace(name: 'Grace')
|
|
461
|
+
# mash.to_hash # => { 'name' => 'Grace' }
|
|
280
462
|
def replace(other_hash)
|
|
281
463
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
282
464
|
other_hash.each { |key, value| self[key] = value }
|
|
283
465
|
self
|
|
284
466
|
end
|
|
285
467
|
|
|
468
|
+
# @example Discover dynamic readers and suffix methods
|
|
469
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
470
|
+
# mash.respond_to?(:name) # => true
|
|
471
|
+
# mash.respond_to?(:missing) # => false
|
|
472
|
+
# mash.respond_to?(:missing!) # => true
|
|
286
473
|
def respond_to_missing?(method_name, *args)
|
|
287
474
|
return true if key?(method_name)
|
|
475
|
+
|
|
288
476
|
suffix = method_suffix(method_name)
|
|
289
477
|
if suffix
|
|
290
478
|
true
|
|
@@ -293,22 +481,34 @@ module Hashie
|
|
|
293
481
|
end
|
|
294
482
|
end
|
|
295
483
|
|
|
484
|
+
# @example Recognize suffix methods for existing keys
|
|
485
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
486
|
+
# mash.prefix_method?(:name?) # => true
|
|
487
|
+
# mash.prefix_method?(:missing?) # => false
|
|
296
488
|
def prefix_method?(method_name)
|
|
297
489
|
method_name = method_name.to_s
|
|
298
490
|
method_name.end_with?(*ALLOWED_SUFFIXES) && key?(method_name.chop)
|
|
299
491
|
end
|
|
300
492
|
|
|
301
|
-
|
|
493
|
+
# @example Use dynamic assignment, queries, and nested initialization
|
|
494
|
+
# mash = Hashie::Mash.new
|
|
495
|
+
# mash.user!.name = 'Ada'
|
|
496
|
+
# mash.user.name # => 'Ada'
|
|
497
|
+
# mash.user? # => true
|
|
498
|
+
# mash.missing_.empty? # => true
|
|
499
|
+
# mash.key?(:missing) # => false
|
|
500
|
+
def method_missing(method_name, *args, &blk)
|
|
302
501
|
return self.[](method_name, &blk) if key?(method_name)
|
|
502
|
+
|
|
303
503
|
name, suffix = method_name_and_suffix(method_name)
|
|
304
504
|
case suffix
|
|
305
|
-
when '='
|
|
505
|
+
when '='
|
|
306
506
|
assign_property(name, args.first)
|
|
307
|
-
when '?'
|
|
507
|
+
when '?'
|
|
308
508
|
!!self[name]
|
|
309
|
-
when '!'
|
|
509
|
+
when '!'
|
|
310
510
|
initializing_reader(name)
|
|
311
|
-
when '_'
|
|
511
|
+
when '_'
|
|
312
512
|
underbang_reader(name)
|
|
313
513
|
else
|
|
314
514
|
self[method_name]
|
|
@@ -316,42 +516,69 @@ module Hashie
|
|
|
316
516
|
end
|
|
317
517
|
|
|
318
518
|
# play nice with ActiveSupport Array#extract_options!
|
|
519
|
+
#
|
|
520
|
+
# @example Identify a Mash as eligible for options extraction
|
|
521
|
+
# Hashie::Mash.new(name: 'Ada').extractable_options? # => true
|
|
319
522
|
def extractable_options?
|
|
320
523
|
true
|
|
321
524
|
end
|
|
322
525
|
|
|
323
526
|
# another ActiveSupport method, see issue #270
|
|
527
|
+
#
|
|
528
|
+
# @example Supply defaults without overwriting existing values
|
|
529
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
530
|
+
# mash.reverse_merge(name: 'Grace', active: true).to_hash # => { 'name' => 'Ada', 'active' => true }
|
|
324
531
|
def reverse_merge(other_hash)
|
|
325
532
|
self.class.new(other_hash).merge(self)
|
|
326
533
|
end
|
|
327
534
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
535
|
+
# @example Traverse nested Mashes with symbol keys
|
|
536
|
+
# mash = Hashie::Mash.new(user: { name: 'Ada' })
|
|
537
|
+
# mash.dig(:user, :name) # => 'Ada'
|
|
538
|
+
# mash.dig(:missing, :name) # => nil
|
|
539
|
+
def dig(*keys)
|
|
540
|
+
super(*keys.map { |key| convert_key(key) })
|
|
332
541
|
end
|
|
333
542
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
543
|
+
# @example Transform values into a new Mash
|
|
544
|
+
# transformed = Hashie::Mash.new(visits: 2).transform_values { |value| value * 3 }
|
|
545
|
+
# transformed.visits # => 6
|
|
546
|
+
# transformed.class # => Hashie::Mash
|
|
547
|
+
def transform_values(&blk)
|
|
548
|
+
self.class.new(super)
|
|
549
|
+
end
|
|
338
550
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
551
|
+
# Returns a new instance of the class it was called on, with nil values
|
|
552
|
+
# removed.
|
|
553
|
+
#
|
|
554
|
+
# @example Remove nils while preserving false
|
|
555
|
+
# Hashie::Mash.new(name: nil, active: false).compact.to_hash # => { 'active' => false }
|
|
556
|
+
def compact
|
|
557
|
+
self.class.new(super)
|
|
344
558
|
end
|
|
345
559
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
560
|
+
# @example Select keys with either symbol or string names
|
|
561
|
+
# Hashie::Mash.new(name: 'Ada', age: 36).slice(:name).to_hash # => { 'name' => 'Ada' }
|
|
562
|
+
def slice(*keys)
|
|
563
|
+
string_keys = keys.map { |key| convert_key(key) }
|
|
564
|
+
self.class.new(super(*string_keys))
|
|
565
|
+
end
|
|
351
566
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
567
|
+
# @example Transform keys into a new Mash
|
|
568
|
+
# transformed = Hashie::Mash.new(name: 'Ada').transform_keys(&:upcase)
|
|
569
|
+
# transformed['NAME'] # => 'Ada'
|
|
570
|
+
# transformed.class # => Hashie::Mash
|
|
571
|
+
def transform_keys(&blk)
|
|
572
|
+
self.class.new(super)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# @example Exclude symbol keys from a copy
|
|
576
|
+
# mash = Hashie::Mash.new(name: 'Ada', age: 36)
|
|
577
|
+
# mash.except(:age).to_hash # => { 'name' => 'Ada' }
|
|
578
|
+
# mash.age # => 36
|
|
579
|
+
def except(*keys)
|
|
580
|
+
string_keys = keys.map { |key| convert_key(key) }
|
|
581
|
+
self.class.new(super(*string_keys))
|
|
355
582
|
end
|
|
356
583
|
|
|
357
584
|
protected
|
|
@@ -361,7 +588,7 @@ module Hashie
|
|
|
361
588
|
if method_name.end_with?(*ALLOWED_SUFFIXES)
|
|
362
589
|
[method_name[0..-2], method_name[-1]]
|
|
363
590
|
else
|
|
364
|
-
[method_name
|
|
591
|
+
[method_name, nil]
|
|
365
592
|
end
|
|
366
593
|
end
|
|
367
594
|
|
|
@@ -370,11 +597,15 @@ module Hashie
|
|
|
370
597
|
method_name[-1] if method_name.end_with?(*ALLOWED_SUFFIXES)
|
|
371
598
|
end
|
|
372
599
|
|
|
373
|
-
|
|
374
|
-
|
|
600
|
+
# Symbol#name returns the symbol's interned frozen string, where
|
|
601
|
+
# Symbol#to_s allocates a new one on every read and write.
|
|
602
|
+
def convert_key(key) # :nodoc:
|
|
603
|
+
return key.name if key.is_a?(Symbol)
|
|
604
|
+
|
|
605
|
+
key.respond_to?(:to_sym) ? key.to_s : key
|
|
375
606
|
end
|
|
376
607
|
|
|
377
|
-
def convert_value(val, duping = false)
|
|
608
|
+
def convert_value(val, duping = false) # :nodoc:
|
|
378
609
|
case val
|
|
379
610
|
when self.class
|
|
380
611
|
val.dup
|
|
@@ -384,7 +615,7 @@ module Hashie
|
|
|
384
615
|
val = val.dup if duping
|
|
385
616
|
self.class.new(val)
|
|
386
617
|
when ::Array
|
|
387
|
-
Array.new(val.
|
|
618
|
+
Array.new(val.length) { |index| convert_value(val[index]) }
|
|
388
619
|
else
|
|
389
620
|
val
|
|
390
621
|
end
|
|
@@ -406,11 +637,12 @@ module Hashie
|
|
|
406
637
|
end
|
|
407
638
|
|
|
408
639
|
def log_collision?(method_key)
|
|
409
|
-
return unless
|
|
640
|
+
return false unless method_key.is_a?(String) || method_key.is_a?(Symbol)
|
|
641
|
+
return false unless respond_to?(method_key)
|
|
410
642
|
|
|
411
643
|
_, suffix = method_name_and_suffix(method_key)
|
|
412
644
|
|
|
413
|
-
(!suffix || suffix == '='
|
|
645
|
+
(!suffix || suffix == '=') &&
|
|
414
646
|
!self.class.disable_warnings?(method_key) &&
|
|
415
647
|
!(regular_key?(method_key) || regular_key?(method_key.to_s))
|
|
416
648
|
end
|
data/lib/hashie/railtie.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
begin
|
|
2
4
|
require 'rails/railtie'
|
|
3
5
|
|
|
@@ -11,7 +13,7 @@ begin
|
|
|
11
13
|
initializer 'hashie.patch_hash_except', after: 'load_active_support' do
|
|
12
14
|
if Rails::VERSION::MAJOR >= 6
|
|
13
15
|
require 'hashie/extensions/active_support/core_ext/hash'
|
|
14
|
-
Hashie::Mash.
|
|
16
|
+
Hashie::Mash.include Hashie::Extensions::ActiveSupport::CoreExt::Hash
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -19,3 +21,27 @@ begin
|
|
|
19
21
|
rescue LoadError => e
|
|
20
22
|
Hashie.logger.info("Hashie skipping railtie as #{e.message}")
|
|
21
23
|
end
|
|
24
|
+
|
|
25
|
+
# @!parse
|
|
26
|
+
# module Hashie
|
|
27
|
+
# # Integrates Hashie's logger and Mash exclusions with Rails.
|
|
28
|
+
# #
|
|
29
|
+
# # @example Run the Rails integration initializers
|
|
30
|
+
# # require 'logger'
|
|
31
|
+
# # require 'rails'
|
|
32
|
+
# # require 'hashie/railtie'
|
|
33
|
+
# # previous_rails_logger = Rails.logger
|
|
34
|
+
# # previous_hashie_logger = Hashie.logger
|
|
35
|
+
# # begin
|
|
36
|
+
# # Rails.logger = Logger.new(IO::NULL)
|
|
37
|
+
# # initializers = Hashie::Railtie.initializers.select { |item| item.name.start_with?('hashie.') }
|
|
38
|
+
# # initializers.each { |initializer| initializer.run(nil) }
|
|
39
|
+
# # Hashie.logger.equal?(Rails.logger) # => true
|
|
40
|
+
# # Hashie::Mash.new(name: 'Ada', age: 37).except(:age).to_h # => {'name' => 'Ada'}
|
|
41
|
+
# # ensure
|
|
42
|
+
# # Rails.logger = previous_rails_logger
|
|
43
|
+
# # Hashie.logger = previous_hashie_logger
|
|
44
|
+
# # end
|
|
45
|
+
# class Railtie < Rails::Railtie
|
|
46
|
+
# end
|
|
47
|
+
# end
|