From ca23502935d6cbfc8511594c8396bcd970770162 Mon Sep 17 00:00:00 2001 From: Michael Herold Date: Thu, 22 Oct 2020 21:07:14 -0500 Subject: [PATCH] Ensure all properties are set on exported Dash When exporting a Dash via `#to_h` or `#to_hash`, we expect all properties to be exported whether or not they are set. However, in the change that allows codependent properties to be nilified, we regressed the behavior of exporting all properties. There is a gotcha here, which I note in the tests for the specs. For posterity, MRI does not send the `#to_hash` method to anything that subclasses `Hash` when you double-splat it. Thus, we cannot override the behavior within MRI. For more information, see [this comment][1] where I detail the behavior of double-splat within MRI. Currently, JRuby also follows this behavior, but it's not guaranteed that other Rubies will. [1]: https://github.com/hashie/hashie/issues/353#issuecomment-363294886 --- CHANGELOG.md | 1 + lib/hashie/dash.rb | 7 ++ .../extensions/dash/indifferent_access.rb | 9 +++ spec/hashie/dash_spec.rb | 72 +++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb383a79..946b3545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Any violations of this scheme are considered to be bugs. * [#516](https://github.com/hashie/hashie/issues/516): Fixed `NoMethodError` raised when including `Hashie::Extensions::Mash::SymbolizeKeys` and `Hashie::Extensions::SymbolizeKeys` in mashes/hashes with non string or symbol keys - [@carolineartz](https://github.com/carolineartz). * [#531](https://github.com/hashie/hashie/pull/531): Fixed [slice doesn't work using symbols](https://github.com/hashie/hashie/issues/529) using hash with `IndifferentAccess` extension - [@gnomex](https://github.com/gnomex). * [#533](https://github.com/hashie/hashie/pull/533): Fixed `NoMethodError: undefined method `to_json'` at `hashie/dash_spec` - [@gnomex](https://github.com/gnomex). +* [#535](https://github.com/hashie/hashie/pull/535): Restored the exporting of all properties as part of `Dash#to_h` and `Dash#to_hash` - [@michaelherold](https://github.com/michaelherold). * Your contribution here. ### Security diff --git a/lib/hashie/dash.rb b/lib/hashie/dash.rb index 460790b2..89a77a15 100644 --- a/lib/hashie/dash.rb +++ b/lib/hashie/dash.rb @@ -169,6 +169,13 @@ def replace(other_hash) self end + def to_h + defaults = ::Hash[self.class.properties.map { |prop| [prop, self.class.defaults[prop]] }] + + defaults.merge(self) + end + alias to_hash to_h + def update_attributes!(attributes) update_attributes(attributes) diff --git a/lib/hashie/extensions/dash/indifferent_access.rb b/lib/hashie/extensions/dash/indifferent_access.rb index 204cae6d..50d4f935 100644 --- a/lib/hashie/extensions/dash/indifferent_access.rb +++ b/lib/hashie/extensions/dash/indifferent_access.rb @@ -19,6 +19,15 @@ def self.requires_class_methods?(klass) end private_class_method :requires_class_methods? + def to_h + defaults = ::Hash[self.class.properties.map do |prop| + [Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]] + end] + + defaults.merge(self) + end + alias to_hash to_h + module ClassMethods # Check to see if the specified property has already been # defined. diff --git a/spec/hashie/dash_spec.rb b/spec/hashie/dash_spec.rb index 387f0cb1..20ffb331 100644 --- a/spec/hashie/dash_spec.rb +++ b/spec/hashie/dash_spec.rb @@ -416,6 +416,7 @@ class KeepingMash < Hashie::Mash Class.new(Hashie::Dash) do property :a, required: -> { b.nil? }, message: 'is required if b is not set.' property :b, required: -> { a.nil? }, message: 'is required if a is not set.' + property :c, default: -> { 'c' } end end @@ -434,6 +435,40 @@ class KeepingMash < Hashie::Mash it 'raises an error when neither property is set' do expect { codependent.new(a: nil, b: nil) }.to raise_error(ArgumentError) end + + context 'exporting nil values' do + describe '#to_h' do + it 'does not prune nil values' do + expect(codependent.new(a: 'hi', b: nil).to_h).to eq(a: 'hi', b: nil, c: 'c') + expect(codependent.new(a: 'hi', b: nil, c: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') + expect(codependent.new(a: 'hi', b: nil).merge(c: nil).to_h).to eq(a: 'hi', b: nil, c: nil) + end + end + + describe '#to_hash' do + it 'does not prune nil values' do + expect(codependent.new(a: 'hi', b: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') + expect(codependent.new(a: 'hi', b: nil, c: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') + expect(codependent.new(a: 'hi', b: nil).merge(c: nil).to_hash).to eq(a: 'hi', b: nil, c: nil) + end + end + + describe '**' do + # Note: This test is an implementation detail of MRI and may not hold for + # other Ruby interpreters. But it's important to note in the test suite + # because it can be surprising for people unfamiliar with the semantics of + # double-splatting. + # + # For more information, see [this link][1]: + # + # [1]: https://github.com/hashie/hashie/issues/353#issuecomment-363294886 + it 'prunes nil values because they are not set in the dash' do + dash = codependent.new(a: 'hi', b: nil) + + expect(**dash).to eq(a: 'hi', c: 'c') + end + end + end end end end @@ -482,6 +517,43 @@ class KeepingMash < Hashie::Mash expect(@bottom.new).to have_key(:echo) expect(@bottom.new).to_not have_key('echo') end + + context 'exporting nil values' do + let(:test) do + Class.new(Hashie::Dash) do + property :foo + property :bar + end + end + + describe '#to_h' do + it 'does not prune nil values' do + expect(test.new(foo: 'hi', bar: nil).to_h).to eq(foo: 'hi', bar: nil) + end + end + + describe '#to_hash' do + it 'does not prune nil values' do + expect(test.new(foo: 'hi', bar: nil).to_hash).to eq(foo: 'hi', bar: nil) + end + end + + describe '**' do + # Note: This test is an implementation detail of MRI and may not hold for + # other Ruby interpreters. But it's important to note in the test suite + # because it can be surprising for people unfamiliar with the semantics of + # double-splatting. + # + # For more information, see [this link][1]: + # + # [1]: https://github.com/hashie/hashie/issues/353#issuecomment-363294886 + it 'prunes nil values because they are not set in the dash' do + dash = test.new(foo: 'hi', bar: nil) + + expect(**dash).to eq(foo: 'hi') + end + end + end end describe SubclassedTest do