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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. 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 (<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.
24
- # Used to test existance in deep Mashes.
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, "The following file doesn't exist: #{path}" unless File.file?(path)
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; 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
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
- # any key passed in to a string before retrieving.
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. Key will be converted to
134
- # a string before it is set, and Hashes will be converted
135
- # into Mashes for nesting purposes.
136
- def custom_writer(key, value, convert = true) #:nodoc:
137
- key_as_symbol = (key = convert_key(key)).to_sym
138
-
139
- log_built_in_message(key_as_symbol) if log_collision?(key_as_symbol)
140
- regular_writer(key, convert ? convert_value(value) : value)
141
- end
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(&blk))
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(&blk))
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
- if with_minimum_ruby?('2.6.0')
211
- # Performs a deep_update on a duplicate of the
212
- # current mash.
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
- # Recursively merges this mash with the passed
218
- # in hash, merging each hash in the hierarchy.
219
- def deep_update(*other_hashes, &blk)
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
- # Recursively merges this mash with the passed
233
- # in hash, merging each hash in the hierarchy.
234
- 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|
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
- 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)
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 '='.freeze
505
+ when '='
306
506
  assign_property(name, args.first)
307
- when '?'.freeze
507
+ when '?'
308
508
  !!self[name]
309
- when '!'.freeze
509
+ when '!'
310
510
  initializing_reader(name)
311
- when '_'.freeze
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
- with_minimum_ruby('2.3.0') do
329
- def dig(*keys)
330
- super(*keys.map { |key| convert_key(key) })
331
- end
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
- with_minimum_ruby('2.4.0') do
335
- def transform_values(&blk)
336
- self.class.new(super(&blk))
337
- end
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
- # Returns a new instance of the class it was called on, with nil values
340
- # removed.
341
- def compact
342
- self.class.new(super)
343
- end
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
- with_minimum_ruby('2.5.0') do
347
- def slice(*keys)
348
- string_keys = keys.map { |key| convert_key(key) }
349
- self.class.new(super(*string_keys))
350
- 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
351
566
 
352
- def transform_keys(&blk)
353
- self.class.new(super(&blk))
354
- 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)
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[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:
374
- key.to_s
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) #: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,11 +637,12 @@ module Hashie
406
637
  end
407
638
 
408
639
  def log_collision?(method_key)
409
- 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)
410
642
 
411
643
  _, suffix = method_name_and_suffix(method_key)
412
644
 
413
- (!suffix || suffix == '='.freeze) &&
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
@@ -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