hashie 3.6.0 → 4.0.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +95 -7
  4. data/UPGRADING.md +78 -2
  5. data/hashie.gemspec +2 -1
  6. data/lib/hashie.rb +20 -19
  7. data/lib/hashie/dash.rb +2 -1
  8. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  9. data/lib/hashie/extensions/coercion.rb +23 -16
  10. data/lib/hashie/extensions/dash/indifferent_access.rb +20 -1
  11. data/lib/hashie/extensions/dash/property_translation.rb +5 -2
  12. data/lib/hashie/extensions/deep_fetch.rb +4 -2
  13. data/lib/hashie/extensions/deep_find.rb +12 -3
  14. data/lib/hashie/extensions/deep_locate.rb +22 -7
  15. data/lib/hashie/extensions/indifferent_access.rb +1 -3
  16. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  17. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  18. data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -1
  19. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  20. data/lib/hashie/extensions/method_access.rb +5 -2
  21. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +9 -4
  22. data/lib/hashie/extensions/strict_key_access.rb +8 -4
  23. data/lib/hashie/hash.rb +16 -9
  24. data/lib/hashie/mash.rb +99 -43
  25. data/lib/hashie/railtie.rb +7 -0
  26. data/lib/hashie/rash.rb +1 -1
  27. data/lib/hashie/version.rb +1 -1
  28. data/spec/hashie/dash_spec.rb +18 -8
  29. data/spec/hashie/extensions/coercion_spec.rb +17 -8
  30. data/spec/hashie/extensions/deep_find_spec.rb +12 -6
  31. data/spec/hashie/extensions/deep_locate_spec.rb +2 -1
  32. data/spec/hashie/extensions/deep_merge_spec.rb +6 -2
  33. data/spec/hashie/extensions/ignore_undeclared_spec.rb +2 -1
  34. data/spec/hashie/extensions/mash/define_accessors_spec.rb +90 -0
  35. data/spec/hashie/extensions/method_access_spec.rb +8 -1
  36. data/spec/hashie/extensions/strict_key_access_spec.rb +9 -10
  37. data/spec/hashie/extensions/symbolize_keys_spec.rb +3 -1
  38. data/spec/hashie/hash_spec.rb +45 -6
  39. data/spec/hashie/mash_spec.rb +314 -8
  40. data/spec/hashie/trash_spec.rb +9 -3
  41. data/spec/integration/elasticsearch/integration_spec.rb +3 -2
  42. data/spec/integration/rails/app.rb +5 -12
  43. data/spec/integration/rails/integration_spec.rb +22 -1
  44. metadata +8 -4
@@ -2,6 +2,7 @@ require 'hashie/hash'
2
2
  require 'hashie/array'
3
3
  require 'hashie/utils'
4
4
  require 'hashie/logger'
5
+ require 'hashie/extensions/key_conflict_warning'
5
6
 
6
7
  module Hashie
7
8
  # Mash allows you to create pseudo-objects that have method-like
@@ -16,8 +17,10 @@ module Hashie
16
17
  # * No punctuation: Returns the value of the hash for that key, or nil if none exists.
17
18
  # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
18
19
  # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
19
- # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
20
- # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key. Used to test existance in deep Mashes.
20
+ # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it
21
+ # as "touch" for mashes.
22
+ # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key.
23
+ # Used to test existance in deep Mashes.
21
24
  #
22
25
  # == Basic Example
23
26
  #
@@ -60,41 +63,10 @@ module Hashie
60
63
  class Mash < Hash
61
64
  include Hashie::Extensions::PrettyInspect
62
65
  include Hashie::Extensions::RubyVersionCheck
66
+ extend Hashie::Extensions::KeyConflictWarning
63
67
 
64
68
  ALLOWED_SUFFIXES = %w[? ! = _].freeze
65
69
 
66
- class CannotDisableMashWarnings < StandardError
67
- def initialize(message = 'You cannot disable warnings on the base Mash class. Please subclass the Mash and disable it in the subclass.')
68
- super(message)
69
- end
70
- end
71
-
72
- # Disable the logging of warnings based on keys conflicting keys/methods
73
- #
74
- # @api semipublic
75
- # @return [void]
76
- def self.disable_warnings
77
- raise CannotDisableMashWarnings if self == Hashie::Mash
78
- @disable_warnings = true
79
- end
80
-
81
- # Checks whether this class disables warnings for conflicting keys/methods
82
- #
83
- # @api semipublic
84
- # @return [Boolean]
85
- def self.disable_warnings?
86
- @disable_warnings ||= false
87
- end
88
-
89
- # Inheritance hook that sets class configuration when inherited.
90
- #
91
- # @api semipublic
92
- # @return [void]
93
- def self.inherited(subclass)
94
- super
95
- subclass.disable_warnings if disable_warnings?
96
- end
97
-
98
70
  def self.load(path, options = {})
