contracts 0.7 → 0.8
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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +16 -1
- data/README.md +12 -4
- data/TUTORIAL.md +67 -18
- data/benchmarks/bench.rb +20 -20
- data/benchmarks/invariants.rb +28 -18
- data/benchmarks/io.rb +62 -0
- data/benchmarks/wrap_test.rb +11 -13
- data/contracts.gemspec +1 -1
- data/lib/contracts.rb +167 -109
- data/lib/contracts/builtin_contracts.rb +132 -84
- data/lib/contracts/decorators.rb +41 -53
- data/lib/contracts/eigenclass.rb +3 -7
- data/lib/contracts/errors.rb +1 -1
- data/lib/contracts/formatters.rb +134 -0
- data/lib/contracts/invariants.rb +7 -8
- data/lib/contracts/method_reference.rb +33 -9
- data/lib/contracts/support.rb +9 -3
- data/lib/contracts/testable.rb +6 -6
- data/lib/contracts/version.rb +1 -1
- data/script/rubocop.rb +5 -0
- data/spec/builtin_contracts_spec.rb +57 -9
- data/spec/contracts_spec.rb +182 -53
- data/spec/fixtures/fixtures.rb +130 -9
- data/spec/invariants_spec.rb +0 -2
- data/spec/module_spec.rb +2 -2
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +18 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +22 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +55 -0
- data/spec/spec_helper.rb +9 -2
- data/spec/support.rb +4 -0
- data/spec/support_spec.rb +21 -0
- metadata +11 -3
- data/lib/contracts/core_ext.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 124deda42f022fb31b53dc3389bd51ff85860d30
|
4
|
+
data.tar.gz: 8ea3f78006952432311338880c563cdbdf327f33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 644753384168c11234cd6a77b6d64384cc0f6fca93f3614e4d71ae3629ec7d05d85b02bfbd0150b3db21f94e04fe980f784d0e4decee0e5dcfd912f66526b2fb
|
7
|
+
data.tar.gz: 8a92fd259e98d4320fe5a4a52cc0d2d6cd01e7362b0fc6ed809001ef4e4618a7d1fab4513b2cb2db5946028fdf7a1c745e13491bf9d1e4e897f2dfe39446aafe
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
contracts (0.
|
4
|
+
contracts (0.7)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
8
8
|
specs:
|
9
|
+
ast (2.0.0)
|
10
|
+
astrolabe (1.3.0)
|
11
|
+
parser (>= 2.2.0.pre.3, < 3.0)
|
9
12
|
diff-lcs (1.2.5)
|
10
13
|
hirb (0.7.2)
|
11
14
|
method_profiler (2.0.1)
|
12
15
|
hirb (>= 0.6.0)
|
16
|
+
parser (2.2.0.3)
|
17
|
+
ast (>= 1.1, < 3.0)
|
18
|
+
powerpack (0.1.0)
|
19
|
+
rainbow (2.0.0)
|
13
20
|
rspec (3.1.0)
|
14
21
|
rspec-core (~> 3.1.0)
|
15
22
|
rspec-expectations (~> 3.1.0)
|
@@ -22,7 +29,14 @@ GEM
|
|
22
29
|
rspec-mocks (3.1.3)
|
23
30
|
rspec-support (~> 3.1.0)
|
24
31
|
rspec-support (3.1.2)
|
32
|
+
rubocop (0.29.1)
|
33
|
+
astrolabe (~> 1.3)
|
34
|
+
parser (>= 2.2.0.1, < 3.0)
|
35
|
+
powerpack (~> 0.1)
|
36
|
+
rainbow (>= 1.99.1, < 3.0)
|
37
|
+
ruby-progressbar (~> 1.4)
|
25
38
|
ruby-prof (0.15.2)
|
39
|
+
ruby-progressbar (1.7.5)
|
26
40
|
|
27
41
|
PLATFORMS
|
28
42
|
ruby
|
@@ -31,4 +45,5 @@ DEPENDENCIES
|
|
31
45
|
contracts!
|
32
46
|
method_profiler
|
33
47
|
rspec
|
48
|
+
rubocop (~> 0.29)
|
34
49
|
ruby-prof
|
data/README.md
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
# contracts.ruby
|
2
|
-
|
3
|
-
[](https://travis-ci.org/egonSchiele/contracts.ruby)
|
1
|
+
# contracts.ruby [](https://travis-ci.org/egonSchiele/contracts.ruby) [](https://gitter.im/egonSchiele/contracts.ruby)
|
4
2
|
|
5
3
|
Contracts let you clearly – even beautifully – express how your code behaves, and free you from writing tons of boilerplate, defensive code.
|
6
4
|
|
@@ -65,11 +63,21 @@ Using contracts.ruby results in very little slowdown. Check out [this blog post]
|
|
65
63
|
|
66
64
|
If you're using the library, please [let me know](https://github.com/egonSchiele) what project you're using it on :)
|
67
65
|
|
66
|
+
## Testimonials
|
67
|
+
|
68
|
+
> Contracts literally saves us hours of pain at Snowplow every day
|
69
|
+
|
70
|
+
Alexander Dean, creator of [Snowplow](https://github.com/snowplow/snowplow)
|
71
|
+
|
72
|
+
> Contracts caught a bug that saved us several hundred dollars. It took less than 30 seconds to add the contract.
|
73
|
+
|
74
|
+
Michael Tomer
|
75
|
+
|
68
76
|
## Credits
|
69
77
|
|
70
78
|
Inspired by [contracts.coffee](http://disnetdev.com/contracts.coffee/).
|
71
79
|
|
72
|
-
Copyright 2012 [Aditya Bhargava](http://adit.io).
|
80
|
+
Copyright 2012-2015 [Aditya Bhargava](http://adit.io).
|
73
81
|
Major improvements by [Alexey Fedorov](https://github.com/waterlink).
|
74
82
|
|
75
83
|
BSD Licensed.
|
data/TUTORIAL.md
CHANGED
@@ -59,17 +59,33 @@ You can also see the contract for a function with the `functype` method:
|
|
59
59
|
functype(:add)
|
60
60
|
=> "add :: Num, Num => Num"
|
61
61
|
|
62
|
-
This can be useful if you're in a
|
63
|
-
|
64
|
-
##
|
65
|
-
|
66
|
-
`Num` is one of the
|
67
|
-
|
68
|
-
contracts.ruby comes with a lot of
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
62
|
+
This can be useful if you're in a REPL and want to figure out how a function should be used.
|
63
|
+
|
64
|
+
## Built-in Contracts
|
65
|
+
|
66
|
+
`Num` is one of the built-in contracts that contracts.ruby comes with. The built-in contracts are in the `Contracts` namespace. The easiest way to use them is to include the `Contracts` module in your class/module.
|
67
|
+
|
68
|
+
contracts.ruby comes with a lot of built-in contracts, including the following:
|
69
|
+
|
70
|
+
* [`Num`](http://www.rubydoc.info/gems/contracts/Contracts/Num) – checks that the argument is `Numeric`
|
71
|
+
* [`Pos`](http://www.rubydoc.info/gems/contracts/Contracts/Pos) – checks that the argument is a positive number
|
72
|
+
* [`Neg`](http://www.rubydoc.info/gems/contracts/Contracts/Neg) – checks that the argument is a negative number
|
73
|
+
* [`Nat`](http://www.rubydoc.info/gems/contracts/Contracts/Nat) – checks that the argument is a natural number
|
74
|
+
* [`Bool`](http://www.rubydoc.info/gems/contracts/Contracts/Bool) – checks that the argument is `true` or `false`
|
75
|
+
* [`Any`](http://www.rubydoc.info/gems/contracts/Contracts/Any) – Passes for any argument. Use when the argument has no constraints.
|
76
|
+
* [`None`](http://www.rubydoc.info/gems/contracts/Contracts/None) – Fails for any argument. Use when the method takes no arguments.
|
77
|
+
* [`Or`](http://www.rubydoc.info/gems/contracts/Contracts/Or) – passes if any of the given contracts pass, e.g. `Or[Fixnum, Float]`
|
78
|
+
* [`Xor`](http://www.rubydoc.info/gems/contracts/Contracts/Xor) – passes if exactly one of the given contracts pass, e.g. `Xor[Fixnum, Float]`
|
79
|
+
* [`And`](http://www.rubydoc.info/gems/contracts/Contracts/And) – passes if all contracts pass, e.g. `And[Fixnum, Float]`
|
80
|
+
* [`Not`](http://www.rubydoc.info/gems/contracts/Contracts/Not) – passes if all contracts fail for the given argument, e.g. `Not[nil]`
|
81
|
+
* [`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]`
|
82
|
+
* [`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]`
|
83
|
+
* [`Maybe`](http://www.rubydoc.info/gems/contracts/Contracts/Maybe) – passes if the argument is `nil`, or if the given contract passes
|
84
|
+
* [`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]`
|
85
|
+
* [`Send`](http://www.rubydoc.info/gems/contracts/Contracts/Send) – checks that all named methods return true, e.g. `Send[:valid?]`
|
86
|
+
* [`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]`.
|
87
|
+
|
88
|
+
To see all the built-in contracts and their full descriptions, check out the [RDoc](http://rubydoc.info/gems/contracts/Contracts).
|
73
89
|
|
74
90
|
## More Examples
|
75
91
|
|
@@ -106,7 +122,7 @@ This introduces some new syntax. One of the valid values for a contract is an in
|
|
106
122
|
Contract Or.new(Fixnum, Float) => Or.new(Fixnum, Float)
|
107
123
|
```
|
108
124
|
|
109
|
-
All the
|
125
|
+
All the built-in contracts have overridden the square brackets (`[]`) to give the same functionality. So you could write
|
110
126
|
|
111
127
|
```ruby
|
112
128
|
Contract Or[Fixnum, Float] => Or[Fixnum, Float]
|
@@ -224,8 +240,26 @@ give_largest_value("a" => 1, 2 => 2, c: 3)
|
|
224
240
|
|
225
241
|
### Contracts On Functions
|
226
242
|
|
227
|
-
|
228
|
-
|
243
|
+
Lets say you are writing a simple map function:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
def map(arr, func)
|
247
|
+
```
|
248
|
+
|
249
|
+
`map` takes an array, and a function. Suppose you want to add a contract to this function. You could try this:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
Contract ArrayOf[Any], Proc => ArrayOf[Any]
|
253
|
+
def map(arr, func)
|
254
|
+
```
|
255
|
+
|
256
|
+
This says that the second argument should be a `Proc`. You can call the function like so:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
p map([1, 2, 3], lambda { |x| x + 1 }) # works
|
260
|
+
```
|
261
|
+
|
262
|
+
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 it's argument, and uses that contract on the function that you pass in.
|
229
263
|
|
230
264
|
Here's a `map` function that requires an array of numbers, and a function that takes a number and returns a number:
|
231
265
|
|
@@ -240,13 +274,24 @@ def map(arr, func)
|
|
240
274
|
end
|
241
275
|
```
|
242
276
|
|
243
|
-
|
277
|
+
Earlier, we used `Proc`, which just says "make sure the second variable is a Proc". Now we are using `Func[Num => Num]`, which says "make sure the second variable is a Proc that takes a number and returns a number". Better!
|
278
|
+
|
279
|
+
Try this map function with these two examples:
|
244
280
|
|
245
281
|
```ruby
|
246
282
|
p map([1, 2, 3], lambda { |x| x + 1 }) # works
|
247
283
|
p map([1, 2, 3], lambda { |x| "oops" }) # fails, the lambda returns a string.
|
248
284
|
```
|
249
285
|
|
286
|
+
NOTE: This is not valid:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
Contract ArrayOf[Num], Func => ArrayOf[Num]
|
290
|
+
def map(arr, &func)
|
291
|
+
```
|
292
|
+
|
293
|
+
Here I am using `Func` without specifying a contract, like `Func[Num => Num]`. That's not a legal contract. If you just want to validate that the second argument is a proc, use `Proc`.
|
294
|
+
|
250
295
|
### Returning Multiple Values
|
251
296
|
Treat the return value as an array. For example, here's a function that returns two numbers:
|
252
297
|
|
@@ -491,8 +536,8 @@ class MyBirthday < Struct.new(:day, :month)
|
|
491
536
|
include Contracts
|
492
537
|
include Contracts::Invariants
|
493
538
|
|
494
|
-
|
495
|
-
|
539
|
+
invariant(:day) { 1 <= day && day <= 31 }
|
540
|
+
invariant(:month) { 1 <= month && month <= 12 }
|
496
541
|
|
497
542
|
Contract None => Fixnum
|
498
543
|
def silly_next_day!
|
@@ -514,7 +559,11 @@ If you run it, last line will generate invariant violation:
|
|
514
559
|
At: main.rb:9
|
515
560
|
```
|
516
561
|
|
517
|
-
Which means, that after `#silly_next_day!` all checks specified in `
|
562
|
+
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.
|
563
|
+
|
564
|
+
## Auto-generate documentation using contracts
|
565
|
+
|
566
|
+
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!
|
518
567
|
|
519
568
|
## Misc
|
520
569
|
|
data/benchmarks/bench.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require "./lib/contracts"
|
2
|
+
require "benchmark"
|
3
|
+
require "rubygems"
|
4
|
+
require "method_profiler"
|
5
|
+
require "ruby-prof"
|
6
6
|
|
7
7
|
include Contracts
|
8
8
|
|
@@ -16,22 +16,22 @@ def contracts_add a, b
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def explicit_add a, b
|
19
|
-
|
20
|
-
|
19
|
+
fail unless a.is_a?(Numeric)
|
20
|
+
fail unless b.is_a?(Numeric)
|
21
21
|
c = a + b
|
22
|
-
|
22
|
+
fail unless c.is_a?(Numeric)
|
23
23
|
c
|
24
24
|
end
|
25
25
|
|
26
26
|
def benchmark
|
27
27
|
Benchmark.bm 30 do |x|
|
28
|
-
x.report
|
29
|
-
|
28
|
+
x.report "testing add" do
|
29
|
+
1_000_000.times do |_|
|
30
30
|
add(rand(1000), rand(1000))
|
31
31
|
end
|
32
32
|
end
|
33
|
-
x.report
|
34
|
-
|
33
|
+
x.report "testing contracts add" do
|
34
|
+
1_000_000.times do |_|
|
35
35
|
contracts_add(rand(1000), rand(1000))
|
36
36
|
end
|
37
37
|
end
|
@@ -46,20 +46,20 @@ def profile
|
|
46
46
|
profilers << MethodProfiler.observe(Contracts::Decorator)
|
47
47
|
profilers << MethodProfiler.observe(Contracts::Support)
|
48
48
|
profilers << MethodProfiler.observe(UnboundMethod)
|
49
|
-
|
49
|
+
10_000.times do |_|
|
50
50
|
contracts_add(rand(1000), rand(1000))
|
51
51
|
end
|
52
52
|
profilers.each { |p| puts p.report }
|
53
53
|
end
|
54
54
|
|
55
55
|
def ruby_prof
|
56
|
-
RubyProf.start
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
result = RubyProf.stop
|
61
|
-
printer = RubyProf::FlatPrinter.new(result)
|
62
|
-
printer.print(STDOUT)
|
56
|
+
RubyProf.start
|
57
|
+
100_000.times do |_|
|
58
|
+
contracts_add(rand(1000), rand(1000))
|
59
|
+
end
|
60
|
+
result = RubyProf.stop
|
61
|
+
printer = RubyProf::FlatPrinter.new(result)
|
62
|
+
printer.print(STDOUT)
|
63
63
|
end
|
64
64
|
|
65
65
|
benchmark
|
data/benchmarks/invariants.rb
CHANGED
@@ -1,24 +1,34 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require "./lib/contracts"
|
2
|
+
require "benchmark"
|
3
|
+
require "rubygems"
|
4
|
+
require "method_profiler"
|
5
|
+
require "ruby-prof"
|
6
6
|
|
7
|
-
class Obj
|
7
|
+
class Obj
|
8
8
|
include Contracts
|
9
9
|
|
10
|
+
attr_accessor :value
|
11
|
+
def initialize value
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
10
15
|
Contract Num, Num => Num
|
11
16
|
def contracts_add a, b
|
12
17
|
a + b
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
16
|
-
class ObjWithInvariants
|
21
|
+
class ObjWithInvariants
|
17
22
|
include Contracts
|
18
23
|
include Contracts::Invariants
|
19
24
|
|
20
|
-
|
21
|
-
|
25
|
+
invariant(:value_not_nil) { value != nil }
|
26
|
+
invariant(:value_not_string) { !value.is_a?(String) }
|
27
|
+
|
28
|
+
attr_accessor :value
|
29
|
+
def initialize value
|
30
|
+
@value = value
|
31
|
+
end
|
22
32
|
|
23
33
|
Contract Num, Num => Num
|
24
34
|
def contracts_add a, b
|
@@ -31,16 +41,16 @@ def benchmark
|
|
31
41
|
obj_with_invariants = ObjWithInvariants.new(3)
|
32
42
|
|
33
43
|
Benchmark.bm 30 do |x|
|
34
|
-
x.report
|
35
|
-
|
44
|
+
x.report "testing contracts add" do
|
45
|
+
1_000_000.times do |_|
|
36
46
|
obj.contracts_add(rand(1000), rand(1000))
|
37
47
|
end
|
38
48
|
end
|
39
|
-
x.report
|
40
|
-
|
49
|
+
x.report "testing contracts add with invariants" do
|
50
|
+
1_000_000.times do |_|
|
41
51
|
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
42
52
|
end
|
43
|
-
end
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
46
56
|
|
@@ -55,19 +65,19 @@ def profile
|
|
55
65
|
profilers << MethodProfiler.observe(Contracts::Invariants::InvariantExtension)
|
56
66
|
profilers << MethodProfiler.observe(UnboundMethod)
|
57
67
|
|
58
|
-
|
68
|
+
10_000.times do |_|
|
59
69
|
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
60
|
-
end
|
70
|
+
end
|
61
71
|
|
62
72
|
profilers.each { |p| puts p.report }
|
63
73
|
end
|
64
74
|
|
65
75
|
def ruby_prof
|
66
|
-
RubyProf.start
|
76
|
+
RubyProf.start
|
67
77
|
|
68
78
|
obj_with_invariants = ObjWithInvariants.new(3)
|
69
79
|
|
70
|
-
|
80
|
+
100_000.times do |_|
|
71
81
|
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
72
82
|
end
|
73
83
|
|
data/benchmarks/io.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "./lib/contracts"
|
2
|
+
require "benchmark"
|
3
|
+
require "rubygems"
|
4
|
+
require "method_profiler"
|
5
|
+
require "ruby-prof"
|
6
|
+
require "open-uri"
|
7
|
+
|
8
|
+
include Contracts
|
9
|
+
|
10
|
+
def download url
|
11
|
+
open("https://www.#{url}/").read
|
12
|
+
end
|
13
|
+
|
14
|
+
Contract String => String
|
15
|
+
def contracts_download url
|
16
|
+
open("https://www.#{url}").read
|
17
|
+
end
|
18
|
+
|
19
|
+
@urls = %w{facebook.com google.com bing.com}
|
20
|
+
|
21
|
+
def benchmark
|
22
|
+
Benchmark.bm 30 do |x|
|
23
|
+
x.report "testing download" do
|
24
|
+
100.times do |_|
|
25
|
+
download(@urls.sample)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
x.report "testing contracts download" do
|
29
|
+
100.times do |_|
|
30
|
+
contracts_download(@urls.sample)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def profile
|
37
|
+
profilers = []
|
38
|
+
profilers << MethodProfiler.observe(Contract)
|
39
|
+
profilers << MethodProfiler.observe(Object)
|
40
|
+
profilers << MethodProfiler.observe(Contracts::MethodDecorators)
|
41
|
+
profilers << MethodProfiler.observe(Contracts::Decorator)
|
42
|
+
profilers << MethodProfiler.observe(Contracts::Support)
|
43
|
+
profilers << MethodProfiler.observe(UnboundMethod)
|
44
|
+
10.times do |_|
|
45
|
+
contracts_download(@urls.sample)
|
46
|
+
end
|
47
|
+
profilers.each { |p| puts p.report }
|
48
|
+
end
|
49
|
+
|
50
|
+
def ruby_prof
|
51
|
+
RubyProf.start
|
52
|
+
10.times do |_|
|
53
|
+
contracts_download(@urls.sample)
|
54
|
+
end
|
55
|
+
result = RubyProf.stop
|
56
|
+
printer = RubyProf::FlatPrinter.new(result)
|
57
|
+
printer.print(STDOUT)
|
58
|
+
end
|
59
|
+
|
60
|
+
benchmark
|
61
|
+
profile
|
62
|
+
ruby_prof if ENV["FULL_BENCH"] # takes some time
|