diff options
-rw-r--r-- | .rubocop_todo.yml | 6 | ||||
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | UPGRADING.md | 41 | ||||
-rw-r--r-- | lib/hashie/mash.rb | 77 | ||||
-rw-r--r-- | spec/hashie/mash_spec.rb | 162 |
5 files changed, 279 insertions, 10 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ab2db99..bc15a46 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-07-17 09:23:49 -0400 using RuboCop version 0.52.1. +# on 2019-08-13 23:33:30 -0400 using RuboCop version 0.52.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,13 +13,13 @@ Metrics/AbcSize: # Offense count: 2 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 221 + Max: 266 # Offense count: 7 Metrics/CyclomaticComplexity: Max: 11 -# Offense count: 19 +# Offense count: 18 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 28 diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b6170..fd227bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,12 @@ scheme are considered to be bugs. * [#323](https://github.com/intridea/hashie/pull/323): Added `Hashie::Extensions::Mash::DefineAccessors` - [@marshall-lee](https://github.com/marshall-lee). * [#474](https://github.com/intridea/hashie/pull/474): Expose `YAML#safe_load` options in `Mash#load` - [@riouruma](https://github.com/riouruma), [@dblock](https://github.com/dblock). * [#478](https://github.com/intridea/hashie/pull/478): Added optional array parameter to `Hashie::Mash.disable_warnings` - [@bobbymcwho](https://github.com/bobbymcwho). +* [#481](https://github.com/intridea/hashie/pull/481): Ruby 2.6 - Support Hash#merge and #merge! called with multiple Hashes/Mashes - [@bobbymcwho](https://github.com/bobbymcwho). +* Your contribution here. ### Changed +* [#481](https://github.com/intridea/hashie/pull/481): Implement non-destructive standard Hash methods - [@bobbymcwho](https://github.com/bobbymcwho). * Your contribution here. ### Deprecated diff --git a/UPGRADING.md b/UPGRADING.md index 6c8ac93..ce01eca 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,45 @@ Upgrading Hashie ================ +### Upgrading to 4.0.0 + +#### Non-destructive Hash methods called on Mash + +The following non-destructive Hash methods called on Mash will now return an instance of the class it was called on. + +| method | ruby | +| ----------------- | ---- | +| #compact | | +| #invert | | +| #reject | | +| #select | | +| #slice | 2.5 | +| #transform_keys | 2.5 | +| #transform_values | 2.4 | + +```ruby +class Parents < Hashie::Mash; end + +parents = Parents.new(father: 'Dad', mother: 'Mom') +cool_parents = parents.transform_values { |v| v + v[-1] + 'io'} + +p cool_parents + +# before: +{"father"=>"Daddio", "mother"=>"Mommio"} + => {"father"=>"Daddio", "mother"=>"Mommio"} + +# after: +#<Parents father="Daddio" mother="Mommio"> +=> {"father"=>"Dad", "mother"=>"Mom"} +``` + +This may make places where you had to re-make the Mash redundant, and may cause unintended side effects if your application was expecting a plain old ruby Hash. + +### Ruby 2.6: Mash#merge and Mash#merge! + +In Ruby > 2.6.0, Hashie now supports passing multiple hash and Mash objects to Mash#merge and Mash#merge!. + ### Upgrading to 3.7.0 #### Mash#load takes options @@ -200,5 +239,3 @@ instance.to_hash # => { :first => 'First', "last" => 'Last' } The behavior with `symbolize_keys` and `stringify_keys` is unchanged. See [#152](https://github.com/intridea/hashie/pull/152) for more information. - - diff --git a/lib/hashie/mash.rb b/lib/hashie/mash.rb index 06cef59..888b4f6 100644 --- a/lib/hashie/mash.rb +++ b/lib/hashie/mash.rb @@ -207,6 +207,31 @@ module Hashie super(*keys.map { |key| convert_key(key) }) end + # Returns a new instance of the class it was called on, with nil values + # removed. + def compact + self.class.new(super) + end + + # Returns a new instance of the class it was called on, using its keys as + # values, and its values as keys. The new values and keys will always be + # strings. + def invert + self.class.new(super) + end + + # Returns a new instance of the class it was called on, containing elements + # for which the given block returns false. + def reject(&blk) + self.class.new(super(&blk)) + end + + # Returns a new instance of the class it was called on, containing elements + # for which the given block returns true. + def select(&blk) + self.class.new(super(&blk)) + end + alias regular_dup dup # Duplicates the current mash as a new mash. def dup @@ -226,11 +251,39 @@ module Hashie def deep_merge(other_hash, &blk) dup.deep_update(other_hash, &blk) end - alias merge deep_merge # Recursively merges this mash with the passed # in hash, merging each hash in the hierarchy. def deep_update(other_hash, &blk) + _deep_update(other_hash, &blk) + self + end + + with_minimum_ruby('2.6.0') do + # Performs a deep_update on a duplicate of the + # current mash. + def deep_merge(*other_hashes, &blk) + dup.deep_update(*other_hashes, &blk) + end + + # Recursively merges this mash with the passed + # in hash, merging each hash in the hierarchy. + def deep_update(*other_hashes, &blk) + other_hashes.each do |other_hash| + _deep_update(other_hash, &blk) + end + self + end + end + + # Alias these lexically so they get the correctly defined + # #deep_merge and #deep_update based on ruby version. + alias merge deep_merge + alias deep_merge! deep_update + alias update deep_update + alias merge! update + + def _deep_update(other_hash, &blk) other_hash.each_pair do |k, v| key = convert_key(k) if v.is_a?(::Hash) && key?(key) && regular_reader(key).is_a?(Mash) @@ -241,11 +294,8 @@ module Hashie custom_writer(key, value, false) end end - self end - alias deep_merge! deep_update - alias update deep_update - alias merge! update + private :_deep_update # Assigns a value to a key def assign_property(name, value) @@ -320,6 +370,23 @@ module Hashie end end + with_minimum_ruby('2.4.0') do + def transform_values(&blk) + self.class.new(super(&blk)) + end + end + + with_minimum_ruby('2.5.0') do + def slice(*keys) + string_keys = keys.map { |key| convert_key(key) } + self.class.new(super(*string_keys)) + end + + def transform_keys(&blk) + self.class.new(super(&blk)) + end + end + protected def method_name_and_suffix(method_name) diff --git a/spec/hashie/mash_spec.rb b/spec/hashie/mash_spec.rb index 3bcb478..e2a178c 100644 --- a/spec/hashie/mash_spec.rb +++ b/spec/hashie/mash_spec.rb @@ -878,6 +878,90 @@ describe Hashie::Mash do end end + describe '#compact' do + subject(:mash) { described_class.new(a: 1, b: nil) } + + it 'returns a Hashie::Mash' do + expect(mash.compact).to be_kind_of(described_class) + end + + it 'removes keys with nil values' do + expect(mash.compact).to eq('a' => 1) + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: nil) } + + it 'creates an instance of subclass' do + expect(sub_mash.compact).to be_kind_of(subclass) + end + end + end + + describe '#invert' do + subject(:mash) { described_class.new(a: 'apple', b: 4) } + + it 'returns a Hashie::Mash' do + expect(mash.invert).to be_kind_of(described_class) + end + + it 'returns a mash with the keys and values inverted' do + expect(mash.invert).to eq('apple' => 'a', '4' => 'b') + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: nil) } + + it 'creates an instance of subclass' do + expect(sub_mash.invert).to be_kind_of(subclass) + end + end + end + + describe '#reject' do + subject(:mash) { described_class.new(a: 1, b: nil) } + + it 'returns a Hashie::Mash' do + expect(mash.reject { |_k, v| v.nil? }).to be_kind_of(described_class) + end + + it 'rejects keys for which the block returns true' do + expect(mash.reject { |_k, v| v.nil? }).to eq('a' => 1) + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: nil) } + + it 'creates an instance of subclass' do + expect(sub_mash.reject { |_k, v| v.nil? }).to be_kind_of(subclass) + end + end + end + + describe '#select' do + subject(:mash) { described_class.new(a: 'apple', b: 4) } + + it 'returns a Hashie::Mash' do + expect(mash.select { |_k, v| v.is_a? String }).to be_kind_of(described_class) + end + + it 'selects keys for which the block returns true' do + expect(mash.select { |_k, v| v.is_a? String }).to eq('a' => 'apple') + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: nil) } + + it 'creates an instance of subclass' do + expect(sub_mash.select { |_k, v| v.is_a? String }).to be_kind_of(subclass) + end + end + end + with_minimum_ruby('2.3.0') do describe '#dig' do subject { described_class.new(a: { b: 1 }) } @@ -895,4 +979,82 @@ describe Hashie::Mash do end end end + + with_minimum_ruby('2.4.0') do + describe '#transform_values' do + subject(:mash) { described_class.new(a: 1) } + + it 'returns a Hashie::Mash' do + expect(mash.transform_values(&:to_s)).to be_kind_of(described_class) + end + + it 'transforms the value' do + expect(mash.transform_values(&:to_s).a).to eql('1') + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1).transform_values { |a| a + 2 } } + + it 'creates an instance of subclass' do + expect(sub_mash).to be_kind_of(subclass) + end + end + end + end + + with_minimum_ruby('2.5.0') do + describe '#slice' do + subject(:mash) { described_class.new(a: 1, b: 2) } + + it 'returns a Hashie::Mash' do + expect(mash.slice(:a)).to be_kind_of(described_class) + end + + it 'returns a Mash with only the keys passed' do + expect(mash.slice(:a).to_hash).to eq('a' => 1) + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: 2) } + + it 'creates an instance of subclass' do + expect(sub_mash.slice(:a)).to be_kind_of(subclass) + end + end + end + + describe '#transform_keys' do + subject(:mash) { described_class.new(a: 1, b: 2) } + + it 'returns a Hashie::Mash' do + expect(mash.transform_keys { |k| k + k }).to be_kind_of(described_class) + end + + it 'returns a Mash with transformed keys' do + expect(mash.transform_keys { |k| k + k }).to eq('aa' => 1, 'bb' => 2) + end + + context 'when using with subclass' do + let(:subclass) { Class.new(Hashie::Mash) } + subject(:sub_mash) { subclass.new(a: 1, b: 2) } + + it 'creates an instance of subclass' do + expect(sub_mash.transform_keys { |k| k + k }).to be_kind_of(subclass) + end + end + end + end + + with_minimum_ruby('2.6.0') do + context 'ruby 2.6 merging' do + subject(:mash) { Hashie::Mash.new(model: 'Honda') } + it 'merges multiple hashes and mashes passeed to #merge' do + first_hash = { model: 'Ford' } + second_hash = { model: 'DeLorean' } + expect(mash.merge(first_hash, second_hash)).to eq('model' => 'DeLorean') + end + end + end end |