Skip to content

Commit

Permalink
Fix inconsistencies with Dash defaults
Browse files Browse the repository at this point in the history
The normal behavior of Dash with respect to property defaults differed
from the behavior of a Dash/Trash with IgnoreUndeclared mixed in. This
is because some situations called the defaults and some did not.

This change normalizes the behavior so that all situations where the
defaults should be used to override unset values behave consistently,
as well as all situations where the default should not override a `nil`
value.
  • Loading branch information
michaelherold authored and Michael Herold committed Oct 24, 2020
1 parent 14e923d commit f152e25
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -40,6 +40,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).
* [#537](https://github.com/hashie/hashie/pull/537): Fixed inconsistencies with handling defaults in `Dash` with and without `IgnoreUnclared` mixed in - [@michaelherold](https://github.com/michaelherold).
* Your contribution here.

### Security
Expand Down
21 changes: 21 additions & 0 deletions README.md
Expand Up @@ -875,6 +875,27 @@ p = Tricky.new('trick' => 'two')
p.trick # => NoMethodError
```

If you would like to update a Dash and use any default values set in the case of a `nil` value, use `#update_attributes!`.

```ruby
class WithDefaults < Hashie::Dash
property :description, default: 'none'
end

dash = WithDefaults.new
dash.description #=> 'none'

dash.description = 'You committed one of the classic blunders!'
dash.description #=> 'You committed one of the classic blunders!'

dash.description = nil
dash.description #=> nil

dash.description = 'Only slightly less known is ...'
dash.update_attributes!(description: nil)
dash.description #=> 'none'
```

### Potential Gotchas

Because Dashes are subclasses of the built-in Ruby Hash class, the double-splat operator takes the Dash as-is without any conversion. This can lead to strange behavior when you use the double-splat operator on a Dash as the first part of a keyword list or built Hash. For example:
Expand Down
25 changes: 9 additions & 16 deletions lib/hashie/dash.rb
Expand Up @@ -104,19 +104,6 @@ def self.required?(name)
def initialize(attributes = {}, &block)
super(&block)

self.class.defaults.each_pair do |prop, value|
self[prop] = begin
val = value.dup
if val.is_a?(Proc)
val.arity == 1 ? val.call(self) : val.call
else
val
end
rescue TypeError
value
end
end

initialize_attributes(attributes)
assert_required_attributes_set!
end
Expand Down Expand Up @@ -173,13 +160,19 @@ def update_attributes!(attributes)
update_attributes(attributes)

self.class.defaults.each_pair do |prop, value|
next unless self[prop].nil?
next unless fetch(prop, nil).nil?
self[prop] = begin
value.dup
val = value.dup
if val.is_a?(Proc)
val.arity == 1 ? val.call(self) : val.call
else
val
end
rescue TypeError
value
end
end

assert_required_attributes_set!
end

Expand All @@ -189,7 +182,7 @@ def initialize_attributes(attributes)
return unless attributes

cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
update_attributes(cleaned_attributes)
update_attributes!(cleaned_attributes)
end

def update_attributes(attributes)
Expand Down
9 changes: 4 additions & 5 deletions lib/hashie/extensions/ignore_undeclared.rb
Expand Up @@ -31,12 +31,11 @@ module Extensions
module IgnoreUndeclared
def initialize_attributes(attributes)
return unless attributes

klass = self.class
translations = klass.respond_to?(:translations) && klass.translations
attributes.each_pair do |att, value|
next unless klass.property?(att) || (translations && translations.include?(att))
self[att] = value
end
translations = klass.respond_to?(:translations) && klass.translations || []

super(attributes.select { |attr, _| klass.property?(attr) || translations.include?(attr) })
end

def property_exists?(property)
Expand Down
10 changes: 10 additions & 0 deletions spec/hashie/dash_spec.rb
Expand Up @@ -606,3 +606,13 @@ class DashWithMethodAccess < Hashie::Dash
it { is_expected.to eq true }
end
end

RSpec.describe Hashie::Dash do
let(:test) do
Class.new(Hashie::Dash) do
property :description, default: ''
end
end

include_examples 'Dash default handling', :description
end
12 changes: 12 additions & 0 deletions spec/hashie/extensions/ignore_undeclared_spec.rb
Expand Up @@ -27,6 +27,18 @@ class ForgivingTrash < Hashie::Trash
hash = subject.new(city: 'Toronto')
expect { hash.country = 'Canada' }.to raise_error(NoMethodError)
end

context 'with a default value' do
let(:test) do
Class.new(Hashie::Trash) do
include Hashie::Extensions::IgnoreUndeclared

property :description, from: :desc, default: ''
end
end

include_examples 'Dash default handling', :description, :desc
end
end

context 'combined with DeepMerge' do
Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Expand Up @@ -13,6 +13,8 @@
require './spec/support/logger'
require './spec/support/matchers'

Dir[File.expand_path(File.join(__dir__, 'support', '**', '*'))].sort.each { |file| require file }

RSpec.configure do |config|
config.extend RubyVersionCheck
config.expect_with :rspec do |expect|
Expand Down
26 changes: 26 additions & 0 deletions spec/support/shared_examples.rb
@@ -0,0 +1,26 @@
RSpec.shared_examples 'Dash default handling' do |property, name = property|
it 'uses the default when initializing' do
expect(test.new(name => nil).public_send(property)).to eq ''
end

it 'allows you to set the value to nil with the hash writer' do
trash = test.new(name => 'foo')
trash[name] = nil

expect(trash.public_send(property)).to be_nil
end

it 'allows you to set the value to nil with the method writer' do
trash = test.new(name => 'foo')
trash[name] = nil

expect(trash.public_send(property)).to be_nil
end

it 'uses the default when updating with defaults' do
trash = test.new(name => 'foo')
trash.update_attributes!(name => nil)

expect(trash.public_send(property)).to eq ''
end
end

0 comments on commit f152e25

Please sign in to comment.