637 lines
18 KiB
Ruby
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
|