hashie 3.2.0 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Hashie
2
- VERSION = '3.2.0'
2
+ VERSION = '3.3.1'
3
3
  end
@@ -1,16 +1,20 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Hashie::Extensions::Coercion do
4
+ class NotInitializable
5
+ private_class_method :new
6
+ end
7
+
4
8
  class Initializable
5
9
  attr_reader :coerced, :value
6
10
 
7
- def initialize(obj, coerced = false)
11
+ def initialize(obj, coerced = nil)
8
12
  @coerced = coerced
9
13
  @value = obj.class.to_s
10
14
  end
11
15
 
12
16
  def coerced?
13
- !!@coerced
17
+ !@coerced.nil?
14
18
  end
15
19
  end
16
20
 
@@ -32,6 +36,84 @@ describe Hashie::Extensions::Coercion do
32
36
  let(:instance) { subject.new }
33
37
 
34
38
  describe '#coerce_key' do
39
+ context 'nesting' do
40
+ class BaseCoercableHash < Hash
41
+ include Hashie::Extensions::Coercion
42
+ include Hashie::Extensions::MergeInitializer
43
+ end
44
+
45
+ class NestedCoercableHash < BaseCoercableHash
46
+ coerce_key :foo, String
47
+ coerce_key :bar, Integer
48
+ end
49
+
50
+ class RootCoercableHash < BaseCoercableHash
51
+ coerce_key :nested, NestedCoercableHash
52
+ coerce_key :nested_list, Array[NestedCoercableHash]
53
+ coerce_key :nested_hash, Hash[String => NestedCoercableHash]
54
+ end
55
+
56
+ def test_nested_object(obj)
57
+ expect(obj).to be_a(NestedCoercableHash)
58
+ expect(obj[:foo]).to be_a(String)
59
+ expect(obj[:bar]).to be_an(Integer)
60
+ end
61
+
62
+ subject { RootCoercableHash }
63
+ let(:instance) { subject.new }
64
+
65
+ it 'coerces nested objects' do
66
+ instance[:nested] = { foo: 123, bar: '456' }
67
+ test_nested_object(instance[:nested])
68
+ end
69
+
70
+ it 'coerces nested arrays' do
71
+ instance[:nested_list] = [
72
+ { foo: 123, bar: '456' },
73
+ { foo: 234, bar: '567' },
74
+ { foo: 345, bar: '678' }
75
+ ]
76
+ expect(instance[:nested_list]).to be_a Array
77
+ expect(instance[:nested_list].size).to eq(3)
78
+ instance[:nested_list].each do | nested |
79
+ test_nested_object nested
80
+ end
81
+ end
82
+
83
+ it 'coerces nested hashes' do
84
+ instance[:nested_hash] = {
85
+ a: { foo: 123, bar: '456' },
86
+ b: { foo: 234, bar: '567' },
87
+ c: { foo: 345, bar: '678' }
88
+ }
89
+ expect(instance[:nested_hash]).to be_a Hash
90
+ expect(instance[:nested_hash].size).to eq(3)
91
+ instance[:nested_hash].each do | key, nested |
92
+ expect(key).to be_a(String)
93
+ test_nested_object nested
94
+ end
95
+ end
96
+
97
+ context 'when repetitively including the module' do
98
+ class RepetitiveCoercableHash < NestedCoercableHash
99
+ include Hashie::Extensions::Coercion
100
+ include Hashie::Extensions::MergeInitializer
101
+
102
+ coerce_key :nested, NestedCoercableHash
103
+ end
104
+
105
+ subject { RepetitiveCoercableHash }
106
+ let(:instance) { subject.new }
107
+
108
+ it 'does not raise a stack overflow error' do
109
+ expect do
110
+ instance[:nested] = { foo: 123, bar: '456' }
111
+ test_nested_object(instance[:nested])
112
+ end.not_to raise_error
113
+ end
114
+ end
115
+ end
116
+
35
117
  it { expect(subject).to be_respond_to(:coerce_key) }
36
118
 
37
119
  it 'runs through coerce on a specified key' do
@@ -41,6 +123,13 @@ describe Hashie::Extensions::Coercion do
41
123
  expect(instance[:foo]).to be_coerced
42
124
  end
43
125
 
