hashie 5.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 +56 -0
- data/CONTRIBUTING.md +38 -1
- data/README.md +816 -181
- data/Rakefile +29 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +8 -7
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +144 -16
- 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 -19
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
- data/lib/hashie/extensions/dash/property_translation.rb +134 -16
- 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 +15 -28
- 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 +21 -3
- data/lib/hashie/extensions/indifferent_access.rb +100 -14
- 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 +6 -2
- 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 +93 -7
- 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 +72 -16
- data/lib/hashie/hash.rb +16 -0
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +293 -62
- 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 +45 -0
- metadata +15 -17
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,12 +17,12 @@ 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 (
|
|
25
|
+
# * Under Bang (`_`): Like Bang, but returns a new Mash rather than creating a key.
|
|
24
26
|
# Used to test existence in deep Mashes.
|
|
25
27
|
#
|
|
26
28
|
# == Basic Example
|
|
@@ -61,23 +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::RubyVersionCheck
|
|
66
71
|
extend Hashie::Extensions::KeyConflictWarning
|
|
67
72
|
|
|
68
73
|
ALLOWED_SUFFIXES = %w[? ! = _].freeze
|
|
69
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
|
|
70
92
|
def self.load(path, options = {})
|
|
71
93
|
@_mashes ||= new
|
|
72
94
|
|
|
73
95
|
return @_mashes[path] if @_mashes.key?(path)
|
|
74
|
-
raise ArgumentError, "
|
|
96
|
+
raise ArgumentError, "file not found: `#{path}`" unless File.file?(path)
|
|
75
97
|
|
|
76
98
|
options = options.dup
|
|
77
99
|
parser = options.delete(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
|
|
78
100
|
@_mashes[path] = new(parser.perform(path, options)).freeze
|
|
79
101
|
end
|
|
80
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'
|
|
81
107
|
def to_module(mash_method_name = :settings)
|
|
82
108
|
mash = self
|
|
83
109
|
Module.new do |m|
|
|
@@ -87,16 +113,28 @@ module Hashie
|
|
|
87
113
|
end
|
|
88
114
|
end
|
|
89
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
|
|
90
121
|
def with_accessors!
|
|
91
122
|
extend Hashie::Extensions::Mash::DefineAccessors
|
|
92
123
|
end
|
|
93
124
|
|
|
125
|
+
# @example Inspect the Mash's attributes
|
|
126
|
+
# Hashie::Mash.new(name: 'Ada').to_s # => '#<Hashie::Mash name="Ada">'
|
|
94
127
|
alias to_s inspect
|
|
95
128
|
|
|
96
129
|
# If you pass in an existing hash, it will
|
|
97
130
|
# convert it to a Mash including recursively
|
|
98
131
|
# descending into arrays and hashes, converting
|
|
99
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
|
|
100
138
|
def initialize(source_hash = nil, default = nil, &blk)
|
|
101
139
|
deep_update(source_hash) if source_hash
|
|
102
140
|
default ? super(default) : super(&blk)
|
|
@@ -108,6 +146,11 @@ module Hashie
|
|
|
108
146
|
# Hashie::Mash.quiet.new(hash) all warnings disabled.
|
|
109
147
|
# Hashie::Mash.quiet(:zip).new(hash) only zip warning
|
|
110
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
|
|
111
154
|
def self.quiet(*method_keys)
|
|
112
155
|
@memoized_classes ||= {}
|
|
113
156
|
@memoized_classes[method_keys] ||= Class.new(self) do
|
|
@@ -115,13 +158,33 @@ module Hashie
|
|
|
115
158
|
end
|
|
116
159
|
end
|
|
117
160
|
|
|
118
|
-
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
|
|
119
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
|
|
120
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' }
|
|
121
178
|
alias regular_writer []=
|
|
122
179
|
|
|
123
180
|
# Retrieves an attribute set in the Mash. Will convert a key passed in
|
|
124
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']
|
|
125
188
|
def custom_reader(key)
|
|
126
189
|
default_proc.call(self, key) if default_proc && !key?(key)
|
|
127
190
|
value = regular_reader(convert_key(key))
|
|
@@ -132,16 +195,36 @@ module Hashie
|
|
|
132
195
|
# Sets an attribute in the Mash. Symbol keys will be converted to
|
|
133
196
|
# strings before being set, and Hashes will be converted into Mashes
|
|
134
197
|
# for nesting purposes.
|
|
135
|
-
|
|
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:
|
|
136
205
|
log_built_in_message(key) if key.respond_to?(:to_sym) && log_collision?(key.to_sym)
|
|
137
206
|
regular_writer(convert_key(key), convert ? convert_value(value) : value)
|
|
138
207
|
end
|
|
139
208
|
|
|
209
|
+
# @example Read symbol and string keys interchangeably
|
|
210
|
+
# mash = Hashie::Mash.new(name: 'Ada')
|
|
211
|
+
# mash[:name] # => 'Ada'
|
|
212
|
+
# mash['name'] # => 'Ada'
|
|
140
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'
|
|
141
219
|
alias []= custom_writer
|
|
142
220
|
|
|
143
221
|
# This is the bang method reader, it will return a new Mash
|
|
144
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'
|
|
145
228
|
def initializing_reader(key)
|
|
146
229
|
ck = convert_key(key)
|
|
147
230
|
regular_writer(ck, self.class.new) unless key?(ck)
|
|
@@ -150,6 +233,11 @@ module Hashie
|
|
|
150
233
|
|
|
151
234
|
# This is the under bang method reader, it will return a temporary new Mash
|
|
152
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
|
|
153
241
|
def underbang_reader(key)
|
|
154
242
|
ck = convert_key(key)
|
|
155
243
|
if key?(ck)
|
|
@@ -159,14 +247,24 @@ module Hashie
|
|
|
159
247
|
end
|
|
160
248
|
end
|
|
161
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'
|
|
162
254
|
def fetch(key, *args)
|
|
163
255
|
super(convert_key(key), *args)
|
|
164
256
|
end
|
|
165
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
|
|
166
262
|
def delete(key)
|
|
167
263
|
super(convert_key(key))
|
|
168
264
|
end
|
|
169
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]
|
|
170
268
|
def values_at(*keys)
|
|
171
269
|
super(*keys.map { |key| convert_key(key) })
|
|
172
270
|
end
|
|
@@ -174,71 +272,139 @@ module Hashie
|
|
|
174
272
|
# Returns a new instance of the class it was called on, using its keys as
|
|
175
273
|
# values, and its values as keys. The new values and keys will always be
|
|
176
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
|
|
177
280
|
def invert
|
|
178
281
|
self.class.new(super)
|
|
179
282
|
end
|
|
180
283
|
|
|
181
284
|
# Returns a new instance of the class it was called on, containing elements
|
|
182
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
|
|
183
291
|
def reject(&blk)
|
|
184
|
-
self.class.new(super
|
|
292
|
+
self.class.new(super)
|
|
185
293
|
end
|
|
186
294
|
|
|
187
295
|
# Returns a new instance of the class it was called on, containing elements
|
|
188
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
|
|
189
302
|
def select(&blk)
|
|
190
|
-
self.class.new(super
|
|
303
|
+
self.class.new(super)
|
|
191
304
|
end
|
|
192
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
|
|
193
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
|
+
|
|
194
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'
|
|
195
326
|
def dup
|
|
327
|
+
default = HASH_DEFAULT.bind_call(self)
|
|
328
|
+
default_proc = HASH_DEFAULT_PROC.bind_call(self)
|
|
196
329
|
self.class.new(self, default, &default_proc)
|
|
197
330
|
end
|
|
198
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
|
|
199
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
|
|
200
342
|
def key?(key)
|
|
201
343
|
super(convert_key(key))
|
|
202
344
|
end
|
|
345
|
+
# @example Test key presence even for a nil value
|
|
346
|
+
# Hashie::Mash.new(name: nil).has_key?(:name) # => true
|
|
203
347
|
alias has_key? key?
|
|
204
|
-
alias include? key?
|
|
205
|
-
alias member? key?
|
|
206
348
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def deep_merge(*other_hashes, &blk)
|
|
211
|
-
dup.deep_update(*other_hashes, &blk)
|
|
212
|
-
end
|
|
349
|
+
# @example Test membership with a symbol key
|
|
350
|
+
# Hashie::Mash.new(name: 'Ada').include?(:name) # => true
|
|
351
|
+
alias include? key?
|
|
213
352
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
other_hashes.each do |other_hash|
|
|
218
|
-
_deep_update(other_hash, &blk)
|
|
219
|
-
end
|
|
220
|
-
self
|
|
221
|
-
end
|
|
222
|
-
else
|
|
223
|
-
# Performs a deep_update on a duplicate of the
|
|
224
|
-
# current mash.
|
|
225
|
-
def deep_merge(other_hash, &blk)
|
|
226
|
-
dup.deep_update(other_hash, &blk)
|
|
227
|
-
end
|
|
353
|
+
# @example Test membership with a string key
|
|
354
|
+
# Hashie::Mash.new(name: 'Ada').member?('name') # => true
|
|
355
|
+
alias member? key?
|
|
228
356
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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|
|
|
232
378
|
_deep_update(other_hash, &blk)
|
|
233
|
-
self
|
|
234
379
|
end
|
|
380
|
+
self
|
|
235
381
|
end
|
|
236
382
|
|
|
237
383
|
# Alias these lexically so they get the correctly defined
|
|
238
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 }
|
|
239
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 }
|
|
240
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'
|
|
241
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 }
|
|
242
408
|
alias merge! update
|
|
243
409
|
|
|
244
410
|
def _deep_update(other_hash, &blk)
|
|
@@ -256,17 +422,32 @@ module Hashie
|
|
|
256
422
|
private :_deep_update
|
|
257
423
|
|
|
258
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'
|
|
259
430
|
def assign_property(name, value)
|
|
260
431
|
self[name] = value
|
|
261
432
|
end
|
|
262
433
|
|
|
263
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
|
|
264
440
|
def shallow_merge(other_hash)
|
|
265
441
|
dup.shallow_update(other_hash)
|
|
266
442
|
end
|
|
267
443
|
|
|
268
444
|
# Merges (non-recursively) the hash from the argument,
|
|
269
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' }
|
|
270
451
|
def shallow_update(other_hash)
|
|
271
452
|
other_hash.each_pair do |k, v|
|
|
272
453
|
regular_writer(convert_key(k), convert_value(v, true))
|
|
@@ -274,14 +455,24 @@ module Hashie
|
|
|
274
455
|
self
|
|
275
456
|
end
|
|
276
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' }
|
|
277
462
|
def replace(other_hash)
|
|
278
463
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
279
464
|
other_hash.each { |key, value| self[key] = value }
|
|
280
465
|
self
|
|
281
466
|
end
|
|
282
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
|
|
283
473
|
def respond_to_missing?(method_name, *args)
|
|
284
474
|
return true if key?(method_name)
|
|
475
|
+
|
|
285
476
|
suffix = method_suffix(method_name)
|
|
286
477
|
if suffix
|
|
287
478
|
true
|
|
@@ -290,22 +481,34 @@ module Hashie
|
|
|
290
481
|
end
|
|
291
482
|
end
|
|
292
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
|
|
293
488
|
def prefix_method?(method_name)
|
|
294
489
|
method_name = method_name.to_s
|
|
295
490
|
method_name.end_with?(*ALLOWED_SUFFIXES) && key?(method_name.chop)
|
|
296
491
|
end
|
|
297
492
|
|
|
298
|
-
|
|
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)
|
|
299
501
|
return self.[](method_name, &blk) if key?(method_name)
|
|
502
|
+
|
|
300
503
|
name, suffix = method_name_and_suffix(method_name)
|
|
301
504
|
case suffix
|
|
302
|
-
when '='
|
|
505
|
+
when '='
|
|
303
506
|
assign_property(name, args.first)
|
|
304
|
-
when '?'
|
|
507
|
+
when '?'
|
|
305
508
|
!!self[name]
|
|
306
|
-
when '!'
|
|
509
|
+
when '!'
|
|
307
510
|
initializing_reader(name)
|
|
308
|
-
when '_'
|
|
511
|
+
when '_'
|
|
309
512
|
underbang_reader(name)
|
|
310
513
|
else
|
|
311
514
|
self[method_name]
|
|
@@ -313,45 +516,69 @@ module Hashie
|
|
|
313
516
|
end
|
|
314
517
|
|
|
315
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
|
|
316
522
|
def extractable_options?
|
|
317
523
|
true
|
|
318
524
|
end
|
|
319
525
|
|
|
320
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 }
|
|
321
531
|
def reverse_merge(other_hash)
|
|
322
532
|
self.class.new(other_hash).merge(self)
|
|
323
533
|
end
|
|
324
534
|
|
|
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
|
|
325
539
|
def dig(*keys)
|
|
326
540
|
super(*keys.map { |key| convert_key(key) })
|
|
327
541
|
end
|
|
328
542
|
|
|
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
|
|
329
547
|
def transform_values(&blk)
|
|
330
|
-
self.class.new(super
|
|
548
|
+
self.class.new(super)
|
|
331
549
|
end
|
|
332
550
|
|
|
333
551
|
# Returns a new instance of the class it was called on, with nil values
|
|
334
552
|
# removed.
|
|
553
|
+
#
|
|
554
|
+
# @example Remove nils while preserving false
|
|
555
|
+
# Hashie::Mash.new(name: nil, active: false).compact.to_hash # => { 'active' => false }
|
|
335
556
|
def compact
|
|
336
557
|
self.class.new(super)
|
|
337
558
|
end
|
|
338
559
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
344
566
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
348
573
|
end
|
|
349
574
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
+
|
|
374
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,12 +637,12 @@ module Hashie
|
|
|
406
637
|
end
|
|
407
638
|
|
|
408
639
|
def log_collision?(method_key)
|
|
409
|
-
return unless method_key.is_a?(String) || method_key.is_a?(Symbol)
|
|
410
|
-
return unless respond_to?(method_key)
|
|
640
|
+
return false unless method_key.is_a?(String) || method_key.is_a?(Symbol)
|
|
641
|
+
return false unless respond_to?(method_key)
|
|
411
642
|
|
|
412
643
|
_, suffix = method_name_and_suffix(method_key)
|
|
413
644
|
|
|
414
|
-
(!suffix || suffix == '='
|
|
645
|
+
(!suffix || suffix == '=') &&
|
|
415
646
|
!self.class.disable_warnings?(method_key) &&
|
|
416
647
|
!(regular_key?(method_key) || regular_key?(method_key.to_s))
|
|
417
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
|