hashie 3.6.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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