126
+ it 'skips unnecessary coercions' do
127
+ subject.coerce_key :foo, Coercable
128
+
129
+ instance[:foo] = Coercable.new('bar')
130
+ expect(instance[:foo]).to_not be_coerced
131
+ end
132
+
44
133
  it 'supports an array of keys' do
45
134
  subject.coerce_keys :foo, :bar, Coercable
46
135
 
@@ -92,6 +181,116 @@ describe Hashie::Extensions::Coercion do
92
181
  expect(instance[:foo].keys).to all(be_coerced)
93
182
  end
94
183
 
184
+ context 'coercing core types' do
185
+ def test_coercion(literal, target_type, coerce_method)
186
+ subject.coerce_key :foo, target_type
187
+ instance[:foo] = literal
188
+ expect(instance[:foo]).to be_a(target_type)
189
+ expect(instance[:foo]).to eq(literal.send(coerce_method))
190
+ end
191
+
192
+ RSpec.shared_examples 'coerces from numeric types' do |target_type, coerce_method|
193
+ it "coerces from String to #{target_type} via #{coerce_method}" do
194
+ test_coercion '2.0', target_type, coerce_method
195
+ end
196
+
197
+ it "coerces from Integer to #{target_type} via #{coerce_method}" do
198
+ # Fixnum
199
+ test_coercion 2, target_type, coerce_method
200
+ # Bignum
201
+ test_coercion 12_345_667_890_987_654_321, target_type, coerce_method
202
+ end
203
+
204
+ it "coerces from Rational to #{target_type} via #{coerce_method}" do
205
+ test_coercion Rational(2, 3), target_type, coerce_method
206
+ end
207
+ end
208
+
209
+ RSpec.shared_examples 'coerces from alphabetical types' do |target_type, coerce_method|
210
+ it "coerces from String to #{target_type} via #{coerce_method}" do
211
+ test_coercion 'abc', target_type, coerce_method
212
+ end
213
+
214
+ it "coerces from Symbol to #{target_type} via #{coerce_method}" do
215
+ test_coercion :abc, target_type, coerce_method
216
+ end
217
+ end
218
+
219
+ include_examples 'coerces from numeric types', Integer, :to_i
220
+ include_examples 'coerces from numeric types', Float, :to_f
221
+ include_examples 'coerces from numeric types', String, :to_s
222
+
223
+ include_examples 'coerces from alphabetical types', String, :to_s
224
+ include_examples 'coerces from alphabetical types', Symbol, :to_sym
225
+
226
+ it 'can coerce String to Rational when possible' do
227
+ test_coercion '2/3', Rational, :to_r
228
+ end
229
+
230
+ it 'can coerce String to Complex when possible' do
231
+ test_coercion '2/3+3/4i', Complex, :to_c
232
+ end
233
+
234
+ it 'coerces collections with core types' do
235
+ subject.coerce_key :foo, Hash[String => String]
236
+
237
+ instance[:foo] = {
238
+ abc: 123,
239
+ xyz: 987
240
+ }
241
+ expect(instance[:foo]).to eq(
242
+ 'abc' => '123',
243
+ 'xyz' => '987'
244
+ )
245
+ end
246
+
247
+ it 'can coerce via a proc' do
248
+ subject.coerce_key(:foo, lambda do |v|
249
+ case v
250
+ when String
251
+ return !!(v =~ /^(true|t|yes|y|1)$/i)
252
+ when Numeric
253
+ return !v.to_i.zero?
254
+ else
255
+ return v == true
256
+ end
257
+ end)
258
+
259
+ true_values = [true, 'true', 't', 'yes', 'y', '1', 1, -1]
260
+ false_values = [false, 'false', 'f', 'no', 'n', '0', 0]
261
+
262
+ true_values.each do |v|
263
+ instance[:foo] = v
264
+ expect(instance[:foo]).to be_a(TrueClass)
265
+ end
266
+ false_values.each do |v|
267
+ instance[:foo] = v
268
+ expect(instance[:foo]).to be_a(FalseClass)
269
+ end
270
+ end
271
+
272
+ it 'raises errors for non-coercable types' do
273
+ subject.coerce_key :foo, NotInitializable
274
+ expect { instance[:foo] = 'true' }.to raise_error(Hashie::CoercionError, /NotInitializable is not a coercable type/)
275
+ end
276
+
277
+ it 'can coerce false' do
278
+ subject.coerce_key :foo, Coercable
279
+
280
+ instance[:foo] = false
281
+ expect(instance[:foo]).to be_coerced
282
+ expect(instance[:foo].value).to eq('FalseClass')
283
+ end
284
+
285
+ it 'does not coerce nil' do
286
+ subject.coerce_key :foo, String
287
+
288
+ instance[:foo] = nil
289
+ expect(instance[:foo]).to_not eq('')
290
+ expect(instance[:foo]).to be_nil
291
+ end
292
+ end
293
+
95
294
  it 'calls #new if no coerce method is available' do
