contracts 0.9 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,44 +1,59 @@
1
1
  module Contracts
2
2
  module Support
3
- def self.method_position(method)
4
- return method.method_position if method.is_a?(MethodReference)
3
+ class << self
4
+ def method_position(method)
5
+ return method.method_position if method.is_a?(MethodReference)
5
6
 
6
- if RUBY_VERSION =~ /^1\.8/
7
- if method.respond_to?(:__file__)
8
- method.__file__ + ":" + method.__line__.to_s
7
+ if RUBY_VERSION =~ /^1\.8/
8
+ if method.respond_to?(:__file__)
9
+ method.__file__ + ":" + method.__line__.to_s
10
+ else
11
+ method.inspect
12
+ end
9
13
  else
10
- method.inspect
14
+ file, line = method.source_location
15
+ file + ":" + line.to_s
11
16
  end
12
- else
13
- file, line = method.source_location
14
- file + ":" + line.to_s
15
17
  end
16
- end
17
18
 
18
- def self.method_name(method)
19
- method.is_a?(Proc) ? "Proc" : method.name
20
- end
19
+ def method_name(method)
20
+ method.is_a?(Proc) ? "Proc" : method.name
21
+ end
21
22
 
22
- # Generates unique id, which can be used as a part of identifier
23
- #
24
- # Example:
25
- # Contracts::Support.unique_id # => "i53u6tiw5hbo"
26
- def self.unique_id
27
- # Consider using SecureRandom.hex here, and benchmark which one is better
28
- (Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
29
- end
23
+ # Generates unique id, which can be used as a part of identifier
24
+ #
25
+ # Example:
26
+ # Contracts::Support.unique_id # => "i53u6tiw5hbo"
27
+ def unique_id
28
+ # Consider using SecureRandom.hex here, and benchmark which one is better
29
+ (Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
30
+ end
30
31
 
31
- def self.eigenclass_hierarchy_supported?
32
- return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
33
- RUBY_VERSION.to_f > 1.8
34
- end
32
+ def contract_id(contract)
33
+ contract.object_id
34
+ end
35
35
 
36
- def self.eigenclass_of(target)
37
- class << target; self; end
38
- end
36
+ def eigenclass_hierarchy_supported?
37
+ return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
38
+ RUBY_VERSION.to_f > 1.8
39
+ end
40
+
41
+ def eigenclass_of(target)
42
+ class << target; self; end
43
+ end
39
44
 
40
- def self.eigenclass?(target)
41
- target <= eigenclass_of(Object)
45
+ def eigenclass?(target)
46
+ module_eigenclass?(target) ||
47
+ target <= eigenclass_of(Object)
48
+ end
49
+
50
+ private
51
+
52
+ # Module eigenclass can be detected by its ancestor chain
53
+ # containing a Module
54
+ def module_eigenclass?(target)
55
+ target < Module
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -0,0 +1,127 @@
1
+ module Contracts
2
+ module Validators
3
+ DEFAULT_VALIDATOR_STRATEGIES = {
4
+ # e.g. lambda {true}
5
+ Proc => lambda { |contract| contract },
6
+
7
+ # e.g. [Num, String]
8
+ # TODO: account for these errors too
9
+ Array => lambda do |contract|
10
+ lambda do |arg|
11
+ return false unless arg.is_a?(Array) && arg.length == contract.length
12
+ arg.zip(contract).all? do |_arg, _contract|
13
+ Contract.valid?(_arg, _contract)
14
+ end
15
+ end
16
+ end,
17
+
18
+ # e.g. { :a => Num, :b => String }
19
+ Hash => lambda do |contract|
20
+ lambda do |arg|
21
+ return false unless arg.is_a?(Hash)
22
+ contract.keys.all? do |k|
23
+ Contract.valid?(arg[k], contract[k])
24
+ end
25
+ end
26
+ end,
27
+
28
+ Contracts::Args => lambda do |contract|
29
+ lambda do |arg|
30
+ Contract.valid?(arg, contract.contract)
31
+ end
32
+ end,
33
+
34
+ Contracts::Func => lambda do |_|
35
+ lambda do |arg|
36
+ arg.is_a?(Method) || arg.is_a?(Proc)
37
+ end
38
+ end,
39
+
40
+ :valid => lambda do |contract|
41
+ lambda { |arg| contract.valid?(arg) }
42
+ end,
43
+
44
+ :class => lambda do |contract|
45
+ lambda { |arg| arg.is_a?(contract) }
46
+ end,
47
+
48
+ :default => lambda do |contract|
49
+ lambda { |arg| contract == arg }
50
+ end
51
+ }.freeze
52
+
53
+ # Allows to override validator with custom one.
54
+ # Example:
55
+ # Contract.override_validator(Array) do |contract|
56
+ # lambda do |arg|
57
+ # # .. implementation for Array contract ..
58
+ # end
59
+ # end
60
+ #
61
+ # Contract.override_validator(:class) do |contract|
62
+ # lambda do |arg|
63
+ # arg.is_a?(contract) || arg.is_a?(RSpec::Mocks::Double)
64
+ # end
65
+ # end
66
+ def override_validator(name, &block)
67
+ validator_strategies[name] = block
68
+ end
69
+
70
+ # This is a little weird. For each contract
71
+ # we pre-make a proc to validate it so we
72
+ # don't have to go through this decision tree every time.
73
+ # Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
74
+ def make_validator!(contract)
75
+ klass = contract.class
76
+ key = if validator_strategies.key?(klass)
77
+ klass
78
+ else
79
+ if contract.respond_to? :valid?
80
+ :valid
81
+ elsif klass == Class || klass == Module
82
+ :class
83
+ else
84
+ :default
85
+ end
86
+ end
87
+
88
+ validator_strategies[key].call(contract)
89
+ end
90
+
91
+ def make_validator(contract)
92
+ contract_id = Support.contract_id(contract)
93
+
94
+ if memoized_validators.key?(contract_id)
95
+ return memoized_validators[contract_id]
96
+ end
97
+
98
+ memoized_validators[contract_id] = make_validator!(contract)
99
+ end
100
+
101
+ # @private
102
+ def reset_validators
103
+ clean_memoized_validators
104
+ restore_validators
105
+ end
106
+
107
+ # @private
108
+ def validator_strategies
109
+ @_validator_strategies ||= restore_validators
110
+ end
111
+
112
+ # @private
113
+ def restore_validators
114
+ @_validator_strategies = DEFAULT_VALIDATOR_STRATEGIES.dup
115
+ end
116
+
117
+ # @private
118
+ def memoized_validators
119
+ @_memoized_validators ||= clean_memoized_validators
120
+ end
121
+
122
+ # @private
123
+ def clean_memoized_validators
124
+ @_memoized_validators = {}
125
+ end
126
+ end
127
+ end
@@ -1,3 +1,3 @@
1
1
  module Contracts
2
- VERSION = "0.9"
2
+ VERSION = "0.10"
3
3
  end
@@ -12,14 +12,18 @@ RSpec.describe "Contracts:" do
12
12
  expect { @o.double(2.2) }.to_not raise_error
13
13
  end
14
14
 
15
- it "should fail for Strings" do
16
- expect { @o.double("bad") }.to raise_error(ContractError)
15
+ it "should fail for nil and other data types" do
16
+ expect { @o.double(nil) }.to raise_error(ContractError)
17
+ expect { @o.double(:x) }.to raise_error(ContractError)
18
+ expect { @o.double("x") }.to raise_error(ContractError)
19
+ expect { @o.double(/x/) }.to raise_error(ContractError)
17
20
  end
18
21
  end
19
22
 
20
23
  describe "Pos:" do
21
24
  it "should pass for positive numbers" do
22
25
  expect { @o.pos_test(1) }.to_not raise_error
26
+ expect { @o.pos_test(1.6) }.to_not raise_error
23
27
  end
24
28
 
25
29
  it "should fail for 0" do
@@ -28,12 +32,21 @@ RSpec.describe "Contracts:" do
28
32
 
29
33
  it "should fail for negative numbers" do
30
34
  expect { @o.pos_test(-1) }.to raise_error(ContractError)
35
+ expect { @o.pos_test(-1.6) }.to raise_error(ContractError)
36
+ end
37
+
38
+ it "should fail for nil and other data types" do
39
+ expect { @o.pos_test(nil) }.to raise_error(ContractError)
40
+ expect { @o.pos_test(:x) }.to raise_error(ContractError)
41
+ expect { @o.pos_test("x") }.to raise_error(ContractError)
42
+ expect { @o.pos_test(/x/) }.to raise_error(ContractError)
31
43
  end
32
44
  end
33
45
 
34
46
  describe "Neg:" do
35
47
  it "should pass for negative numbers" do
36
48
  expect { @o.neg_test(-1) }.to_not raise_error
49
+ expect { @o.neg_test(-1.6) }.to_not raise_error
37
50
  end
38
51
 
39
52
  it "should fail for 0" do
@@ -42,6 +55,14 @@ RSpec.describe "Contracts:" do
42
55
 
43
56
  it "should fail for positive numbers" do
44
57
  expect { @o.neg_test(1) }.to raise_error(ContractError)
58
+ expect { @o.neg_test(1.6) }.to raise_error(ContractError)
59
+ end
60
+
61
+ it "should fail for nil and other data types" do
62
+ expect { @o.neg_test(nil) }.to raise_error(ContractError)
63
+ expect { @o.neg_test(:x) }.to raise_error(ContractError)
64
+ expect { @o.neg_test("x") }.to raise_error(ContractError)
65
+ expect { @o.neg_test(/x/) }.to raise_error(ContractError)
45
66
  end
46
67
  end
47
68
 
@@ -60,6 +81,14 @@ RSpec.describe "Contracts:" do
60
81
 
61
82
  it "should fail for negative numbers" do
62
83
  expect { @o.nat_test(-1) }.to raise_error(ContractError)
84
+ expect { @o.nat_test(-1.6) }.to raise_error(ContractError)
85
+ end
86
+
87
+ it "should fail for nil and other data types" do
88
+ expect { @o.nat_test(nil) }.to raise_error(ContractError)
89
+ expect { @o.nat_test(:x) }.to raise_error(ContractError)
90
+ expect { @o.nat_test("x") }.to raise_error(ContractError)
91
+ expect { @o.nat_test(/x/) }.to raise_error(ContractError)
63
92
  end
64
93
  end
65
94
 
@@ -215,6 +244,45 @@ RSpec.describe "Contracts:" do
215
244
  end
216
245
  end
217
246
 
247
+ describe "RangeOf:" do
248
+ require "date"
249
+ it "should pass for a range of nums" do
250
+ expect { @o.first_in_range_num(3..10) }.to_not raise_error
251
+ end
252
+
253
+ it "should pass for a range of dates" do
254
+ d1 = Date.today
255
+ d2 = d1 + 18
256
+ expect { @o.first_in_range_date(d1..d2) }.to_not raise_error
257
+ end
258
+
259
+ it "should fail for a non-range" do
260
+ expect { @o.first_in_range_num("foo") }.to raise_error(ContractError)
261
+ expect { @o.first_in_range_num(:foo) }.to raise_error(ContractError)
262
+ expect { @o.first_in_range_num(5) }.to raise_error(ContractError)
263
+ expect { @o.first_in_range_num(nil) }.to raise_error(ContractError)
264
+ end
265
+
266
+ it "should fail for a range with incorrect data type" do
267
+ expect { @o.first_in_range_num("a".."z") }.to raise_error(ContractError)
268
+ end
269
+
270
+ it "should fail for a badly-defined range" do
271
+ # For some reason, Ruby 2.0.0 allows (date .. number) as a range.
272
+ # Perhaps other Ruby versions do too.
273
+ # Note that (date .. string) gives ArgumentError.
274
+ # This test guards against ranges with inconsistent data types.
275
+ begin
276
+ d1 = Date.today
277
+ expect { @o.first_in_range_date(d1..10).to raise_error(ContractError) }
278
+ expect { @o.first_in_range_num(d1..10).to raise_error(ContractError) }
279
+ rescue ArgumentError
280
+ # If Ruby doesn't like the range, we ignore the test.
281
+ :nop
282
+ end
283
+ end
284
+ end
285
+
218
286
  describe "SetOf:" do
219
287
  it "should pass for a set of nums" do
220
288
  expect { @o.product_from_set(Set.new([1, 2, 3])) }.to_not raise_error
@@ -254,6 +322,14 @@ RSpec.describe "Contracts:" do
254
322
  end
255
323
  end
256
324
 
325
+ describe "Optional:" do
326
+ it "can't be used outside of KeywordArgs" do
327
+ expect do
328
+ BareOptionalContractUsed.new.something(3, 5)
329
+ end.to raise_error(ArgumentError, Contracts::Optional::UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH)
330
+ end
331
+ end
332
+
257
333
  describe "HashOf:" do
258
334
  it "doesn't allow to specify multiple key-value pairs with pretty syntax" do
259
335
  expect do
@@ -250,7 +250,6 @@ RSpec.describe "Contracts:" do
250
250
  let(:mod) do
251
251
  Module.new do
252
252
  include Contracts
253
- include Contracts::Modules
254
253
 
255
254
  Contract String => String
256
255
  def greeting(name)
@@ -404,6 +403,26 @@ RSpec.describe "Contracts:" do
404
403
  @o.double_with_proc(4)
405
404
  end.to raise_error(ContractError, /Actual: nil/)
406
405
  end
406
+
407
+ it "should succeed for maybe proc with no proc" do
408
+ expect do
409
+ @o.maybe_call(5)
410
+ end.to_not raise_error
411
+ end
412
+
413
+ it "should succeed for maybe proc with proc" do
414
+ expect do
415
+ @o.maybe_call(5) do
416
+ 2 + 2
417
+ end
418
+ end.to_not raise_error
419
+ end
420
+
421
+ it "should fail for maybe proc with invalid input" do
422
+ expect do
423
+ @o.maybe_call("bad")
424
+ end.to raise_error(ContractError)
425
+ end
407
426
  end
408
427
 
409
428
  describe "varargs" do
@@ -489,6 +508,18 @@ RSpec.describe "Contracts:" do
489
508
  it "should fail for a function that doesn't pass the contract with weak other args" do
490
509
  expect { @o.map_plain(["hello", "joe"], lambda { |_| nil }) }.to raise_error(ContractError)
491
510
  end
511
+
512
+ it "should fail for a returned function that doesn't pass the contract" do
513
+ expect { @o.lambda_with_wrong_return.call("hello") }.to raise_error(ContractError)
514
+ end
515
+
516
+ it "should fail for a returned function that receives the wrong argument type" do
517
+ expect { @o.lambda_with_correct_return.call(123) }.to raise_error(ContractError)
518
+ end
519
+
520
+ it "should not fail for a returned function that passes the contract" do
521
+ expect { @o.lambda_with_correct_return.call("hello") }.to_not raise_error
522
+ end
492
523
  end
493
524
 
494
525
  describe "default args to functions" do
@@ -536,6 +567,38 @@ RSpec.describe "Contracts:" do
536
567
  end
537
568
  end
538
569
 
570
+ describe "module contracts" do
571
+ it "passes for instance of class including module" do
572
+ expect(
573
+ ModuleContractExample.hello(ModuleContractExample::AClassWithModule.new)
574
+ ).to eq(:world)
575
+ end
576
+
577
+ it "passes for instance of class including inherited module" do
578
+ expect(
579
+ ModuleContractExample.hello(ModuleContractExample::AClassWithInheritedModule.new)
580
+ ).to eq(:world)
581
+ end
582
+
583
+ it "does not pass for instance of class not including module" do
584
+ expect do
585
+ ModuleContractExample.hello(ModuleContractExample::AClassWithoutModule.new)
586
+ end.to raise_error(ContractError, /Expected: ModuleContractExample::AModule/)
587
+ end
588
+
589
+ it "does not pass for instance of class including another module" do
590
+ expect do
591
+ ModuleContractExample.hello(ModuleContractExample::AClassWithAnotherModule.new)
592
+ end.to raise_error(ContractError, /Expected: ModuleContractExample::AModule/)
593
+ end
594
+
595
+ it "passes for instance of class including both modules" do
596
+ expect(
597
+ ModuleContractExample.hello(ModuleContractExample::AClassWithBothModules.new)
598
+ ).to eq(:world)
599
+ end
600
+ end
601
+
539
602
  describe "Contracts to_s formatting in expected" do
540
603
  def not_s(match)
541
604
  Regexp.new "[^\"\']#{match}[^\"\']"
@@ -1,3 +1,5 @@
1
+ require "date"
2
+
1
3
  class A
2
4
  include Contracts
3
5
 
@@ -120,8 +122,13 @@ class GenericExample
120
122
  end
121
123
 
122
124
  Contract Proc => Any
123
- def do_call(&blk)
124
- blk.call
125
+ def do_call(&block)
126
+ block.call
127
+ end
128
+
129
+ Contract Args[Num], Maybe[Proc] => Any
130
+ def maybe_call(*vals, &block)
131
+ block.call if block
125
132
  end
126
133
 
127
134
  Contract Args[Num] => Num
@@ -219,6 +226,16 @@ class GenericExample
219
226
  end
220
227
  end
221
228
 
229
+ Contract RangeOf[Num] => Num
230
+ def first_in_range_num(r)
231
+ r.first
232
+ end
233
+
234
+ Contract RangeOf[Date] => Date
235
+ def first_in_range_date(r)
236
+ r.first
237
+ end
238
+
222
239
  Contract Bool => nil
223
240
  def bool_test(x)
224
241
  end
@@ -262,6 +279,16 @@ class GenericExample
262
279
  end
263
280
  end
264
281
 
282
+ Contract None => Func[String => Num]
283
+ def lambda_with_wrong_return
284
+ lambda { |x| x }
285
+ end
286
+
287
+ Contract None => Func[String => Num]
288
+ def lambda_with_correct_return
289
+ lambda { |x| x.length }
290
+ end
291
+
265
292
  Contract Num => Num
266
293
  def default_args(x = 1)
267
294
  2
@@ -528,10 +555,7 @@ with_enabled_no_contracts do
528
555
  end
529
556
 
530
557
  module ModuleExample
531
- # This inclusion is required to actually override `method_added`
532
- # hooks for module.
533
558
  include Contracts
534
- include Contracts::Modules
535
559
 
536
560
  Contract Num, Num => Num
537
561
  def plus(a, b)
@@ -567,3 +591,51 @@ end
567
591
 
568
592
  class SingletonInheritanceExampleSubclass < SingletonInheritanceExample
569
593
  end
594
+
595
+ class BareOptionalContractUsed
596
+ include Contracts
597
+
598
+ Contract Num, Optional[Num] => nil
599
+ def something(a, b)
600
+ nil
601
+ end
602
+ end
603
+
604
+ module ModuleContractExample
605
+ include Contracts
606
+
607
+ module AModule
608
+ end
609
+
610
+ module AnotherModule
611
+ end
612
+
613
+ module InheritedModule
614
+ include AModule
615
+ end
616
+
617
+ class AClassWithModule
618
+ include AModule
619
+ end
620
+
621
+ class AClassWithoutModule
622
+ end
623
+
624
+ class AClassWithAnotherModule
625
+ include AnotherModule
626
+ end
627
+
628
+ class AClassWithInheritedModule
629
+ include InheritedModule
630
+ end
631
+
632
+ class AClassWithBothModules
633
+ include AModule
634
+ include AnotherModule
635
+ end
636
+
637
+ Contract AModule => Symbol
638
+ def self.hello(thing)
639
+ :world
640
+ end
641
+ end