contracts 0.9 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +5 -2
- data/TUTORIAL.md +136 -21
- data/benchmarks/hash.rb +69 -0
- data/lib/contracts.rb +20 -156
- data/lib/contracts/builtin_contracts.rb +100 -3
- data/lib/contracts/call_with.rb +96 -0
- data/lib/contracts/decorators.rb +4 -194
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +46 -0
- data/lib/contracts/engine/target.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/support.rb +45 -30
- data/lib/contracts/validators.rb +127 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +78 -2
- data/spec/contracts_spec.rb +64 -1
- data/spec/fixtures/fixtures.rb +77 -5
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +10 -2
- metadata +12 -6
- data/lib/contracts/eigenclass.rb +0 -38
- data/lib/contracts/modules.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb8713bcd0175e6069a34ede6834e012395e11a4
|
4
|
+
data.tar.gz: 185ac772d51ee7a5fb80c3c4058155aae7094e08
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd78da1e9ed5f3af971fdabab93faa1924c9b74619b36298852ddbfb3b525f161678cd34d2d9a619aff0c958bf3da3694afcee05b0d1bf76b07bd9a69400561a
|
7
|
+
data.tar.gz: 6295dd08adbc49c431b8e2cc234f500592c035c1b9547c6c7fe6ab63b0b91420b323b8d57385d9685aee1510645b881bdd1b575637efa848f3e2175f4ccfca03
|
data/CHANGELOG.markdown
CHANGED
@@ -2,14 +2,17 @@
|
|
2
2
|
- MAJOR fix in pattern-matching: If the return contract for a pattern-matched function fails, it should NOT try the next pattern-match function. Pattern-matching is only for params, not return values.
|
3
3
|
- raise an error if multiple defns have the same contract for pattern matching.
|
4
4
|
|
5
|
-
- New syntax for functions with no input params
|
5
|
+
- New syntax for functions with no input params (the old style still works)
|
6
6
|
Old way:
|
7
|
+
```ruby
|
7
8
|
Contract nil => 1
|
8
9
|
def one
|
9
|
-
|
10
|
+
```
|
10
11
|
New way:
|
12
|
+
```
|
11
13
|
Contract 1
|
12
14
|
def one
|
15
|
+
```
|
13
16
|
|
14
17
|
- Prettier HashOf contract can now be written like this: `HashOf[Num => String]`
|
15
18
|
- Add `SetOf` contract
|
data/TUTORIAL.md
CHANGED
@@ -67,24 +67,40 @@ This can be useful if you're in a REPL and want to figure out how a function sho
|
|
67
67
|
|
68
68
|
contracts.ruby comes with a lot of built-in contracts, including the following:
|
69
69
|
|
70
|
-
*
|
71
|
-
* [`
|
72
|
-
* [`
|
73
|
-
* [`
|
74
|
-
* [`
|
75
|
-
* [`
|
76
|
-
* [`
|
77
|
-
* [`
|
78
|
-
|
79
|
-
*
|
80
|
-
* [`
|
81
|
-
* [`
|
82
|
-
* [`
|
83
|
-
* [`
|
84
|
-
* [`
|
85
|
-
|
86
|
-
*
|
87
|
-
* [`
|
70
|
+
* Basic types
|
71
|
+
* [`Num`](http://www.rubydoc.info/gems/contracts/Contracts/Num) – checks that the argument is `Numeric`
|
72
|
+
* [`Pos`](http://www.rubydoc.info/gems/contracts/Contracts/Pos) – checks that the argument is a positive number
|
73
|
+
* [`Neg`](http://www.rubydoc.info/gems/contracts/Contracts/Neg) – checks that the argument is a negative number
|
74
|
+
* [`Nat`](http://www.rubydoc.info/gems/contracts/Contracts/Nat) – checks that the argument is a natural number (>= 0)
|
75
|
+
* [`Bool`](http://www.rubydoc.info/gems/contracts/Contracts/Bool) – checks that the argument is `true` or `false`
|
76
|
+
* [`Any`](http://www.rubydoc.info/gems/contracts/Contracts/Any) – Passes for any argument. Use when the argument has no constraints.
|
77
|
+
* [`None`](http://www.rubydoc.info/gems/contracts/Contracts/None) – Fails for any argument. Use when the method takes no arguments.
|
78
|
+
|
79
|
+
* Logical combinations
|
80
|
+
* [`Maybe`](http://www.rubydoc.info/gems/contracts/Contracts/Maybe) – specifies that a value _may be_ nil, e.g. `Maybe[String]` (equivalent to `Or[String,nil]`)
|
81
|
+
* [`Or`](http://www.rubydoc.info/gems/contracts/Contracts/Or) – passes if any of the given contracts pass, e.g. `Or[Fixnum, Float]`
|
82
|
+
* [`Xor`](http://www.rubydoc.info/gems/contracts/Contracts/Xor) – passes if exactly one of the given contracts pass, e.g. `Xor[Fixnum, Float]`
|
83
|
+
* [`And`](http://www.rubydoc.info/gems/contracts/Contracts/And) – passes if all contracts pass, e.g. `And[Nat, -> (n) { n.even? }]`
|
84
|
+
* [`Not`](http://www.rubydoc.info/gems/contracts/Contracts/Not) – passes if all contracts fail for the given argument, e.g. `Not[nil]`
|
85
|
+
|
86
|
+
* Collections
|
87
|
+
* [`ArrayOf`](http://www.rubydoc.info/gems/contracts/Contracts/ArrayOf) – checks that the argument is an array, and all elements pass the given contract, e.g. `ArrayOf[Num]`
|
88
|
+
* [`SetOf`](http://www.rubydoc.info/gems/contracts/Contracts/SetOf) – checks that the argument is a set, and all elements pass the given contract, e.g. `SetOf[Num]`
|
89
|
+
* [`HashOf`](http://www.rubydoc.info/gems/contracts/Contracts/HashOf) – checks that the argument is a hash, and all keys and values pass the given contract, e.g. `HashOf[Symbol => String]` or `HashOf[Symbol,String]`
|
90
|
+
* [`RangeOf`](http://www.rubydoc.info/gems/contracts/Contracts/RangeOf) – checks that the argument is a range whose elements (#first and #last) pass the given contract, e.g. `RangeOf[Date]`
|
91
|
+
|
92
|
+
* Keyword arguments
|
93
|
+
* [`KeywordArgs`](http://www.rubydoc.info/gems/contracts/Contracts/KeywordArgs) – checks that the argument is an options hash, and all required keyword arguments are present, and all values pass their respective contracts, e.g. `KeywordArgs[:number => Num, :description => Optional[String]]`
|
94
|
+
* [`Optional`](http://www.rubydoc.info/gems/contracts/Contracts/Optional) – checks that the keyword argument is either not present or pass the given contract, can not be used outside of `KeywordArgs` contract, e.g. `Optional[Num]`
|
95
|
+
|
96
|
+
* Duck typing
|
97
|
+
* [`RespondTo`](http://www.rubydoc.info/gems/contracts/Contracts/RespondTo) – checks that the argument responds to all of the given methods, e.g. `RespondTo[:password, :credit_card]`
|
98
|
+
* [`Send`](http://www.rubydoc.info/gems/contracts/Contracts/Send) – checks that all named methods return a truthy value, e.g. `Send[:valid?]`
|
99
|
+
|
100
|
+
* Miscellaneous
|
101
|
+
* [`Exactly`](http://www.rubydoc.info/gems/contracts/Contracts/Exactly) – checks that the argument has the given type, not accepting sub-classes, e.g. `Exactly[Numeric]`.
|
102
|
+
* [`Eq`](http://www.rubydoc.info/gems/contracts/Contracts/Eq) – checks that the argument is precisely equal to the given value, e.g. `Eq[String]` matches the class `String` and not a string instance.
|
103
|
+
* [`Func`](http://www.rubydoc.info/gems/contracts/Contracts/Func) – specifies the contract for a proc/lambda e.g. `Contract ArrayOf[Num], Func[Num => Num] => ArrayOf[Num]`. See section "Contracts On Functions".
|
88
104
|
|
89
105
|
To see all the built-in contracts and their full descriptions, check out the [RDoc](http://rubydoc.info/gems/contracts/Contracts).
|
90
106
|
|
@@ -239,6 +255,59 @@ give_largest_value(a: 1, b: 2, c: 3) # returns 3
|
|
239
255
|
give_largest_value("a" => 1, 2 => 2, c: 3)
|
240
256
|
```
|
241
257
|
|
258
|
+
### Contracts On Keyword Arguments
|
259
|
+
|
260
|
+
ruby 2.0+, but can be used for normal hashes too, when keyword arguments are
|
261
|
+
not available
|
262
|
+
|
263
|
+
Lets say you are writing a simple function and require a bunch of keyword arguments:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
def connect(host, port:, user:, password:)
|
267
|
+
```
|
268
|
+
|
269
|
+
You can of course put `Hash` contract on it:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
Contract String, { :port => Num, :user => String, :password => String } => Connection
|
273
|
+
def connect(host, port:, user:, password:)
|
274
|
+
```
|
275
|
+
|
276
|
+
But this will not quite work if you want to have a default values:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
Contract String, { :port => Num, :user => String, :password => String } => Connection
|
280
|
+
def connect(host, port: 5000, user:, password:)
|
281
|
+
# ...
|
282
|
+
end
|
283
|
+
|
284
|
+
# No value is passed for port
|
285
|
+
connect("example.org", user: "me", password: "none")
|
286
|
+
```
|
287
|
+
|
288
|
+
Results in:
|
289
|
+
|
290
|
+
```
|
291
|
+
ContractError: Contract violation for argument 2 of 2:
|
292
|
+
Expected: {:port=>Num, :user=>String, :password=>String},
|
293
|
+
Actual: {:user=>"me", :password=>"none"}
|
294
|
+
Value guarded in: Object::connect
|
295
|
+
With Contract: String, Hash => Connection
|
296
|
+
At: (irb):12
|
297
|
+
```
|
298
|
+
|
299
|
+
This can be fixed with contract `{ :port => Maybe[Num], ... }`, but that will
|
300
|
+
allow `nil` to be passed in, which is not the original intent.
|
301
|
+
|
302
|
+
So that is where `KeywordArgs` and `Optional` contracts jump in:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
Contract String, KeywordArgs[ :port => Optional[Num], :user => String, :password => String ] => Connection
|
306
|
+
def connect(host, port: 5000, user:, password:)
|
307
|
+
```
|
308
|
+
|
309
|
+
It looks just like the hash contract, but wrapped in `KeywordArgs` contract. Notice the usage of `Optional` contract - this way you specify that `:port` argument is optional. And it will not fail, when you omit this argument, but it will fail when you pass in `nil`.
|
310
|
+
|
242
311
|
### Contracts On Functions
|
243
312
|
|
244
313
|
Lets say you are writing a simple map function:
|
@@ -260,7 +329,7 @@ This says that the second argument should be a `Proc`. You can call the function
|
|
260
329
|
p map([1, 2, 3], lambda { |x| x + 1 }) # works
|
261
330
|
```
|
262
331
|
|
263
|
-
But suppose you want to have a contract on the Proc too! Suppose you want to make sure that the Proc returns a number. Use the `Func` contract. `Func` takes a contract as
|
332
|
+
But suppose you want to have a contract on the Proc too! Suppose you want to make sure that the Proc returns a number. Use the `Func` contract. `Func` takes a contract as its argument, and uses that contract on the function that you pass in.
|
264
333
|
|
265
334
|
Here's a `map` function that requires an array of numbers, and a function that takes a number and returns a number:
|
266
335
|
|
@@ -284,6 +353,12 @@ p map([1, 2, 3], lambda { |x| x + 1 }) # works
|
|
284
353
|
p map([1, 2, 3], lambda { |x| "oops" }) # fails, the lambda returns a string.
|
285
354
|
```
|
286
355
|
|
356
|
+
The above examples showed a method accepting a `Proc` as the last argument, but the same contract works on methods that accept a block:
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
def map(arr, &block)
|
360
|
+
```
|
361
|
+
|
287
362
|
NOTE: This is not valid:
|
288
363
|
|
289
364
|
```ruby
|
@@ -448,6 +523,36 @@ end
|
|
448
523
|
|
449
524
|
If your failure callback returns `false`, the method that the contract is guarding will not be called (the default behaviour).
|
450
525
|
|
526
|
+
## Providing your own custom validators
|
527
|
+
|
528
|
+
This can be done with `Contract.override_validator`:
|
529
|
+
|
530
|
+
```ruby
|
531
|
+
# Make contracts accept all RSpec doubles
|
532
|
+
Contract.override_validator(:class) do |contract|
|
533
|
+
lambda do |arg|
|
534
|
+
arg.is_a?(RSpec::Mocks::Double) ||
|
535
|
+
arg.is_a?(contract)
|
536
|
+
end
|
537
|
+
end
|
538
|
+
```
|
539
|
+
|
540
|
+
The block you provide should always return lambda accepting one argument - validated argument. Block itself accepts contract as an argument.
|
541
|
+
|
542
|
+
Possible validator overrides:
|
543
|
+
|
544
|
+
- `override_validator(MyCustomContract)` - allows to add some special behaviour for custom contracts,
|
545
|
+
- `override_validator(Proc)` - e.g. `lambda { true }`,
|
546
|
+
- `override_validator(Array)` - e.g. `[Num, String]`,
|
547
|
+
- `override_validator(Hash)` - e.g. `{ :a => Num, :b => String }`,
|
548
|
+
- `override_validator(Contracts::Args)` - e.g. `Args[Num]`,
|
549
|
+
- `override_validator(Contracts::Func)` - e.g. `Func[Num => Num]`,
|
550
|
+
- `override_validator(:valid)` - allows to override how contracts that respond to `:valid?` are handled,
|
551
|
+
- `override_validator(:class)` - allows to override how class/module contract constants are handled,
|
552
|
+
- `override_validator(:default)` - otherwise, raw value contracts.
|
553
|
+
|
554
|
+
Default validators can be found here: [lib/contracts/validators.rb](https://github.com/egonSchiele/contracts.ruby/blob/master/lib/contracts/validators.rb).
|
555
|
+
|
451
556
|
## Disabling contracts
|
452
557
|
|
453
558
|
If you want to disable contracts, set the `NO_CONTRACTS` environment variable. This will disable contracts and you won't have a performance hit. Pattern matching will still work if you disable contracts in this way! With NO_CONTRACTS only pattern-matching contracts are defined.
|
@@ -510,12 +615,11 @@ This is because the first contract eliminated the possibility of `age` being les
|
|
510
615
|
|
511
616
|
## Contracts in modules
|
512
617
|
|
513
|
-
|
618
|
+
Usage is the same as contracts in classes:
|
514
619
|
|
515
620
|
```ruby
|
516
621
|
module M
|
517
622
|
include Contracts
|
518
|
-
include Contracts::Modules
|
519
623
|
|
520
624
|
Contract String => String
|
521
625
|
def self.parse
|
@@ -562,6 +666,17 @@ If you run it, last line will generate invariant violation:
|
|
562
666
|
|
563
667
|
Which means, that after `#silly_next_day!` all checks specified in `invariant` statement will be verified, and if at least one fail, then invariant violation error will be raised.
|
564
668
|
|
669
|
+
## Using contracts within your own code
|
670
|
+
|
671
|
+
contracts.ruby is obviously designed to check method parameters and return values. But if you want to check whether some other data obeys a contract, you can use `Contract.valid?(value, contract)`. For instance:
|
672
|
+
|
673
|
+
```ruby
|
674
|
+
data = parse(user_input)
|
675
|
+
unless Contract.valid?(data, HashOf[String,Nat])
|
676
|
+
raise UserInputError.new(user_input)
|
677
|
+
end
|
678
|
+
```
|
679
|
+
|
565
680
|
## Auto-generate documentation using contracts
|
566
681
|
|
567
682
|
If you are generating documentation for your code with [YARD](http://yardoc.org/), check out [yard-contracts](https://github.com/sfcgeorge/yard-contracts). It will automatically annotate your functions with contracts information. Instead of documenting each parameter for a function yourself, you can just add a contract and yard-contracts will generate the documentation for you!
|
data/benchmarks/hash.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "./lib/contracts"
|
2
|
+
require "benchmark"
|
3
|
+
require "rubygems"
|
4
|
+
require "method_profiler"
|
5
|
+
require "ruby-prof"
|
6
|
+
|
7
|
+
include Contracts
|
8
|
+
|
9
|
+
def add opts
|
10
|
+
opts[:a] + opts[:b]
|
11
|
+
end
|
12
|
+
|
13
|
+
Contract ({ :a => Num, :b => Num}) => Num
|
14
|
+
def contracts_add opts
|
15
|
+
opts[:a] + opts[:b]
|
16
|
+
end
|
17
|
+
|
18
|
+
def explicit_add opts
|
19
|
+
a = opts[:a]
|
20
|
+
b = opts[:b]
|
21
|
+
fail unless a.is_a?(Numeric)
|
22
|
+
fail unless b.is_a?(Numeric)
|
23
|
+
c = a + b
|
24
|
+
fail unless c.is_a?(Numeric)
|
25
|
+
c
|
26
|
+
end
|
27
|
+
|
28
|
+
def benchmark
|
29
|
+
Benchmark.bm 30 do |x|
|
30
|
+
x.report "testing add" do
|
31
|
+
1_000_000.times do |_|
|
32
|
+
add(:a => rand(1000), :b => rand(1000))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
x.report "testing contracts add" do
|
36
|
+
1_000_000.times do |_|
|
37
|
+
contracts_add(:a => rand(1000), :b => rand(1000))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def profile
|
44
|
+
profilers = []
|
45
|
+
profilers << MethodProfiler.observe(Contract)
|
46
|
+
profilers << MethodProfiler.observe(Object)
|
47
|
+
profilers << MethodProfiler.observe(Contracts::MethodDecorators)
|
48
|
+
profilers << MethodProfiler.observe(Contracts::Decorator)
|
49
|
+
profilers << MethodProfiler.observe(Contracts::Support)
|
50
|
+
profilers << MethodProfiler.observe(UnboundMethod)
|
51
|
+
10_000.times do |_|
|
52
|
+
contracts_add(:a => rand(1000), :b => rand(1000))
|
53
|
+
end
|
54
|
+
profilers.each { |p| puts p.report }
|
55
|
+
end
|
56
|
+
|
57
|
+
def ruby_prof
|
58
|
+
RubyProf.start
|
59
|
+
100_000.times do |_|
|
60
|
+
contracts_add(:a => rand(1000), :b => rand(1000))
|
61
|
+
end
|
62
|
+
result = RubyProf.stop
|
63
|
+
printer = RubyProf::FlatPrinter.new(result)
|
64
|
+
printer.print(STDOUT)
|
65
|
+
end
|
66
|
+
|
67
|
+
benchmark
|
68
|
+
profile
|
69
|
+
ruby_prof if ENV["FULL_BENCH"] # takes some time
|
data/lib/contracts.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
require "contracts/builtin_contracts"
|
2
2
|
require "contracts/decorators"
|
3
|
-
require "contracts/eigenclass"
|
4
3
|
require "contracts/errors"
|
5
4
|
require "contracts/formatters"
|
6
5
|
require "contracts/invariants"
|
7
6
|
require "contracts/method_reference"
|
8
|
-
require "contracts/modules"
|
9
7
|
require "contracts/support"
|
8
|
+
require "contracts/engine"
|
9
|
+
require "contracts/method_handler"
|
10
|
+
require "contracts/validators"
|
11
|
+
require "contracts/call_with"
|
10
12
|
|
11
13
|
module Contracts
|
12
14
|
def self.included(base)
|
@@ -18,15 +20,13 @@ module Contracts
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def self.common(base)
|
21
|
-
Eigenclass.lift(base)
|
22
|
-
|
23
23
|
return if base.respond_to?(:Contract)
|
24
24
|
|
25
25
|
base.extend(MethodDecorators)
|
26
26
|
|
27
27
|
base.instance_eval do
|
28
28
|
def functype(funcname)
|
29
|
-
contracts =
|
29
|
+
contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname)
|
30
30
|
if contracts.nil?
|
31
31
|
"No contract for #{self}.#{funcname}"
|
32
32
|
else
|
@@ -36,22 +36,14 @@ module Contracts
|
|
36
36
|
end
|
37
37
|
|
38
38
|
base.class_eval do
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
puts %{
|
44
|
-
Warning: You have added a Contract on a module function
|
45
|
-
without including Contracts::Modules. Your Contract will
|
46
|
-
just be ignored. Please include Contracts::Modules into
|
47
|
-
your module.}
|
48
|
-
end
|
49
|
-
self.class.Contract(*args)
|
50
|
-
end
|
39
|
+
# TODO: deprecate
|
40
|
+
# Required when contracts are included in global scope
|
41
|
+
def Contract(*args)
|
42
|
+
self.class.Contract(*args)
|
51
43
|
end
|
52
44
|
|
53
45
|
def functype(funcname)
|
54
|
-
contracts = self.class.
|
46
|
+
contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname)
|
55
47
|
if contracts.nil?
|
56
48
|
"No contract for #{self.class}.#{funcname}"
|
57
49
|
else
|
@@ -68,7 +60,15 @@ end
|
|
68
60
|
# Contract [contract names] => return_value
|
69
61
|
#
|
70
62
|
# This class also provides useful callbacks and a validation method.
|
63
|
+
#
|
64
|
+
# For #make_validator and related logic see file
|
65
|
+
# lib/contracts/validators.rb
|
66
|
+
# For #call_with and related logic see file
|
67
|
+
# lib/contracts/call_with.rb
|
71
68
|
class Contract < Contracts::Decorator
|
69
|
+
extend Contracts::Validators
|
70
|
+
include Contracts::CallWith
|
71
|
+
|
72
72
|
# Default implementation of failure_callback. Provided as a block to be able
|
73
73
|
# to monkey patch #failure_callback only temporary and then switch it back.
|
74
74
|
# First important usage - for specs.
|
@@ -113,8 +113,9 @@ class Contract < Contracts::Decorator
|
|
113
113
|
# == @has_proc_contract
|
114
114
|
last_contract = args_contracts.last
|
115
115
|
is_a_proc = last_contract.is_a?(Class) && (last_contract <= Proc || last_contract <= Method)
|
116
|
+
maybe_a_proc = last_contract.is_a?(Contracts::Maybe) && last_contract.include_proc?
|
116
117
|
|
117
|
-
@has_proc_contract = is_a_proc || last_contract.is_a?(Contracts::Func)
|
118
|
+
@has_proc_contract = is_a_proc || maybe_a_proc || last_contract.is_a?(Contracts::Func)
|
118
119
|
# ====
|
119
120
|
|
120
121
|
# == @has_options_contract
|
@@ -218,54 +219,6 @@ class Contract < Contracts::Decorator
|
|
218
219
|
make_validator(contract)[arg]
|
219
220
|
end
|
220
221
|
|
221
|
-
# This is a little weird. For each contract
|
222
|
-
# we pre-make a proc to validate it so we
|
223
|
-
# don't have to go through this decision tree every time.
|
224
|
-
# Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
|
225
|
-
def self.make_validator(contract)
|
226
|
-
# if is faster than case!
|
227
|
-
klass = contract.class
|
228
|
-
if klass == Proc
|
229
|
-
# e.g. lambda {true}
|
230
|
-
contract
|
231
|
-
elsif klass == Array
|
232
|
-
# e.g. [Num, String]
|
233
|
-
# TODO: account for these errors too
|
234
|
-
lambda do |arg|
|
235
|
-
return false unless arg.is_a?(Array) && arg.length == contract.length
|
236
|
-
arg.zip(contract).all? do |_arg, _contract|
|
237
|
-
Contract.valid?(_arg, _contract)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
elsif klass == Hash
|
241
|
-
# e.g. { :a => Num, :b => String }
|
242
|
-
lambda do |arg|
|
243
|
-
return false unless arg.is_a?(Hash)
|
244
|
-
contract.keys.all? do |k|
|
245
|
-
Contract.valid?(arg[k], contract[k])
|
246
|
-
end
|
247
|
-
end
|
248
|
-
elsif klass == Contracts::Args
|
249
|
-
lambda do |arg|
|
250
|
-
Contract.valid?(arg, contract.contract)
|
251
|
-
end
|
252
|
-
elsif klass == Contracts::Func
|
253
|
-
lambda do |arg|
|
254
|
-
arg.is_a?(Method) || arg.is_a?(Proc)
|
255
|
-
end
|
256
|
-
else
|
257
|
-
# classes and everything else
|
258
|
-
# e.g. Fixnum, Num
|
259
|
-
if contract.respond_to? :valid?
|
260
|
-
lambda { |arg| contract.valid?(arg) }
|
261
|
-
elsif klass == Class
|
262
|
-
lambda { |arg| arg.is_a?(contract) }
|
263
|
-
else
|
264
|
-
lambda { |arg| contract == arg }
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
222
|
def [](*args, &blk)
|
270
223
|
call(*args, &blk)
|
271
224
|
end
|
@@ -296,95 +249,6 @@ class Contract < Contracts::Decorator
|
|
296
249
|
end
|
297
250
|
end
|
298
251
|
|
299
|
-
def call_with(this, *args, &blk)
|
300
|
-
args << blk if blk
|
301
|
-
|
302
|
-
# Explicitly append blk=nil if nil != Proc contract violation anticipated
|
303
|
-
maybe_append_block!(args, blk)
|
304
|
-
|
305
|
-
# Explicitly append options={} if Hash contract is present
|
306
|
-
maybe_append_options!(args, blk)
|
307
|
-
|
308
|
-
# Loop forward validating the arguments up to the splat (if there is one)
|
309
|
-
(@args_contract_index || args.size).times do |i|
|
310
|
-
contract = args_contracts[i]
|
311
|
-
arg = args[i]
|
312
|
-
validator = @args_validators[i]
|
313
|
-
|
314
|
-
unless validator && validator[arg]
|
315
|
-
return unless Contract.failure_callback(:arg => arg,
|
316
|
-
:contract => contract,
|
317
|
-
:class => klass,
|
318
|
-
:method => method,
|
319
|
-
:contracts => self,
|
320
|
-
:arg_pos => i+1,
|
321
|
-
:total_args => args.size,
|
322
|
-
:return_value => false)
|
323
|
-
end
|
324
|
-
|
325
|
-
if contract.is_a?(Contracts::Func)
|
326
|
-
args[i] = Contract.new(klass, arg, *contract.contracts)
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
# If there is a splat loop backwards to the lower index of the splat
|
331
|
-
# Once we hit the splat in this direction set its upper index
|
332
|
-
# Keep validating but use this upper index to get the splat validator.
|
333
|
-
if @args_contract_index
|
334
|
-
splat_upper_index = @args_contract_index
|
335
|
-
(args.size - @args_contract_index).times do |i|
|
336
|
-
arg = args[args.size - 1 - i]
|
337
|
-
|
338
|
-
if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
|
339
|
-
splat_upper_index = i
|
340
|
-
end
|
341
|
-
|
342
|
-
# Each arg after the spat is found must use the splat validator
|
343
|
-
j = i < splat_upper_index ? i : splat_upper_index
|
344
|
-
contract = args_contracts[args_contracts.size - 1 - j]
|
345
|
-
validator = @args_validators[args_contracts.size - 1 - j]
|
346
|
-
|
347
|
-
unless validator && validator[arg]
|
348
|
-
return unless Contract.failure_callback(:arg => arg,
|
349
|
-
:contract => contract,
|
350
|
-
:class => klass,
|
351
|
-
:method => method,
|
352
|
-
:contracts => self,
|
353
|
-
:arg_pos => i-1,
|
354
|
-
:total_args => args.size,
|
355
|
-
:return_value => false)
|
356
|
-
end
|
357
|
-
|
358
|
-
if contract.is_a?(Contracts::Func)
|
359
|
-
args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
|
360
|
-
end
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
# If we put the block into args for validating, restore the args
|
365
|
-
args.slice!(-1) if blk
|
366
|
-
result = if method.respond_to?(:call)
|
367
|
-
# proc, block, lambda, etc
|
368
|
-
method.call(*args, &blk)
|
369
|
-
else
|
370
|
-
# original method name referrence
|
371
|
-
method.send_to(this, *args, &blk)
|
372
|
-
end
|
373
|
-
|
374
|
-
unless @ret_validator[result]
|
375
|
-
Contract.failure_callback(:arg => result,
|
376
|
-
:contract => ret_contract,
|
377
|
-
:class => klass,
|
378
|
-
:method => method,
|
379
|
-
:contracts => self,
|
380
|
-
:return_value => true)
|
381
|
-
end
|
382
|
-
|
383
|
-
this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
|
384
|
-
|
385
|
-
result
|
386
|
-
end
|
387
|
-
|
388
252
|
# Used to determine type of failure exception this contract should raise in case of failure
|
389
253
|
def failure_exception
|
390
254
|
if @pattern_match
|