96
295
  subject.coerce_key :foo, Initializable
97
296
 
@@ -239,6 +438,91 @@ describe Hashie::Extensions::Coercion do
239
438
  expect(instance[:foo]).to be_kind_of(Coercable)
240
439
  end
241
440
  end
441
+
442
+ context 'core types' do
443
+ it 'coerces String to Integer when possible' do
444
+ subject.coerce_value String, Integer
445
+
446
+ instance[:foo] = '2'
447
+ instance[:bar] = '2.7'
448
+ instance[:hi] = 'hi'
449
+ expect(instance[:foo]).to be_a(Integer)
450
+ expect(instance[:foo]).to eq(2)
451
+ expect(instance[:bar]).to be_a(Integer)
452
+ expect(instance[:bar]).to eq(2)
453
+ expect(instance[:hi]).to be_a(Integer)
454
+ expect(instance[:hi]).to eq(0) # not what I expected...
455
+ end
456
+
457
+ it 'coerces non-numeric from String to Integer' do
458
+ # This was surprising, but I guess it's "correct"
459
+ # unless there is a stricter `to_i` alternative
460
+ subject.coerce_value String, Integer
461
+ instance[:hi] = 'hi'
462
+ expect(instance[:hi]).to be_a(Integer)
463
+ expect(instance[:hi]).to eq(0)
464
+ end
465
+
466
+ it 'raises a CoercionError when coercion is not possible' do
467
+ subject.coerce_value Fixnum, Symbol
468
+ expect { instance[:hi] = 1 }.to raise_error(Hashie::CoercionError, /Cannot coerce property :hi from Fixnum to Symbol/)
469
+ end
470
+
471
+ it 'coerces Integer to String' do
472
+ subject.coerce_value Integer, String
473
+
474
+ {
475
+ fixnum: 2,
476
+ bignum: 12_345_667_890_987_654_321,
477
+ float: 2.7,
478
+ rational: Rational(2, 3),
479
+ complex: Complex(1)
480
+ }.each do | k, v |
481
+ instance[k] = v
482
+ if v.is_a? Integer
483
+ expect(instance[k]).to be_a(String)
484
+ expect(instance[k]).to eq(v.to_s)
485
+ else
486
+ expect(instance[k]).to_not be_a(String)
487
+ expect(instance[k]).to eq(v)
488
+ end
489
+ end
490
+ end
491
+
492
+ it 'coerces Numeric to String' do
493
+ subject.coerce_value Numeric, String
494
+
495
+ {
496
+ fixnum: 2,
497
+ bignum: 12_345_667_890_987_654_321,
498
+ float: 2.7,
499
+ rational: Rational(2, 3),
500
+ complex: Complex(1)
501
+ }.each do | k, v |
502
+ instance[k] = v
503
+ expect(instance[k]).to be_a(String)
504
+ expect(instance[k]).to eq(v.to_s)
505
+ end
506
+ end
507
+
508
+ it 'can coerce via a proc' do
509
+ subject.coerce_value(String, lambda do |v|
510
+ return !!(v =~ /^(true|t|yes|y|1)$/i)
511
+ end)
512
+
513
+ true_values = %w(true t yes y 1)
514
+ false_values = %w(false f no n 0)
515
+
516
+ true_values.each do |v|
517
+ instance[:foo] = v
518
+ expect(instance[:foo]).to be_a(TrueClass)
519
+ end
520
+ false_values.each do |v|
521
+ instance[:foo] = v
522
+ expect(instance[:foo]).to be_a(FalseClass)
523
+ end
524
+ end
525
+ end
242
526
  end
243
527
 
