summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2019-08-18 08:59:17 -0400
committerGitHub <noreply@github.com>2019-08-18 08:59:17 -0400
commit04cd30af1362574a1adea53aae0008e5732ea994 (patch)
tree9cd7b61174cde9f72b3b5b1ba4a132d8d95cbef2
parent1de19bff87e1f75192af2788e8ee47cda593ade2 (diff)
parent9b3209c8d9be7aff172327564f6c42427fa0f92d (diff)
downloadhashie-04cd30af1362574a1adea53aae0008e5732ea994.tar.gz
Merge pull request #481 from BobbyMcWho/480-implement-non-destructive-methods
Implement non-destructive standard Hash methods
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--CHANGELOG.md3
-rw-r--r--UPGRADING.md41
-rw-r--r--lib/hashie/mash.rb77
-rw-r--r--spec/hashie/mash_spec.rb162
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