diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | CONTRIBUTING.md | 11 | ||||
-rw-r--r-- | Gemfile | 27 | ||||
-rwxr-xr-x | bin/setup | 7 | ||||
-rw-r--r-- | hashie.gemspec | 15 | ||||
-rw-r--r-- | lib/hashie/extensions/deep_merge.rb | 17 | ||||
-rw-r--r-- | lib/hashie/utils.rb | 28 | ||||
-rw-r--r-- | spec/hashie/extensions/deep_merge_spec.rb | 53 |
8 files changed, 135 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b3aad..1b46c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ scheme are considered to be bugs. ### Fixed +* [#467](https://github.com/intridea/hashie/pull/467): Fixed `DeepMerge#deep_merge` mutating nested values within the receiver - [@michaelherold](https://github.com/michaelherold). * Your contribution here. ### Security @@ -37,6 +38,7 @@ scheme are considered to be bugs. ### Miscellaneous +* [#981](https://github.com/hashie/hashie/pull/981): Exclude tests from the gem release to reduce installation size and improve installation speed - [@michaelherold](https://github.com/michaelherold). * Your contribution here. ## [4.0.0] - 2019-10-30 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b96827..7eb5cc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,12 +23,19 @@ git pull upstream master git checkout -b my-feature-branch ``` -#### Bundle Install and Test +#### Install dependencies + +You can use the setup script to install dependencies for the gem and its integration tests. + +``` +bin/setup +``` + +#### Test Ensure that you can build the project and run tests. ``` -bundle install bundle exec rake ``` @@ -11,20 +11,25 @@ group :development do gem 'pry' gem 'pry-stack_explorer', platforms: %i[ruby_19 ruby_20 ruby_21] gem 'rubocop', '0.52.1' + + group :test do + # ActiveSupport required to test compatibility with ActiveSupport Core Extensions. + # rubocop:disable Bundler/DuplicatedGem + require File.expand_path('../lib/hashie/extensions/ruby_version', __FILE__) + if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= + Hashie::Extensions::RubyVersion.new('2.4.0') + gem 'activesupport', '~> 5.x', require: false + else + gem 'activesupport', '~> 4.x', require: false + end + # rubocop:enable Bundler/DuplicatedGem + gem 'rake' + gem 'rspec', '~> 3' + gem 'rspec-pending_for', '~> 0.1' + end end group :test do - # ActiveSupport required to test compatibility with ActiveSupport Core Extensions. - # rubocop:disable Bundler/DuplicatedGem - require File.expand_path('../lib/hashie/extensions/ruby_version', __FILE__) - if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= - Hashie::Extensions::RubyVersion.new('2.4.0') - gem 'activesupport', '~> 5.x', require: false - else - gem 'activesupport', '~> 4.x', require: false - end - # rubocop:enable Bundler/DuplicatedGem gem 'codeclimate-test-reporter', '~> 1.0', require: false gem 'danger-changelog', '~> 0.1.0', require: false - gem 'rspec-core', '~> 3.1.7' end @@ -1,7 +1,12 @@ #!/bin/bash + set -euo pipefail IFS=$'\n\t' bundle install -# Do any other automated setup that you need to do here +for dir in spec/integration/*; do + pushd "$dir" + bundle install + popd +done diff --git a/hashie.gemspec b/hashie.gemspec index 250ecf2..dbb438d 100644 --- a/hashie.gemspec +++ b/hashie.gemspec @@ -14,10 +14,15 @@ Gem::Specification.new do |gem| gem.files = %w[.yardopts CHANGELOG.md CONTRIBUTING.md LICENSE README.md UPGRADING.md] gem.files += %w[Rakefile hashie.gemspec] gem.files += Dir['lib/**/*.rb'] - gem.files += Dir['spec/**/*.rb'] - gem.test_files = Dir['spec/**/*.rb'] - gem.add_development_dependency 'rake', '< 11' - gem.add_development_dependency 'rspec', '~> 3.0' - gem.add_development_dependency 'rspec-pending_for', '~> 0.1' + if gem.respond_to?(:metadata) + gem.metadata = { + 'bug_tracker_uri' => 'https://github.com/hashie/hashie/issues', + 'changelog_uri' => 'https://github.com/hashie/hashie/blob/master/CHANGELOG.md', + 'documentation_uri' => 'https://www.rubydoc.info/gems/hashie', + 'source_code_uri' => 'https://github.com/hashie/hashie' + } + end + + gem.add_development_dependency 'bundler' end diff --git a/lib/hashie/extensions/deep_merge.rb b/lib/hashie/extensions/deep_merge.rb index 5363904..1890e8f 100644 --- a/lib/hashie/extensions/deep_merge.rb +++ b/lib/hashie/extensions/deep_merge.rb @@ -3,7 +3,7 @@ module Hashie module DeepMerge # Returns a new hash with +self+ and +other_hash+ merged recursively. def deep_merge(other_hash, &block) - copy = dup + copy = _deep_dup(self) copy.extend(Hashie::Extensions::DeepMerge) unless copy.respond_to?(:deep_merge!) copy.deep_merge!(other_hash, &block) end @@ -18,6 +18,21 @@ module Hashie private + def _deep_dup(hash) + copy = hash.dup + + copy.each do |key, value| + copy[key] = + if value.is_a?(::Hash) + _deep_dup(value) + else + Hashie::Utils.safe_dup(value) + end + end + + copy + end + def _recursive_merge(hash, other_hash, &block) other_hash.each do |k, v| hash[k] = diff --git a/lib/hashie/utils.rb b/lib/hashie/utils.rb index d8e05fe..5b55b9a 100644 --- a/lib/hashie/utils.rb +++ b/lib/hashie/utils.rb @@ -12,5 +12,33 @@ module Hashie "defined in #{bound_method.owner}" end end + + # Duplicates a value or returns the value when it is not duplicable + # + # @api public + # + # @param value [Object] the value to safely duplicate + # @return [Object] the duplicated value + def self.safe_dup(value) + case value + when Complex, FalseClass, NilClass, Rational, Method, Symbol, TrueClass, *integer_classes + value + else + value.dup + end + end + + # Lists the classes Ruby uses for integers + # + # @api private + # @return [Array<Class>] + def self.integer_classes + @integer_classes ||= + if const_defined?(:Fixnum) + [Fixnum, Bignum] # rubocop:disable Lint/UnifiedInteger + else + [Integer] + end + end end end diff --git a/spec/hashie/extensions/deep_merge_spec.rb b/spec/hashie/extensions/deep_merge_spec.rb index ab79ff6..4ff6c30 100644 --- a/spec/hashie/extensions/deep_merge_spec.rb +++ b/spec/hashie/extensions/deep_merge_spec.rb @@ -14,19 +14,62 @@ describe Hashie::Extensions::DeepMerge do context 'without &block' do let(:h1) do - subject.new.merge(a: 'a', a1: 42, b: 'b', c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }) + subject.new.merge( + a: 'a', + a1: 42, + b: 'b', + c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }, + d: nil, + d1: false, + d2: true, + d3: unbound_method, + d4: Complex(1), + d5: Rational(1) + ) end let(:h2) { { a: 1, a1: 1, c: { c1: 2, c2: 'c2', c3: { d2: 'd2' } }, e: { e1: 1 } } } + let(:unbound_method) { method(:puts) } let(:expected_hash) do - { a: 1, a1: 1, b: 'b', c: { c1: 2, c2: 'c2', c3: { d1: 'd1', d2: 'd2' } }, e: { e1: 1 } } + { + a: 1, + a1: 1, + b: 'b', + c: { c1: 2, c2: 'c2', c3: { d1: 'd1', d2: 'd2' } }, + d: nil, + d1: false, + d2: true, + d3: unbound_method, + d4: Complex(1), + d5: Rational(1), + e: { e1: 1 } + } end - it 'deep merges two hashes' do - expect(h1.deep_merge(h2)).to eq expected_hash + it 'deep merges two hashes without modifying them' do + result = h1.deep_merge(h2) + + expect(result).to eq expected_hash + expect(h1).to( + eq( + a: 'a', + a1: 42, + b: 'b', + c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }, + d: nil, + d1: false, + d2: true, + d3: unbound_method, + d4: Complex(1), + d5: Rational(1) + ) + ) + expect(h2).to eq(a: 1, a1: 1, c: { c1: 2, c2: 'c2', c3: { d2: 'd2' } }, e: { e1: 1 }) end it 'deep merges another hash in place via bang method' do - h1.deep_merge!(h2) + result = h1.deep_merge!(h2) + + expect(result).to eq expected_hash expect(h1).to eq expected_hash end |