hanami-utils/spec/hanami/utils/hash_spec.rb

488 lines
13 KiB
Ruby

require 'bigdecimal'
require 'hanami/utils/hash'
RSpec.describe Hanami::Utils::Hash do
describe '#initialize' do
let(:input_to_hash) do
Class.new do
def to_hash
Hash[foo: 'bar']
end
end.new
end
let(:input_to_h) do
Class.new do
def to_h
Hash[head: 'tail']
end
end.new
end
it 'holds values passed to the constructor' do
hash = Hanami::Utils::Hash.new('foo' => 'bar')
expect(hash['foo']).to eq('bar')
end
it 'assigns default via block' do
hash = Hanami::Utils::Hash.new { |h, k| h[k] = [] }
hash['foo'].push 'bar'
expect(hash).to eq('foo' => ['bar'])
end
it 'accepts a Hanami::Utils::Hash' do
arg = Hanami::Utils::Hash.new('foo' => 'bar')
hash = Hanami::Utils::Hash.new(arg)
expect(hash.to_h).to be_kind_of(::Hash)
end
it 'accepts object that implements #to_hash' do
hash = Hanami::Utils::Hash.new(input_to_hash)
expect(hash.to_h).to eq(input_to_hash.to_hash)
end
it "raises error when object doesn't implement #to_hash" do
expect { Hanami::Utils::Hash.new(input_to_h) }
.to raise_error(NoMethodError)
end
end
describe '#symbolize!' do
it 'symbolize keys' do
hash = Hanami::Utils::Hash.new('fub' => 'baz')
hash.symbolize!
expect(hash['fub']).to be_nil
expect(hash[:fub]).to eq('baz')
end
it 'does not symbolize nested hashes' do
hash = Hanami::Utils::Hash.new('nested' => { 'key' => 'value' })
hash.symbolize!
expect(hash[:nested].keys).to eq(['key'])
end
end
describe '#deep_symbolize!' do
it 'symbolize keys' do
hash = Hanami::Utils::Hash.new('fub' => 'baz')
hash.deep_symbolize!
expect(hash['fub']).to be_nil
expect(hash[:fub]).to eq('baz')
end
it 'symbolizes nested hashes' do
hash = Hanami::Utils::Hash.new('nested' => { 'key' => 'value' })
hash.deep_symbolize!
expect(hash[:nested]).to be_kind_of Hanami::Utils::Hash
expect(hash[:nested][:key]).to eq('value')
end
it 'symbolizes deep nested hashes' do
hash = Hanami::Utils::Hash.new('nested1' => { 'nested2' => { 'nested3' => { 'key' => 1 } } })
hash.deep_symbolize!
expect(hash.keys).to eq([:nested1])
hash1 = hash[:nested1]
expect(hash1.keys).to eq([:nested2])
hash2 = hash1[:nested2]
expect(hash2.keys).to eq([:nested3])
hash3 = hash2[:nested3]
expect(hash3.keys).to eq([:key])
expect(hash3[:key]).to eq(1)
end
it 'symbolize nested Hanami::Utils::Hashes' do
nested = Hanami::Utils::Hash.new('key' => 'value')
hash = Hanami::Utils::Hash.new('nested' => nested)
hash.deep_symbolize!
expect(hash[:nested]).to be_kind_of Hanami::Utils::Hash
expect(hash[:nested][:key]).to eq('value')
end
it 'symbolize nested object that responds to to_hash' do
nested = Hanami::Utils::Hash.new('metadata' => WrappingHash.new('coverage' => 100))
nested.deep_symbolize!
expect(nested[:metadata]).to be_kind_of Hanami::Utils::Hash
expect(nested[:metadata][:coverage]).to eq(100)
end
it "doesn't try to symbolize nested objects" do
hash = Hanami::Utils::Hash.new('foo' => ['bar'])
hash.deep_symbolize!
expect(hash[:foo]).to eq(['bar'])
end
end
describe '#stringify!' do
it 'covert keys to strings' do
hash = Hanami::Utils::Hash.new(fub: 'baz')
hash.stringify!
expect(hash[:fub]).to be_nil
expect(hash['fub']).to eq('baz')
end
it 'stringifies nested hashes' do
hash = Hanami::Utils::Hash.new(nested: { key: 'value' })
hash.stringify!
expect(hash['nested']).to be_kind_of Hanami::Utils::Hash
expect(hash['nested']['key']).to eq('value')
end
it 'stringifies nested Hanami::Utils::Hashes' do
nested = Hanami::Utils::Hash.new(key: 'value')
hash = Hanami::Utils::Hash.new(nested: nested)
hash.stringify!
expect(hash['nested']).to be_kind_of Hanami::Utils::Hash
expect(hash['nested']['key']).to eq('value')
end
it 'stringifies nested object that responds to to_hash' do
nested = Hanami::Utils::Hash.new(metadata: WrappingHash.new(coverage: 100))
nested.stringify!
expect(nested['metadata']).to be_kind_of Hanami::Utils::Hash
expect(nested['metadata']['coverage']).to eq(100)
end
end
describe '#deep_dup' do
it 'returns an instance of Utils::Hash' do
duped = Hanami::Utils::Hash.new('foo' => 'bar').deep_dup
expect(duped).to be_kind_of(Hanami::Utils::Hash)
end
it 'returns a hash with duplicated values' do
hash = Hanami::Utils::Hash.new('foo' => 'bar', 'baz' => 'x')
duped = hash.deep_dup
duped['foo'] = nil
duped['baz'].upcase!
expect(hash['foo']).to eq('bar')
expect(hash['baz']).to eq('x')
end
it "doesn't try to duplicate value that can't perform this operation" do
original = {
'nil' => nil,
'false' => false,
'true' => true,
'symbol' => :symbol,
'fixnum' => 23,
'bignum' => 13_289_301_283**2,
'float' => 1.0,
'complex' => Complex(0.3),
'bigdecimal' => BigDecimal.new('12.0001'),
'rational' => Rational(0.3)
}
hash = Hanami::Utils::Hash.new(original)
duped = hash.deep_dup
expect(duped).to eq(original)
expect(duped.object_id).not_to eq(original.object_id)
end
it 'returns a hash with nested duplicated values' do
hash = Hanami::Utils::Hash.new('foo' => { 'bar' => 'baz' }, 'x' => Hanami::Utils::Hash.new('y' => 'z'))
duped = hash.deep_dup
duped['foo']['bar'].reverse!
duped['x']['y'].upcase!
expect(hash['foo']['bar']).to eq('baz')
expect(hash['x']['y']).to eq('z')
end
it 'preserves original class' do
duped = Hanami::Utils::Hash.new('foo' => {}, 'x' => Hanami::Utils::Hash.new).deep_dup
expect(duped['foo']).to be_kind_of(::Hash)
expect(duped['x']).to be_kind_of(Hanami::Utils::Hash)
end
end
describe 'hash interface' do
it 'returns a new Hanami::Utils::Hash for methods which return a ::Hash' do
hash = Hanami::Utils::Hash.new('a' => 1)
result = hash.clear
expect(hash).to be_empty
expect(result).to be_kind_of(Hanami::Utils::Hash)
end
it 'returns a value that is compliant with ::Hash return value' do
hash = Hanami::Utils::Hash.new('a' => 1)
result = hash.assoc('a')
expect(result).to eq ['a', 1]
end
it 'responds to whatever ::Hash responds to' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash).to respond_to :rehash
expect(hash).not_to respond_to :unknown_method
end
it 'accepts blocks for methods' do
hash = Hanami::Utils::Hash.new('a' => 1)
result = hash.delete_if { |k, _| k == 'a' }
expect(result).to be_empty
end
describe '#to_h' do
it 'returns a ::Hash' do
actual = Hanami::Utils::Hash.new('a' => 1).to_h
expect(actual).to eq('a' => 1)
end
it 'returns nested ::Hash' do
hash = {
tutorial: {
instructions: [
{ title: 'foo', body: 'bar' },
{ title: 'hoge', body: 'fuga' }
]
}
}
utils_hash = Hanami::Utils::Hash.new(hash)
expect(utils_hash).not_to be_kind_of(::Hash)
actual = utils_hash.to_h
expect(actual).to eq(hash)
expect(actual[:tutorial]).to be_kind_of(::Hash)
expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash))
end
it 'returns nested ::Hash (when symbolized)' do
hash = {
'tutorial' => {
'instructions' => [
{ 'title' => 'foo', 'body' => 'bar' },
{ 'title' => 'hoge', 'body' => 'fuga' }
]
}
}
utils_hash = Hanami::Utils::Hash.new(hash).deep_symbolize!
expect(utils_hash).not_to be_kind_of(::Hash)
actual = utils_hash.to_h
expect(actual).to eq(hash)
expect(actual[:tutorial]).to be_kind_of(::Hash)
expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash))
end
end
it 'prevents information escape' do
actual = Hanami::Utils::Hash.new('a' => 1)
hash = actual.to_h
hash['b'] = 2
expect(actual.to_h).to eq('a' => 1)
end
it 'prevents information escape for nested hash'
# it 'prevents information escape for nested hash' do
# actual = Hanami::Utils::Hash.new({'a' => {'b' => 2}})
# hash = actual.to_h
# subhash = hash['a']
# subhash.merge!('c' => 3)
# expect(actual.to_h).to eq({'a' => {'b' => 2}})
# end
it 'serializes nested objects that respond to to_hash' do
nested = Hanami::Utils::Hash.new(metadata: WrappingHash.new(coverage: 100))
expect(nested.to_h).to eq(metadata: { coverage: 100 })
end
end
describe '#to_hash' do
it 'returns a ::Hash' do
actual = Hanami::Utils::Hash.new('a' => 1).to_hash
expect(actual).to eq('a' => 1)
end
it 'returns nested ::Hash' do
hash = {
tutorial: {
instructions: [
{ title: 'foo', body: 'bar' },
{ title: 'hoge', body: 'fuga' }
]
}
}
utils_hash = Hanami::Utils::Hash.new(hash)
expect(utils_hash).not_to be_kind_of(::Hash)
actual = utils_hash.to_h
expect(actual).to eq(hash)
expect(actual[:tutorial]).to be_kind_of(::Hash)
expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash))
end
it 'returns nested ::Hash (when symbolized)' do
hash = {
'tutorial' => {
'instructions' => [
{ 'title' => 'foo', 'body' => 'bar' },
{ 'title' => 'hoge', 'body' => 'fuga' }
]
}
}
utils_hash = Hanami::Utils::Hash.new(hash).deep_symbolize!
expect(utils_hash).not_to be_kind_of(::Hash)
actual = utils_hash.to_h
expect(actual).to eq(hash)
expect(actual[:tutorial]).to be_kind_of(::Hash)
expect(actual[:tutorial][:instructions]).to all(be_kind_of(::Hash))
end
it 'prevents information escape' do
actual = Hanami::Utils::Hash.new('a' => 1)
hash = actual.to_hash
hash['b'] = 2
expect(actual.to_hash).to eq('a' => 1)
end
end
describe '#to_a' do
it 'returns an ::Array' do
actual = Hanami::Utils::Hash.new('a' => 1).to_a
expect(actual).to eq([['a', 1]])
end
it 'prevents information escape' do
actual = Hanami::Utils::Hash.new('a' => 1)
array = actual.to_a
array.push(['b', 2])
expect(actual.to_a).to eq([['a', 1]])
end
end
describe 'equality' do
it 'has a working equality' do
hash = Hanami::Utils::Hash.new('a' => 1)
other = Hanami::Utils::Hash.new('a' => 1)
expect(hash == other).to be_truthy
end
it 'has a working equality with raw hashes' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash == { 'a' => 1 }).to be_truthy
end
end
describe 'case equality' do
it 'has a working case equality' do
hash = Hanami::Utils::Hash.new('a' => 1)
other = Hanami::Utils::Hash.new('a' => 1)
expect(hash === other).to be_truthy # rubocop:disable Style/CaseEquality
end
it 'has a working case equality with raw hashes' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash === { 'a' => 1 }).to be_truthy # rubocop:disable Style/CaseEquality
end
end
describe 'value equality' do
it 'has a working value equality' do
hash = Hanami::Utils::Hash.new('a' => 1)
other = Hanami::Utils::Hash.new('a' => 1)
expect(hash).to eql(other)
end
it 'has a working value equality with raw hashes' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash).to eql('a' => 1)
end
end
describe 'identity equality' do
it 'has a working identity equality' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash).to equal(hash)
end
it 'has a working identity equality with raw hashes' do
hash = Hanami::Utils::Hash.new('a' => 1)
expect(hash).not_to equal('a' => 1)
end
end
describe '#hash' do
it 'returns the same hash result of ::Hash' do
expected = { 'l' => 23 }.hash
actual = Hanami::Utils::Hash.new('l' => 23).hash
expect(actual).to eq expected
end
end
describe '#inspect' do
it 'returns the same output of ::Hash' do
expected = { 'l' => 23, l: 23 }.inspect
actual = Hanami::Utils::Hash.new('l' => 23, l: 23).inspect
expect(actual).to eq expected
end
end
describe 'unknown method' do
it 'raises error' do
begin
Hanami::Utils::Hash.new('l' => 23).party!
rescue NoMethodError => e
expect(e.message).to eq %(undefined method `party!' for {\"l\"=>23}:Hanami::Utils::Hash)
end
end
# See: https://github.com/hanami/utils/issues/48
it 'returns the correct object when a NoMethodError is raised' do
hash = Hanami::Utils::Hash.new('a' => 1)
if RUBY_VERSION == '2.4.0' # rubocop:disable Style/ConditionalAssignment
exception_message = "undefined method `foo' for 1:Integer"
else
exception_message = "undefined method `foo' for 1:Fixnum"
end
expect { hash.all? { |_, v| v.foo } }.to raise_error(NoMethodError, include(exception_message))
end
end
end