hashie 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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