hashie/spec/hashie/extensions/coercion_spec.rb

637 lines
18 KiB
Ruby

require 'spec_helper'
describe Hashie::Extensions::Coercion do
class NotInitializable
private_class_method :new
end
class Initializable
attr_reader :coerced, :value
def initialize(obj, coerced = nil)
@coerced = coerced
@value = obj.class.to_s
end
def coerced?
!@coerced.nil?
end
end
class Coercable < Initializable
def self.coerce(obj)
new(obj, true)
end
end
before(:each) do
class ExampleCoercableHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
end
end
subject { ExampleCoercableHash }
let(:instance) { subject.new }
describe '#coerce_key' do
context 'nesting' do
class BaseCoercableHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
end
class NestedCoercableHash < BaseCoercableHash
coerce_key :foo, String
coerce_key :bar, Integer
end
class OtherNestedCoercableHash < BaseCoercableHash
coerce_key :foo, Symbol
end
class RootCoercableHash < BaseCoercableHash
coerce_key :nested, NestedCoercableHash
coerce_key :other, OtherNestedCoercableHash
coerce_key :nested_list, Array[NestedCoercableHash]
coerce_key :nested_hash, Hash[String => NestedCoercableHash]
end
def test_nested_object(obj)
expect(obj).to be_a(NestedCoercableHash)
expect(obj[:foo]).to be_a(String)
expect(obj[:bar]).to be_an(Integer)
end
subject { RootCoercableHash }
let(:instance) { subject.new }
it 'does not add coercions to superclass' do
instance[:nested] = { foo: 'bar' }
instance[:other] = { foo: 'bar' }
expect(instance[:nested][:foo]).to be_a String
expect(instance[:other][:foo]).to be_a Symbol
end
it 'coerces nested objects' do
instance[:nested] = { foo: 123, bar: '456' }
test_nested_object(instance[:nested])
end
it 'coerces nested arrays' do
instance[:nested_list] = [
{ foo: 123, bar: '456' },
{ foo: 234, bar: '567' },
{ foo: 345, bar: '678' }
]
expect(instance[:nested_list]).to be_a Array
expect(instance[:nested_list].size).to eq(3)
instance[:nested_list].each do |nested|
test_nested_object nested
end
end
it 'coerces nested hashes' do
instance[:nested_hash] = {
a: { foo: 123, bar: '456' },
b: { foo: 234, bar: '567' },
c: { foo: 345, bar: '678' }
}
expect(instance[:nested_hash]).to be_a Hash
expect(instance[:nested_hash].size).to eq(3)
instance[:nested_hash].each do |key, nested|
expect(key).to be_a(String)
test_nested_object nested
end
end
context 'when repetitively including the module' do
class RepetitiveCoercableHash < NestedCoercableHash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
coerce_key :nested, NestedCoercableHash
end
subject { RepetitiveCoercableHash }
let(:instance) { subject.new }
it 'does not raise a stack overflow error' do
expect do
instance[:nested] = { foo: 123, bar: '456' }
test_nested_object(instance[:nested])
end.not_to raise_error
end
end
end
it { expect(subject).to be_respond_to(:coerce_key) }
it 'runs through coerce on a specified key' do
subject.coerce_key :foo, Coercable
instance[:foo] = 'bar'
expect(instance[:foo]).to be_coerced
end
it 'skips unnecessary coercions' do
subject.coerce_key :foo, Coercable
instance[:foo] = Coercable.new('bar')
expect(instance[:foo]).to_not be_coerced
end
it 'supports an array of keys' do
subject.coerce_keys :foo, :bar, Coercable
instance[:foo] = 'bar'
instance[:bar] = 'bax'
expect(instance[:foo]).to be_coerced
expect(instance[:bar]).to be_coerced
end
it 'supports coercion for Array' do
subject.coerce_key :foo, Array[Coercable]
instance[:foo] = %w[bar bar2]
expect(instance[:foo]).to all(be_coerced)
expect(instance[:foo]).to be_a(Array)
end
it 'supports coercion for Set' do
subject.coerce_key :foo, Set[Coercable]
instance[:foo] = Set.new(%w[bar bar2])
expect(instance[:foo]).to all(be_coerced)
expect(instance[:foo]).to be_a(Set)
end
it 'supports coercion for Set of primitive' do
subject.coerce_key :foo, Set[Initializable]
instance[:foo] = %w[bar bar2]
expect(instance[:foo].map(&:value)).to all(eq 'String')
expect(instance[:foo]).to be_none(&:coerced?)
expect(instance[:foo]).to be_a(Set)
end
it 'supports coercion for Hash' do
subject.coerce_key :foo, Hash[Coercable => Coercable]
instance[:foo] = { 'bar_key' => 'bar_value', 'bar2_key' => 'bar2_value' }
expect(instance[:foo].keys).to all(be_coerced)
expect(instance[:foo].values).to all(be_coerced)
expect(instance[:foo]).to be_a(Hash)
end
it 'supports coercion for Hash with primitive as value' do
subject.coerce_key :foo, Hash[Coercable => Initializable]
instance[:foo] = { 'bar_key' => '1', 'bar2_key' => '2' }
expect(instance[:foo].values.map(&:value)).to all(eq 'String')
expect(instance[:foo].keys).to all(be_coerced)
end
context 'coercing core types' do
def test_coercion(literal, target_type, coerce_method)
subject.coerce_key :foo, target_type
instance[:foo] = literal
expect(instance[:foo]).to be_a(target_type)
expect(instance[:foo]).to eq(literal.send(coerce_method))
end
RSpec.shared_examples 'coerces from numeric types' do |target_type, coerce_method|
it "coerces from String to #{target_type} via #{coerce_method}" do
test_coercion '2.0', target_type, coerce_method
end
it "coerces from Integer to #{target_type} via #{coerce_method}" do
# Fixnum
test_coercion 2, target_type, coerce_method
# Bignum
test_coercion 12_345_667_890_987_654_321, target_type, coerce_method
end
it "coerces from Rational to #{target_type} via #{coerce_method}" do
test_coercion Rational(2, 3), target_type, coerce_method
end
end
RSpec.shared_examples 'coerces from alphabetical types' do |target_type, coerce_method|
it "coerces from String to #{target_type} via #{coerce_method}" do
test_coercion 'abc', target_type, coerce_method
end
it "coerces from Symbol to #{target_type} via #{coerce_method}" do
test_coercion :abc, target_type, coerce_method
end
end
include_examples 'coerces from numeric types', Integer, :to_i
include_examples 'coerces from numeric types', Float, :to_f
include_examples 'coerces from numeric types', String, :to_s
include_examples 'coerces from alphabetical types', String, :to_s
include_examples 'coerces from alphabetical types', Symbol, :to_sym
it 'can coerce String to Rational when possible' do
test_coercion '2/3', Rational, :to_r
end
it 'can coerce String to Complex when possible' do
test_coercion '2/3+3/4i', Complex, :to_c
end
it 'coerces collections with core types' do
subject.coerce_key :foo, Hash[String => String]
instance[:foo] = {
abc: 123,
xyz: 987
}
expect(instance[:foo]).to eq(
'abc' => '123',
'xyz' => '987'
)
end
it 'can coerce via a proc' do
subject.coerce_key(:foo, lambda do |v|
case v
when String
return !!(v =~ /^(true|t|yes|y|1)$/i)
when Numeric
return !v.to_i.zero?
else
return v == true
end
end)
true_values = [true, 'true', 't', 'yes', 'y', '1', 1, -1]
false_values = [false, 'false', 'f', 'no', 'n', '0', 0]
true_values.each do |v|
instance[:foo] = v
expect(instance[:foo]).to be_a(TrueClass)
end
false_values.each do |v|
instance[:foo] = v
expect(instance[:foo]).to be_a(FalseClass)
end
end
it 'raises errors for non-coercable types' do
subject.coerce_key :foo, NotInitializable
expect { instance[:foo] = 'true' }
.to raise_error(Hashie::CoercionError, /NotInitializable is not a coercable type/)
end
it 'can coerce false' do
subject.coerce_key :foo, Coercable
instance[:foo] = false
expect(instance[:foo]).to be_coerced
expect(instance[:foo].value).to eq('FalseClass')
end
it 'does not coerce nil' do
subject.coerce_key :foo, String
instance[:foo] = nil
expect(instance[:foo]).to_not eq('')
expect(instance[:foo]).to be_nil
end
end
it 'calls #new if no coerce method is available' do
subject.coerce_key :foo, Initializable
instance[:foo] = 'bar'
expect(instance[:foo].value).to eq 'String'
expect(instance[:foo]).not_to be_coerced
end
it 'coerces when the merge initializer is used' do
subject.coerce_key :foo, Coercable
instance = subject.new(foo: 'bar')
expect(instance[:foo]).to be_coerced
end
context 'when #replace is used' do
before { subject.coerce_key :foo, :bar, Coercable }
let(:instance) do
subject.new(foo: 'bar').replace(foo: 'foz', bar: 'baz', hi: 'bye')
end
it 'coerces relevant keys' do
expect(instance[:foo]).to be_coerced
expect(instance[:bar]).to be_coerced
expect(instance[:hi]).not_to respond_to(:coerced?)
end
it 'sets correct values' do
expect(instance[:hi]).to eq 'bye'
end
end
context 'when used with a Mash' do
class UserMash < Hashie::Mash
end
class TweetMash < Hashie::Mash
include Hashie::Extensions::Coercion
coerce_key :user, UserMash
end
it 'coerces with instance initialization' do
tweet = TweetMash.new(user: { email: 'foo@bar.com' })
expect(tweet[:user]).to be_a(UserMash)
end
it 'coerces when setting with attribute style' do
tweet = TweetMash.new
tweet.user = { email: 'foo@bar.com' }
expect(tweet[:user]).to be_a(UserMash)
end
it 'coerces when setting with string index' do
tweet = TweetMash.new
tweet['user'] = { email: 'foo@bar.com' }
expect(tweet[:user]).to be_a(UserMash)
end
it 'coerces when setting with symbol index' do
tweet = TweetMash.new
tweet[:user] = { email: 'foo@bar.com' }
expect(tweet[:user]).to be_a(UserMash)
end
end
context 'when used with a Trash' do
class UserTrash < Hashie::Trash
property :email
end
class TweetTrash < Hashie::Trash
include Hashie::Extensions::Coercion
property :user, from: :user_data
coerce_key :user, UserTrash
end
it 'coerces with instance initialization' do
tweet = TweetTrash.new(user_data: { email: 'foo@bar.com' })
expect(tweet[:user]).to be_a(UserTrash)
end
end
context 'when used with IndifferentAccess to coerce a Mash' do
class MyHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::IndifferentAccess
include Hashie::Extensions::MergeInitializer
end
class UserHash < MyHash
end
class TweetHash < MyHash
coerce_key :user, UserHash
end
it 'coerces with instance initialization' do
tweet = TweetHash.new(user: Hashie::Mash.new(email: 'foo@bar.com'))
expect(tweet[:user]).to be_a(UserHash)
end
it 'coerces when setting with string index' do
tweet = TweetHash.new
tweet['user'] = Hashie::Mash.new(email: 'foo@bar.com')
expect(tweet[:user]).to be_a(UserHash)
end
it 'coerces when setting with symbol index' do
tweet = TweetHash.new
tweet[:user] = Hashie::Mash.new(email: 'foo@bar.com')
expect(tweet[:user]).to be_a(UserHash)
end
end
context 'when subclassing' do
class MyOwnBase < Hash
include Hashie::Extensions::Coercion
end
class MyOwnHash < MyOwnBase
coerce_key :value, Integer
end
class MyOwnSubclass < MyOwnHash
end
it 'inherits key coercions' do
expect(MyOwnHash.key_coercions).to eql(MyOwnSubclass.key_coercions)
end
it 'the superclass does not accumulate coerced attributes from subclasses' do
expect(MyOwnBase.key_coercions).to eq({})
end
end
context 'when using circular coercion' do
context 'with a proc on one side' do
class CategoryHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
coerce_key :products, lambda { |value|
return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map)
ProductHash.new(v)
}
end
class ProductHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
coerce_key :categories, Array[CategoryHash]
end
let(:category) do
CategoryHash.new(type: 'rubygem', products: [Hashie::Mash.new(name: 'Hashie')])
end
let(:product) do
ProductHash.new(name: 'Hashie', categories: [Hashie::Mash.new(type: 'rubygem')])
end
it 'coerces CategoryHash[:products] correctly' do
expected = [ProductHash]
actual = category[:products].map(&:class)
expect(actual).to eq(expected)
end
it 'coerces ProductHash[:categories] correctly' do
expected = [CategoryHash]
actual = product[:categories].map(&:class)
expect(actual).to eq(expected)
end
end
context 'without a proc on either side' do
it 'fails with a NameError since the other class is not defined yet' do
attempted_code = lambda do
class AnotherCategoryHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
coerce_key :products, Array[AnotherProductHash]
end
class AnotherProductHash < Hash
include Hashie::Extensions::Coercion
include Hashie::Extensions::MergeInitializer
coerce_key :categories, Array[AnotherCategoryHash]
end
end
expect { attempted_code.call }.to raise_error(NameError)
end
end
end
end
describe '#coerce_value' do
context 'with strict: true' do
it 'coerces any value of the exact right class' do
subject.coerce_value String, Coercable
instance[:foo] = 'bar'
instance[:bar] = 'bax'
instance[:hi] = :bye
expect(instance[:foo]).to be_coerced
expect(instance[:bar]).to be_coerced
expect(instance[:hi]).not_to respond_to(:coerced?)
end
it 'coerces values from a #replace call' do
subject.coerce_value String, Coercable
instance[:foo] = :bar
instance.replace(foo: 'bar', bar: 'bax')
expect(instance[:foo]).to be_coerced
expect(instance[:bar]).to be_coerced
end
it 'does not coerce superclasses' do
klass = Class.new(String)
subject.coerce_value klass, Coercable
instance[:foo] = 'bar'
expect(instance[:foo]).not_to be_kind_of(Coercable)
instance[:foo] = klass.new
expect(instance[:foo]).to be_kind_of(Coercable)
end
end
context 'core types' do
it 'coerces String to Integer when possible' do
subject.coerce_value String, Integer
instance[:foo] = '2'
instance[:bar] = '2.7'
instance[:hi] = 'hi'
expect(instance[:foo]).to be_a(Integer)
expect(instance[:foo]).to eq(2)
expect(instance[:bar]).to be_a(Integer)
expect(instance[:bar]).to eq(2)
expect(instance[:hi]).to be_a(Integer)
expect(instance[:hi]).to eq(0) # not what I expected...
end
it 'coerces non-numeric from String to Integer' do
# This was surprising, but I guess it's "correct"
# unless there is a stricter `to_i` alternative
subject.coerce_value String, Integer
instance[:hi] = 'hi'
expect(instance[:hi]).to be_a(Integer)
expect(instance[:hi]).to eq(0)
end
it 'raises a CoercionError when coercion is not possible' do
type = Integer
subject.coerce_value type, Symbol
expect { instance[:hi] = 1 }.to raise_error(
Hashie::CoercionError, /Cannot coerce property :hi from #{type} to Symbol/
)
end
it 'coerces Integer to String' do
type = Integer
subject.coerce_value type, String
{
fixnum: 2,
bignum: 12_345_667_890_987_654_321,
float: 2.7,
rational: Rational(2, 3),
complex: Complex(1)
}.each do |k, v|
instance[k] = v
if v.is_a? type
expect(instance[k]).to be_a(String)
expect(instance[k]).to eq(v.to_s)
else
expect(instance[k]).to_not be_a(String)
expect(instance[k]).to eq(v)
end
end
end
it 'coerces Numeric to String' do
subject.coerce_value Numeric, String
{
fixnum: 2,
bignum: 12_345_667_890_987_654_321,
float: 2.7,
rational: Rational(2, 3),
complex: Complex(1)
}.each do |k, v|
instance[k] = v
expect(instance[k]).to be_a(String)
expect(instance[k]).to eq(v.to_s)
end
end
it 'can coerce via a proc' do
subject.coerce_value(String, lambda do |v|
return !!(v =~ /^(true|t|yes|y|1)$/i)
end)
true_values = %w[true t yes y 1]
false_values = %w[false f no n 0]
true_values.each do |v|
instance[:foo] = v
expect(instance[:foo]).to be_a(TrueClass)
end
false_values.each do |v|
instance[:foo] = v
expect(instance[:foo]).to be_a(FalseClass)
end
end
end
end
after(:each) do
Object.send(:remove_const, :ExampleCoercableHash)
end
end