99
71
  @_mashes ||= new
100
72
 
@@ -102,7 +74,7 @@ module Hashie
102
74
  raise ArgumentError, "The following file doesn't exist: #{path}" unless File.file?(path)
103
75
 
104
76
  parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
105
- @_mashes[path] = new(parser.perform(path)).freeze
77
+ @_mashes[path] = new(parser.perform(path, options.except(:parser))).freeze
106
78
  end
107
79
 
108
80
  def to_module(mash_method_name = :settings)
@@ -114,6 +86,10 @@ module Hashie
114
86
  end
115
87
  end
116
88
 
89
+ def with_accessors!
90
+ extend Hashie::Extensions::Mash::DefineAccessors
91
+ end
92
+
117
93
  alias to_s inspect
118
94
 
119
95
  # If you pass in an existing hash, it will
@@ -125,6 +101,19 @@ module Hashie
125
101
  default ? super(default) : super(&blk)
126
102
  end
127
103
 
104
+ # Creates a new anonymous subclass with key conflict
105
+ # warnings disabled. You may pass an array of method
106
+ # symbols to restrict the disabled warnings to.
107
+ # Hashie::Mash.quiet.new(hash) all warnings disabled.
108
+ # Hashie::Mash.quiet(:zip).new(hash) only zip warning
109
+ # is disabled.
110
+ def self.quiet(*method_keys)
111
+ @memoized_classes ||= {}
112
+ @memoized_classes[method_keys] ||= Class.new(self) do
113
+ disable_warnings(*method_keys)
114
+ end
115
+ end
116
+
128
117
  class << self; alias [] new; end
129
118
 
130
119
  alias regular_reader []
@@ -183,6 +172,31 @@ module Hashie
183
172
  super(*keys.map { |key| convert_key(key) })
184
173
  end
185
174
 
175
+ # Returns a new instance of the class it was called on, with nil values
176
+ # removed.
177
+ def compact
178
+ self.class.new(super)
179
+ end
180
+
181
+ # Returns a new instance of the class it was called on, using its keys as
182
+ # values, and its values as keys. The new values and keys will always be
183
+ # strings.
184
+ def invert
185
+ self.class.new(super)
186
+ end
187
+
188
+ # Returns a new instance of the class it was called on, containing elements
189
+ # for which the given block returns false.
190
+ def reject(&blk)
191
+ self.class.new(super(&blk))
192
+ end
193
+
194
+ # Returns a new instance of the class it was called on, containing elements
195
+ # for which the given block returns true.
196
+ def select(&blk)
197
+ self.class.new(super(&blk))
198
+ end
199
+
186
200
  alias regular_dup dup
187
201
  # Duplicates the current mash as a new mash.
188
202
  def dup
@@ -202,14 +216,42 @@ module Hashie
202
216
  def deep_merge(other_hash, &blk)
203
217
  dup.deep_update(other_hash, &blk)
204
218
  end
205
- alias merge deep_merge
206
219
 
207
220
  # Recursively merges this mash with the passed
208
221
  # in hash, merging each hash in the hierarchy.
209
222
  def deep_update(other_hash, &blk)
223
+ _deep_update(other_hash, &blk)
224
+ self
225
+ end
226
+
227
+ with_minimum_ruby('2.6.0') do
228
+ # Performs a deep_update on a duplicate of the
229
+ # current mash.
230
+ def deep_merge(*other_hashes, &blk)
231
+ dup.deep_update(*other_hashes, &blk)
232
+ end
233
+
234
+ # Recursively merges this mash with the passed
235
+ # in hash, merging each hash in the hierarchy.
236
+ def deep_update(*other_hashes, &blk)
237
+ other_hashes.each do |other_hash|
238
+ _deep_update(other_hash, &blk)
239
+ end
240
+ self
241
+ end
242
+ end
243
+
244
+ # Alias these lexically so they get the correctly defined
245
+ # #deep_merge and #deep_update based on ruby version.
246
+ alias merge deep_merge
247
+ alias deep_merge! deep_update
248
+ alias update deep_update
249
+ alias merge! update
250
+
251
+ def _deep_update(other_hash, &blk)
210
252
  other_hash.each_pair do |k, v|