244
528
  after(:each) do
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Hashie::Extensions::Dash::IndifferentAccess do
4
4
  class TrashWithIndifferentAccess < Hashie::Trash
5
5
  include Hashie::Extensions::Dash::IndifferentAccess
6
- property :per_page, transform_with: lambda { |v| v.to_i }
6
+ property :per_page, transform_with: ->(v) { v.to_i }
7
7
  property :total, from: :total_pages
8
8
  end
9
9
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::DeepFind do
4
+ subject { Class.new(Hash) { include Hashie::Extensions::DeepFind } }
5
+ let(:hash) do
6
+ {
7
+ library: {
8
+ books: [
9
+ { title: 'Call of the Wild' },
10
+ { title: 'Moby Dick' }
11
+ ],
12
+ shelves: nil,
13
+ location: {
14
+ address: '123 Library St.',
15
+ title: 'Main Library'
16
+ }
17
+ }
18
+ }
19
+ end
20
+ let(:instance) { subject.new.update(hash) }
21
+
22
+ describe '#deep_find' do
23
+ it 'detects a value from a nested hash' do
24
+ expect(instance.deep_find(:address)).to eq('123 Library St.')
25
+ end
26
+
27
+ it 'detects a value from a nested array' do
28
+ expect(instance.deep_find(:title)).to eq('Call of the Wild')
29
+ end
30
+
31
+ it 'returns nil if it does not find a match' do
32
+ expect(instance.deep_find(:wahoo)).to be_nil
33
+ end
34
+ end
35
+
36
+ describe '#deep_find_all' do
37
+ it 'detects all values from a nested hash' do
38
+ expect(instance.deep_find_all(:title)).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
39
+ end
40
+
41
+ it 'returns nil if it does not find any matches' do
42
+ expect(instance.deep_find_all(:wahoo)).to be_nil
43
+ end
44
+ end
45
+ end
@@ -52,6 +52,30 @@ describe Hashie::Extensions::IndifferentAccess do
52
52
  h = subject.build(:foo => 'bar', 'baz' => 'qux')
53
53
  expect(h.values_at('foo', :baz)).to eq %w(bar qux)
54
54
  end
55
+
56
+ it 'returns the same instance of the hash that was set' do
57
+ hash = Hash.new
58
+ h = subject.build(foo: hash)
59
+ expect(h.values_at(:foo)[0]).to be(hash)
60
+ end
61
+
62
+ it 'returns the same instance of the array that was set' do
63
+ array = Array.new
64
+ h = subject.build(foo: array)
65
+ expect(h.values_at(:foo)[0]).to be(array)
66
+ end
67
+
68
+ it 'returns the same instance of the string that was set' do
69
+ str = 'my string'
70
+ h = subject.build(foo: str)
71
+ expect(h.values_at(:foo)[0]).to be(str)
72
+ end
73
+
74
+ it 'returns the same instance of the object that was set' do
75
+ object = Object.new
76
+ h = subject.build(foo: object)
77
+ expect(h.values_at(:foo)[0]).to be(object)
78
+ end
55
79
  end
56
80
 
57
81
  describe '#fetch' do
@@ -60,6 +84,30 @@ describe Hashie::Extensions::IndifferentAccess do
60
84
  expect(h.fetch(:foo)).to eq h.fetch('foo')
61
85
  expect(h.fetch(:foo)).to eq 'bar'
62
86
  end
87
+
88
+ it 'returns the same instance of the hash that was set' do
89
+ hash = Hash.new
90
+ h = subject.build(foo: hash)
91
+ expect(h.fetch(:foo)).to be(hash)
92
+ end
93
+
94
+ it 'returns the same instance of the array that was set' do
95
+ array = Array.new
96
+ h = subject.build(foo: array)
97
+ expect(h.fetch(:foo)).to be(array)
98
+ end
99
+
100
+ it 'returns the same instance of the string that was set' do
101
+ str = 'my string'
102
+ h = subject.build(foo: str)
103
+ expect(h.fetch(:foo)).to be(str)
104
+ end
105
+
106
+ it 'returns the same instance of the object that was set' do
107
+ object = Object.new
108
+ h = subject.build(foo: object)
109
+ expect(h.fetch(:foo)).to be(object)
110
+ end
63
111
  end
64
112
 
65
113
  describe '#delete' do