Skip to content

Commit

Permalink
Merge pull request #537 from michaelherold/dash-consistency
Browse files Browse the repository at this point in the history
Fix inconsistencies with Dash defaults
  • Loading branch information
michaelherold committed Oct 30, 2020
2 parents 59d04c8 + f152e25 commit 75410a6
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 @@ -41,6 +41,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 75410a6

Please sign in to comment.