211
253
  key = convert_key(k)
212
- if regular_reader(key).is_a?(Mash) && v.is_a?(::Hash)
254
+ if v.is_a?(::Hash) && key?(key) && regular_reader(key).is_a?(Mash)
213
255
  custom_reader(key).deep_update(v, &blk)
214
256
  else
215
257
  value = convert_value(v, true)
@@ -217,11 +259,8 @@ module Hashie
217
259
  custom_writer(key, value, false)
218
260
  end
219
261
  end
220
- self
221
262
  end
222
- alias deep_merge! deep_update
223
- alias update deep_update
224
- alias merge! update
263
+ private :_deep_update
225
264
 
226
265
  # Assigns a value to a key
227
266
  def assign_property(name, value)
@@ -296,6 +335,23 @@ module Hashie
296
335
  end
297
336
  end
298
337
 
338
+ with_minimum_ruby('2.4.0') do
339
+ def transform_values(&blk)
340
+ self.class.new(super(&blk))
341
+ end
342
+ end
343
+
344
+ with_minimum_ruby('2.5.0') do
345
+ def slice(*keys)
346
+ string_keys = keys.map { |key| convert_key(key) }
347
+ self.class.new(super(*string_keys))
348
+ end
349
+
350
+ def transform_keys(&blk)
351
+ self.class.new(super(&blk))
352
+ end
353
+ end
354
+
299
355
  protected
300
356
 
301
357
  def method_name_and_suffix(method_name)
@@ -337,7 +393,7 @@ module Hashie
337
393
  private
338
394
 
339
395
  def log_built_in_message(method_key)
340
- return if self.class.disable_warnings?
396
+ return if self.class.disable_warnings?(method_key)
341
397
 
342
398
  method_information = Hashie::Utils.method_information(method(method_key))
343
399
 
@@ -350,7 +406,7 @@ module Hashie
350
406
  end
351
407
 
352
408
  def log_collision?(method_key)
353
- respond_to?(method_key) && !self.class.disable_warnings? &&
409
+ respond_to?(method_key) && !self.class.disable_warnings?(method_key) &&
354
410
  !(regular_key?(method_key) || regular_key?(method_key.to_s))
355
411
  end
356
412
  end
@@ -7,6 +7,13 @@ begin
7
7
  initializer 'hashie.configure_logger', after: 'initialize_logger' do
8
8
  Hashie.logger = Rails.logger
9
9
  end
10
+
11
+ initializer 'hashie.patch_hash_except', after: 'load_active_support' do
12
+ if Rails::VERSION::MAJOR >= 6
13
+ require 'hashie/extensions/active_support/core_ext/hash'
14
+ Hashie::Mash.send(:include, Hashie::Extensions::ActiveSupport::CoreExt::Hash)
15
+ end
16
+ end
10
17
  end
11
18
  end
12
19
  rescue LoadError => e
@@ -117,7 +117,7 @@ module Hashie
117
117
  end
118
118
 
119
119
  when Regexp
120
- # Reverse operation: `rash[/regexp/]` returns all the hash's string keys which match the regexp
120
+ # Reverse operation: `rash[/regexp/]` returns all string keys matching the regexp
121
121
  @hash.each do |key, val|
122
122
  yield val if key.is_a?(String) && query =~ key
123
123
  end
@@ -1,3 +1,3 @@
1
1
  module Hashie
2
- VERSION = '3.6.0'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
@@ -129,12 +129,14 @@ describe DashTest do
129
129
  context 'writing to properties' do
130
130
  it 'fails writing a required property to nil' do
131
131
  expect { subject.first_name = nil }.to raise_error(*property_required_error('first_name'))
132
- expect { required_message.first_name = nil }.to raise_error(*property_required_custom_error('first_name'))
132
+ expect { required_message.first_name = nil }
133
+ .to raise_error(*property_required_custom_error('first_name'))
133
134
  end
134
135
 
135
136
  it 'fails writing a required property to nil using []=' do
136
137
  expect { subject[:first_name] = nil }.to raise_error(*property_required_error('first_name'))
137
- expect { required_message[:first_name] = nil }.to raise_error(*property_required_custom_error('first_name'))
138
+ expect { required_message[:first_name] = nil }
139
+ .to raise_error(*property_required_custom_error('first_name'))
138
140
  end
139
141
 
140
142
  it 'fails writing to a non-existent property using []=' do
@@ -263,11 +265,13 @@ describe DashTest do
263
265
  end
