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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CONTRIBUTING.md +38 -1
  4. data/README.md +816 -181
  5. data/Rakefile +29 -1
  6. data/UPGRADING.md +18 -0
  7. data/hashie.gemspec +8 -7
  8. data/lib/hashie/array.rb +15 -12
  9. data/lib/hashie/clash.rb +59 -4
  10. data/lib/hashie/dash.rb +144 -16
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  12. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  13. data/lib/hashie/extensions/block_access.rb +71 -0
  14. data/lib/hashie/extensions/coercion.rb +180 -19
  15. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
  17. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  18. data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
  19. data/lib/hashie/extensions/dash/property_translation.rb +134 -16
  20. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  21. data/lib/hashie/extensions/deep_find.rb +20 -0
  22. data/lib/hashie/extensions/deep_grep.rb +69 -0
  23. data/lib/hashie/extensions/deep_locate.rb +15 -28
  24. data/lib/hashie/extensions/deep_merge.rb +16 -0
  25. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  26. data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
  27. data/lib/hashie/extensions/indifferent_access.rb +100 -14
  28. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  29. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  30. data/lib/hashie/extensions/key_conversion.rb +9 -0
  31. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  32. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  33. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  34. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  35. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  36. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  37. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
  38. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  39. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  40. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  41. data/lib/hashie/extensions/method_access.rb +93 -7
  42. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  43. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  44. data/lib/hashie/extensions/persistable.rb +137 -0
  45. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  46. data/lib/hashie/extensions/ruby_version.rb +18 -1
  47. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  48. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  49. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  50. data/lib/hashie/extensions/symbolize_keys.rb +72 -16
  51. data/lib/hashie/hash.rb +16 -0
  52. data/lib/hashie/logger.rb +18 -1
  53. data/lib/hashie/mash.rb +293 -62
  54. data/lib/hashie/railtie.rb +27 -1
  55. data/lib/hashie/rash.rb +64 -4
  56. data/lib/hashie/trash.rb +8 -0
  57. data/lib/hashie/utils.rb +17 -6
  58. data/lib/hashie/version.rb +3 -1
  59. data/lib/hashie.rb +45 -0
  60. 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 (<tt>=</tt>): Sets the attribute of the given method name.
19
- # * Truthiness (<tt>?</tt>): Returns true or false depending on the truthiness of
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 (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it
23
+ # * Bang (`!`): Forces the existence of this key, used for deep Mashes. Think of it
22
24
  # as "touch" for mashes.
23
- # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key.
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, "The following file doesn't exist: #{path}" unless File.file?(path)
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; alias [] new; end
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
- def custom_writer(key, value, convert = true) #:nodoc:
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(&blk))
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(&blk))
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
- if with_minimum_ruby?('2.6.0')
208
- # Performs a deep_update on a duplicate of the
209
- # current mash.
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
- # Recursively merges this mash with the passed
215
- # in hash, merging each hash in the hierarchy.
216
- def deep_update(*other_hashes, &blk)
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
- # Recursively merges this mash with the passed
230
- # in hash, merging each hash in the hierarchy.
231
- def deep_update(other_hash, &blk)
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
- def method_missing(method_name, *args, &blk) # rubocop:disable Style/MethodMissing
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 '='.freeze
505
+ when '='
303
506
  assign_property(name, args.first)
304
- when '?'.freeze
507
+ when '?'
305
508
  !!self[name]
306
- when '!'.freeze
509
+ when '!'
307
510
  initializing_reader(name)
308
- when '_'.freeze
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(&blk))
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
- with_minimum_ruby('2.5.0') do
340
- def slice(*keys)
341
- string_keys = keys.map { |key| convert_key(key) }
342
- self.class.new(super(*string_keys))
343
- end
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
- def transform_keys(&blk)
346
- self.class.new(super(&blk))
347
- end
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
- with_minimum_ruby('3.0.0') do
351
- def except(*keys)
352
- string_keys = keys.map { |key| convert_key(key) }
353
- self.class.new(super(*string_keys))
354
- end
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[0..-1], nil]
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
- def convert_key(key) #:nodoc:
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) #:nodoc:
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.map { |e| convert_value(e) })
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 == '='.freeze) &&
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
@@ -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.send(:include, Hashie::Extensions::ActiveSupport::CoreExt::Hash)
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