contracts 0.9 → 0.10

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,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