264
266
 
265
267
  it 'fails with non-existent properties' do
266
- expect { subject.merge(middle_name: 'James') }.to raise_error(*no_property_error('middle_name'))
268
+ expect { subject.merge(middle_name: 'James') }
269
+ .to raise_error(*no_property_error('middle_name'))
267
270
  end
268
271
 
269
272
  it 'errors out when attempting to set a required property to nil' do
270
- expect { subject.merge(first_name: nil) }.to raise_error(*property_required_error('first_name'))
273
+ expect { subject.merge(first_name: nil) }
274
+ .to raise_error(*property_required_error('first_name'))
271
275
  end
272
276
 
273
277
  context 'given a block' do
@@ -366,8 +370,12 @@ describe DashTest do
366
370
  let(:params) { { first_name: 'Alice', email: 'alice@example.com' } }
367
371
 
368
372
  context 'when there is coercion' do
369
- let(:params_before) { { city: 'nyc', person: { first_name: 'Bob', email: 'bob@example.com' } } }
370
- let(:params_after) { { city: 'sfo', person: { first_name: 'Alice', email: 'alice@example.com' } } }
373
+ let(:params_before) do
374
+ { city: 'nyc', person: { first_name: 'Bob', email: 'bob@example.com' } }
375
+ end
376
+ let(:params_after) do
377
+ { city: 'sfo', person: { first_name: 'Alice', email: 'alice@example.com' } }
378
+ end
371
379
 
372
380
  subject { DashWithCoercion.new(params_before) }
373
381
 
@@ -505,7 +513,8 @@ end
505
513
 
506
514
  describe ConditionallyRequiredTest do
507
515
  it 'does not allow a conditionally required property to be set to nil if required' do
508
- expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: nil) }.to raise_error(ArgumentError, "The property 'password' must be set, too.")
516
+ expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: nil) }
517
+ .to raise_error(ArgumentError, "The property 'password' must be set, too.")
509
518
  end
510
519
 
511
520
  it 'allows a conditionally required property to be set to nil if not required' do
@@ -513,7 +522,8 @@ describe ConditionallyRequiredTest do
513
522
  end
514
523
 
515
524
  it 'allows a conditionally required property to be set if required' do
516
- expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: '$ecure!') }.not_to raise_error
525
+ expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: '$ecure!') }
526
+ .not_to raise_error
517
527
  end
518
528
  end
519
529
 
@@ -283,7 +283,8 @@ describe Hashie::Extensions::Coercion do
283
283
 
284
284
  it 'raises errors for non-coercable types' do
285
285
  subject.coerce_key :foo, NotInitializable
286
- expect { instance[:foo] = 'true' }.to raise_error(Hashie::CoercionError, /NotInitializable is not a coercable type/)
286
+ expect { instance[:foo] = 'true' }
287
+ .to raise_error(Hashie::CoercionError, /NotInitializable is not a coercable type/)
287
288
  end
288
289
 
289
290
  it 'can coerce false' do
@@ -458,8 +459,12 @@ describe Hashie::Extensions::Coercion do
458
459
  coerce_key :categories, Array[CategoryHash]
459
460
  end
460
461
 
461
- let(:category) { CategoryHash.new(type: 'rubygem', products: [Hashie::Mash.new(name: 'Hashie')]) }
462
- let(:product) { ProductHash.new(name: 'Hashie', categories: [Hashie::Mash.new(type: 'rubygem')]) }
462
+ let(:category) do
463
+ CategoryHash.new(type: 'rubygem', products: [Hashie::Mash.new(name: 'Hashie')])
464
+ end
465
+ let(:product) do
466
+ ProductHash.new(name: 'Hashie', categories: [Hashie::Mash.new(type: 'rubygem')])
467
+ end
463
468
 
464
469
  it 'coerces CategoryHash[:products] correctly' do
465
470
  expected = [ProductHash]
@@ -559,22 +564,26 @@ describe Hashie::Extensions::Coercion do
559
564
 
560
565
  it 'raises a CoercionError when coercion is not possible' do
561
566
  type =
562
- if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0')
567
+ if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >=
568
+ Hashie::Extensions::RubyVersion.new('2.4.0')
563
569
  Integer
564
570
  else
565
- Fixnum # rubocop:disable Lint/UnifiedInteger
571
+ Fixnum
566
572
  end
567
573
 
568
574
  subject.coerce_value type, Symbol
