summaryrefslogtreecommitdiff
path: root/spec/hashie
diff options
context:
space:
mode:
authorMichael Herold <opensource@michaeljherold.com>2018-09-30 21:21:55 -0500
committerMichael Herold <opensource@michaeljherold.com>2019-11-17 12:09:01 -0600
commit72d969260fadea6047a29eb00099b8e88f689fb9 (patch)
treea9d429fedb433afc338bea816f23ea3e399e9b83 /spec/hashie
parent2846ea63a90a594ed67e3eb8ba7c5fd125909089 (diff)
downloadhashie-72d969260fadea6047a29eb00099b8e88f689fb9.tar.gz
Prevent deep_merge from mutating nested hashes
The `DeepMerge` extension has two methods of mutating hashes: a destructive one and a non-destructive one. The `#deep_merge` version should not mutate the original hash or any hash nested within it. The `#deep_merge!` version is free to mutate the receiver. Without deeply duplicating the values contained within the hash, the invariant of immutability cannot be held for the original hash. In order to preserve that invariant, we need to introduce a method of deeply duplicating the hash. The trick here is that we cannot rely on a simple call to `Object#dup`. Some classes within the Ruby standard library are not duplicable in particular versions of Ruby. Newer versions of Ruby allow these classes to be "duplicated" in a way that returns the original value. These classes represent value objects, so it is safe to return the original value ... unless the classes are monkey-patched, but that isn't something we can protect against. This implementation does a best-effort to deeply duplicate an entire hash by relying on these value object classes being able to return themselves without violating immutability.
Diffstat (limited to 'spec/hashie')
-rw-r--r--spec/hashie/extensions/deep_merge_spec.rb53
1 files changed, 48 insertions, 5 deletions
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