569
- expect { instance[:hi] = 1 }.to raise_error(Hashie::CoercionError, /Cannot coerce property :hi from #{type} to Symbol/)
575
+ expect { instance[:hi] = 1 }.to raise_error(
576
+ Hashie::CoercionError, /Cannot coerce property :hi from #{type} to Symbol/
577
+ )
570
578
  end
571
579
 
572
580
  it 'coerces Integer to String' do
573
581
  type =
574
- if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0')
582
+ if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >=
583
+ Hashie::Extensions::RubyVersion.new('2.4.0')
575
584
  Integer
576
585
  else
577
- Fixnum # rubocop:disable Lint/UnifiedInteger
586
+ Fixnum
578
587
  end
579
588
 
580
589
  subject.coerce_value type, String
@@ -36,7 +36,8 @@ describe Hashie::Extensions::DeepFind do
36
36
 
37
37
  describe '#deep_find_all' do
38
38
  it 'detects all values from a nested hash' do
39
- expect(instance.deep_find_all(:title)).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
39
+ expect(instance.deep_find_all(:title))
40
+ .to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
40
41
  end
41
42
 
42
43
  it 'returns nil if it does not find any matches' do
@@ -64,7 +65,8 @@ describe Hashie::Extensions::DeepFind do
64
65
  end
65
66
 
66
67
  it 'detects all values from a nested hash' do
67
- expect(instance.deep_find_all(:title)).to eq([{ type: :string }, 'Call of the Wild', 'Moby Dick', 'Main Library'])
68
+ expect(instance.deep_find_all(:title))
69
+ .to eq([{ type: :string }, 'Call of the Wild', 'Moby Dick', 'Main Library'])
68
70
  end
69
71
  end
70
72
  end
@@ -91,8 +93,10 @@ describe Hashie::Extensions::DeepFind do
91
93
 
92
94
  describe '#deep_find_all' do
93
95
  it 'indifferently detects all values from a nested hash' do
94
- expect(instance.deep_find_all(:title)).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
95
- expect(instance.deep_find_all('title')).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
96
+ expect(instance.deep_find_all(:title))
97
+ .to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
98
+ expect(instance.deep_find_all('title'))
99
+ .to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
96
100
  end
97
101
 
98
102
  it 'indifferently returns nil if it does not find any matches' do
@@ -125,8 +129,10 @@ describe Hashie::Extensions::DeepFind do
125
129
 
126
130
  describe '#deep_find_all' do
127
131
  it 'indifferently detects all values from a nested hash' do
128
- expect(instance.deep_find_all(:title)).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
129
- expect(instance.deep_find_all('title')).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
132
+ expect(instance.deep_find_all(:title))
133
+ .to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
134
+ expect(instance.deep_find_all('title'))
135
+ .to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
130
136
  end
131
137
 
132
138
  it 'indifferently returns nil if it does not find any matches' do
@@ -56,7 +56,8 @@ describe Hashie::Extensions::DeepLocate do
56
56
  describe '.deep_locate' do
57
57
  context 'if called with a non-callable comparator' do
58
58
  it 'creates a key comparator on-th-fly' do
59
- expect(described_class.deep_locate(:lsr10, hash)).to eq([hash[:query][:bool][:must_not][0][:range]])
59
+ expect(described_class.deep_locate(:lsr10, hash))
60
+ .to eq([hash[:query][:bool][:must_not][0][:range]])
60
61
  end
61
62
  end
62
63
 
@@ -13,9 +13,13 @@ describe Hashie::Extensions::DeepMerge do
13
13
  end
14
14
 
15
15
  context 'without &block' do
16
- let(:h1) { subject.new.merge(a: 'a', a1: 42, b: 'b', c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }) }
16
+ let(:h1) do
17
+ subject.new.merge(a: 'a', a1: 42, b: 'b', c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } })
18
+ end
17
19
  let(:h2) { { a: 1, a1: 1, c: { c1: 2, c2: 'c2', c3: { d2: 'd2' } }, e: { e1: 1 } } }
18
- let(:expected_hash) { { a: 1, a1: 1, b: 'b', c: { c1: 2, c2: 'c2', c3: { d1: 'd1', d2: 'd2' } }, e: { e1: 1 } } }
20
+ let(:expected_hash) do
21
+ { a: 1, a1: 1, b: 'b', c: { c1: 2, c2: 'c2', c3: { d1: 'd1', d2: 'd2' } }, e: { e1: 1 } }
22
+ end
19
23
 
20
24
  it 'deep merges two hashes' do
21
25
  expect(h1.deep_merge(h2)).to eq expected_hash