contracts 0.5 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/TUTORIAL.md +42 -1
- data/benchmarks/bench.rb +3 -3
- data/contracts.gemspec +0 -1
- data/lib/contracts.rb +65 -54
- data/lib/contracts/core_ext.rb +15 -0
- data/lib/contracts/decorators.rb +78 -46
- data/lib/contracts/eigenclass.rb +41 -0
- data/lib/contracts/errors.rb +65 -0
- data/lib/contracts/invariants.rb +0 -6
- data/lib/contracts/method_reference.rb +75 -0
- data/lib/contracts/modules.rb +17 -0
- data/lib/contracts/support.rb +16 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +3 -5
- data/spec/contracts_spec.rb +193 -5
- data/spec/fixtures/fixtures.rb +244 -113
- data/spec/module_spec.rb +2 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support.rb +6 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2aee00ef1803ee1ac8e84e96156494f7b7c4162a
|
4
|
+
data.tar.gz: a3798c342c9b29170f523f23083baeb2553cbd83
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1404d6dd2fe5212b9c7a8bfa682b982bf612161200e33bc14ab73609eaac0681b85540a182a3d4d2437d3446380241785d229b6a8145c5a8c3fda8e37f682f5
|
7
|
+
data.tar.gz: 0912d5946702185218f94d1058577e92fd9f7b668e248ab4d796e67b620455f4a43e2a0e02d4871f8def9c8666feb3f4343c4f3f6d04d6c1a25b97541c7afd27
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
data/TUTORIAL.md
CHANGED
@@ -405,7 +405,9 @@ If you want to disable contracts, set the `NO_CONTRACTS` environment variable. T
|
|
405
405
|
|
406
406
|
## Method overloading
|
407
407
|
|
408
|
-
You can use contracts for method overloading!
|
408
|
+
You can use contracts for method overloading! This is commonly called "pattern matching" in functional programming languages.
|
409
|
+
|
410
|
+
For example, here's a factorial function without method overloading:
|
409
411
|
|
410
412
|
```ruby
|
411
413
|
Contract Num => Num
|
@@ -434,6 +436,45 @@ end
|
|
434
436
|
|
435
437
|
For an argument, each function will be tried in order. The first function that doesn't raise a `ContractError` will be used. So in this case, if x == 1, the first function will be used. For all other values, the second function will be used.
|
436
438
|
|
439
|
+
This allows you write methods more declaratively, rather than using conditional branching. This feature is not only useful for recursion; you can use it to keep parallel use cases separate:
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
Contract And[Num, lambda{|n| n < 12 }] => Ticket
|
443
|
+
def get_ticket(age)
|
444
|
+
ChildTicket.new(age: age)
|
445
|
+
end
|
446
|
+
|
447
|
+
Contract And[Num, lambda{|n| n >= 12 }] => Ticket
|
448
|
+
def get_ticket(age)
|
449
|
+
AdultTicket.new(age: age)
|
450
|
+
end
|
451
|
+
|
452
|
+
```
|
453
|
+
|
454
|
+
Note that the second `get_ticket` contract above could have been simplified to:
|
455
|
+
|
456
|
+
```ruby
|
457
|
+
Contract Num => Ticket
|
458
|
+
```
|
459
|
+
|
460
|
+
This is because the first contract eliminated the possibility of `age` being less than 12. However, the simpler contract is less explicit; you may want to "spell out" the age condition for clarity, especially if the method is overloaded with many contracts.
|
461
|
+
|
462
|
+
## Contracts in modules
|
463
|
+
|
464
|
+
To use contracts on module you need to include both `Contracts` and `Contracts::Modules` into it:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
module M
|
468
|
+
include Contracts
|
469
|
+
include Contracts::Modules
|
470
|
+
|
471
|
+
Contract String => String
|
472
|
+
def self.parse
|
473
|
+
# do some hard parsing
|
474
|
+
end
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
437
478
|
## Invariants
|
438
479
|
|
439
480
|
Invariants are conditions on objects that should always hold. If after any method call on given object, any of the Invariants fails, then Invariant violation error will be generated.
|
data/benchmarks/bench.rb
CHANGED
@@ -34,7 +34,7 @@ def benchmark
|
|
34
34
|
1000000.times do |_|
|
35
35
|
contracts_add(rand(1000), rand(1000))
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -48,12 +48,12 @@ def profile
|
|
48
48
|
profilers << MethodProfiler.observe(UnboundMethod)
|
49
49
|
10000.times do |_|
|
50
50
|
contracts_add(rand(1000), rand(1000))
|
51
|
-
end
|
51
|
+
end
|
52
52
|
profilers.each { |p| puts p.report }
|
53
53
|
end
|
54
54
|
|
55
55
|
def ruby_prof
|
56
|
-
RubyProf.start
|
56
|
+
RubyProf.start
|
57
57
|
100000.times do |_|
|
58
58
|
contracts_add(rand(1000), rand(1000))
|
59
59
|
end
|
data/contracts.gemspec
CHANGED
@@ -3,7 +3,6 @@ require File.expand_path(File.join(__FILE__, '../lib/contracts/version'))
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "contracts"
|
5
5
|
s.version = Contracts::VERSION
|
6
|
-
s.date = "2014-05-08"
|
7
6
|
s.summary = "Contracts for Ruby."
|
8
7
|
s.description = "This library provides contracts for Ruby. Contracts let you clearly express how your code behaves, and free you from writing tons of boilerplate, defensive code."
|
9
8
|
s.author = "Aditya Bhargava"
|
data/lib/contracts.rb
CHANGED
@@ -1,54 +1,29 @@
|
|
1
|
+
require 'contracts/core_ext'
|
1
2
|
require 'contracts/support'
|
3
|
+
require 'contracts/method_reference'
|
4
|
+
require 'contracts/errors'
|
2
5
|
require 'contracts/decorators'
|
6
|
+
require 'contracts/eigenclass'
|
3
7
|
require 'contracts/builtin_contracts'
|
8
|
+
require 'contracts/modules'
|
4
9
|
require 'contracts/invariants'
|
5
10
|
|
6
|
-
# @private
|
7
|
-
# Base class for Contract errors
|
8
|
-
#
|
9
|
-
# If default failure callback is used it stores failure data
|
10
|
-
class ContractBaseError < ArgumentError
|
11
|
-
attr_reader :data
|
12
|
-
|
13
|
-
def initialize(message, data)
|
14
|
-
super(message)
|
15
|
-
@data = data
|
16
|
-
end
|
17
|
-
|
18
|
-
# Used to convert to simple ContractError from other contract errors
|
19
|
-
def to_contract_error
|
20
|
-
self
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Default contract error
|
25
|
-
#
|
26
|
-
# If default failure callback is used, users normally see only these contract errors
|
27
|
-
class ContractError < ContractBaseError
|
28
|
-
end
|
29
|
-
|
30
|
-
# @private
|
31
|
-
# Special contract error used internally to detect pattern failure during pattern matching
|
32
|
-
class PatternMatchingError < ContractBaseError
|
33
|
-
# Used to convert to ContractError from PatternMatchingError
|
34
|
-
def to_contract_error
|
35
|
-
ContractError.new(to_s, data)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
11
|
module Contracts
|
40
12
|
def self.included(base)
|
41
|
-
common
|
13
|
+
common(base)
|
42
14
|
end
|
43
15
|
|
44
16
|
def self.extended(base)
|
45
|
-
common
|
17
|
+
common(base)
|
46
18
|
end
|
47
19
|
|
48
|
-
def self.common
|
20
|
+
def self.common(base)
|
21
|
+
Eigenclass.lift(base)
|
22
|
+
|
49
23
|
return if base.respond_to?(:Contract)
|
50
24
|
|
51
|
-
base.extend
|
25
|
+
base.extend(MethodDecorators)
|
26
|
+
|
52
27
|
base.instance_eval do
|
53
28
|
def functype(funcname)
|
54
29
|
contracts = self.decorated_methods[:class_methods][funcname]
|
@@ -59,10 +34,13 @@ module Contracts
|
|
59
34
|
end
|
60
35
|
end
|
61
36
|
end
|
37
|
+
|
62
38
|
base.class_eval do
|
63
|
-
|
64
|
-
|
65
|
-
|
39
|
+
unless base.instance_of?(Module)
|
40
|
+
def Contract(*args)
|
41
|
+
return if ENV["NO_CONTRACTS"]
|
42
|
+
self.class.Contract(*args)
|
43
|
+
end
|
66
44
|
end
|
67
45
|
|
68
46
|
def functype(funcname)
|
@@ -131,8 +109,8 @@ class Contract < Contracts::Decorator
|
|
131
109
|
data[:contract].to_s
|
132
110
|
end
|
133
111
|
|
134
|
-
position = Support.method_position(data[:method])
|
135
|
-
method_name = Support.method_name(data[:method])
|
112
|
+
position = Contracts::Support.method_position(data[:method])
|
113
|
+
method_name = Contracts::Support.method_name(data[:method])
|
136
114
|
|
137
115
|
header = if data[:return_value]
|
138
116
|
"Contract violation for return value:"
|
@@ -261,39 +239,72 @@ class Contract < Contracts::Decorator
|
|
261
239
|
end
|
262
240
|
|
263
241
|
def call_with(this, *args, &blk)
|
264
|
-
|
265
242
|
_args = blk ? args + [blk] : args
|
266
243
|
|
244
|
+
size = @args_contracts.size
|
245
|
+
|
246
|
+
last_contract = @args_contracts.last
|
247
|
+
proc_present = (Contracts::Func === last_contract ||
|
248
|
+
(Class === last_contract && (last_contract <= Proc || last_contract <= Method)))
|
249
|
+
|
250
|
+
_splat_index = proc_present ? -2 : -1
|
251
|
+
splat_present = Contracts::Args === @args_contracts[_splat_index]
|
252
|
+
splat_index = size + _splat_index
|
253
|
+
splat_index += 1 unless splat_present
|
254
|
+
|
255
|
+
# Explicitly append blk=nil if nil != Proc contract violation
|
256
|
+
# anticipated
|
257
|
+
if proc_present && !blk && (splat_present || _args.size < size)
|
258
|
+
_args << nil
|
259
|
+
end
|
260
|
+
|
261
|
+
# Size of our iteration is either count of arguments or count of
|
262
|
+
# contracts. Without splat - count of arguments, with splat -
|
263
|
+
# min(count of contracts, count of arguments)
|
264
|
+
_size = _args.size
|
265
|
+
iteration_size = _size
|
266
|
+
iteration_size = size if !splat_present && size < _size
|
267
|
+
|
267
268
|
# check contracts on arguments
|
268
269
|
# fun fact! This is significantly faster than .zip (3.7 secs vs 4.7 secs). Why??
|
269
|
-
|
270
|
+
|
270
271
|
# times is faster than (0..args.size).each
|
271
|
-
|
272
|
+
iteration_size.times do |i|
|
272
273
|
# this is done to account for extra args (for *args)
|
273
|
-
j = i
|
274
|
+
j = i > splat_index ? splat_index : i
|
275
|
+
j = size - 1 if i == _size - 1 && proc_present
|
274
276
|
#unless true #@args_contracts[i].valid?(args[i])
|
275
277
|
unless @args_validators[j][_args[i]]
|
276
|
-
call_function = Contract.failure_callback({:arg => _args[i], :contract => @args_contracts[j], :class => @klass, :method => @method, :contracts => self, :arg_pos => i+1, :total_args =>
|
278
|
+
call_function = Contract.failure_callback({:arg => _args[i], :contract => @args_contracts[j], :class => @klass, :method => @method, :contracts => self, :arg_pos => i+1, :total_args => _size})
|
277
279
|
return unless call_function
|
278
280
|
end
|
279
281
|
end
|
280
282
|
|
281
283
|
if @has_func_contracts
|
282
284
|
# contracts on methods
|
285
|
+
contracts_size = @args_contracts.size
|
283
286
|
@args_contracts.each_with_index do |contract, i|
|
284
|
-
if
|
285
|
-
|
287
|
+
next if contracts_size - 1 == i && proc_present && blk
|
288
|
+
|
289
|
+
if contract.is_a?(Contracts::Func)
|
290
|
+
args[i] = Contract.new(@klass, args[i], *contract.contracts)
|
286
291
|
end
|
287
292
|
end
|
293
|
+
|
294
|
+
if proc_present && blk && last_contract.is_a?(Contracts::Func)
|
295
|
+
blk_contract = Contract.new(@klass, blk, *last_contract.contracts)
|
296
|
+
blk = Proc.new { |*args, &blk| blk_contract.call(*args, &blk) }
|
297
|
+
end
|
288
298
|
end
|
289
299
|
|
290
|
-
result = if @method.respond_to?
|
291
|
-
#
|
292
|
-
@method.bind(this).call(*args, &blk)
|
293
|
-
else
|
294
|
-
# class method
|
300
|
+
result = if @method.respond_to?(:call)
|
301
|
+
# proc, block, lambda, etc
|
295
302
|
@method.call(*args, &blk)
|
303
|
+
else
|
304
|
+
# original method name referrence
|
305
|
+
@method.send_to(this, *args, &blk)
|
296
306
|
end
|
307
|
+
|
297
308
|
unless @ret_validator[result]
|
298
309
|
Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self, :return_value => true})
|
299
310
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Module
|
2
|
+
unless Object.respond_to?(:singleton_class)
|
3
|
+
# Compatibility with ruby 1.8
|
4
|
+
def singleton_class
|
5
|
+
class << self; self; end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
unless Object.respond_to?(:singleton_class?)
|
10
|
+
# Compatibility with ruby 1.8
|
11
|
+
def singleton_class?
|
12
|
+
self <= Object.singleton_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/contracts/decorators.rb
CHANGED
@@ -8,6 +8,22 @@ module Contracts
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
module EigenclassWithOwner
|
12
|
+
def self.lift(eigenclass)
|
13
|
+
unless with_owner?(eigenclass)
|
14
|
+
raise Contracts::ContractsNotIncluded
|
15
|
+
end
|
16
|
+
|
17
|
+
eigenclass
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.with_owner?(eigenclass)
|
23
|
+
eigenclass.respond_to?(:owner_class) && eigenclass.owner_class
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
11
27
|
# first, when you write a contract, the decorate method gets called which
|
12
28
|
# sets the @decorators variable. Then when the next method after the contract
|
13
29
|
# is defined, method_added is called and we look at the @decorators variable
|
@@ -18,25 +34,32 @@ module Contracts
|
|
18
34
|
super
|
19
35
|
end
|
20
36
|
|
21
|
-
def singleton_method_added
|
37
|
+
def singleton_method_added(name)
|
22
38
|
common_method_added name, true
|
23
39
|
super
|
24
40
|
end
|
25
41
|
|
26
|
-
def
|
27
|
-
|
42
|
+
def pop_decorators
|
43
|
+
Array(@decorators).tap { @decorators = nil }
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_decorators
|
47
|
+
pop_decorators + Eigenclass.lift(self).pop_decorators
|
48
|
+
end
|
49
|
+
|
50
|
+
def common_method_added(name, is_class_method)
|
51
|
+
decorators = fetch_decorators
|
52
|
+
return if decorators.empty?
|
28
53
|
|
29
|
-
decorators = @decorators.dup
|
30
|
-
@decorators = nil
|
31
54
|
@decorated_methods ||= {:class_methods => {}, :instance_methods => {}}
|
32
55
|
|
33
56
|
if is_class_method
|
34
|
-
method_reference = method(name)
|
57
|
+
method_reference = SingletonMethodReference.new(name, method(name))
|
35
58
|
method_type = :class_methods
|
36
59
|
# private_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
37
60
|
is_private = self.private_methods.include?(name) || self.private_methods.include?(name.to_s)
|
38
61
|
else
|
39
|
-
method_reference = instance_method(name)
|
62
|
+
method_reference = MethodReference.new(name, instance_method(name))
|
40
63
|
method_type = :instance_methods
|
41
64
|
# private_instance_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
42
65
|
is_private = self.private_instance_methods.include?(name) || self.private_instance_methods.include?(name.to_s)
|
@@ -44,6 +67,7 @@ module Contracts
|
|
44
67
|
|
45
68
|
@decorated_methods[method_type][name] ||= []
|
46
69
|
|
70
|
+
pattern_matching = false
|
47
71
|
decorators.each do |klass, args|
|
48
72
|
# a reference to the method gets passed into the contract here. This is good because
|
49
73
|
# we are going to redefine this method with a new name below...so this reference is
|
@@ -51,14 +75,21 @@ module Contracts
|
|
51
75
|
# We assume here that the decorator (klass) responds to .new
|
52
76
|
decorator = klass.new(self, method_reference, *args)
|
53
77
|
@decorated_methods[method_type][name] << decorator
|
78
|
+
pattern_matching ||= decorator.pattern_match?
|
54
79
|
end
|
55
80
|
|
56
81
|
if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
|
57
82
|
@decorated_methods[method_type][name].each do |decorator|
|
58
83
|
decorator.pattern_match!
|
59
84
|
end
|
85
|
+
|
86
|
+
pattern_matching = true
|
60
87
|
end
|
61
88
|
|
89
|
+
method_reference.make_alias(self)
|
90
|
+
|
91
|
+
return if ENV["NO_CONTRACTS"] && !pattern_matching
|
92
|
+
|
62
93
|
# in place of this method, we are going to define our own method. This method
|
63
94
|
# just calls the decorator passing in all args that were to be passed into the method.
|
64
95
|
# The decorator in turn has a reference to the actual method, so it can call it
|
@@ -100,52 +131,54 @@ Here's why: Suppose you have this code:
|
|
100
131
|
means you would always call Bar's to_s...infinite recursion! Instead, you want to
|
101
132
|
call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
|
102
133
|
=end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
134
|
+
|
135
|
+
current = self
|
136
|
+
method_reference.make_definition(self) do |*args, &blk|
|
137
|
+
ancestors = current.ancestors
|
138
|
+
ancestors.shift # first one is just the class itself
|
139
|
+
while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
140
|
+
current = ancestors.shift
|
141
|
+
end
|
142
|
+
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
143
|
+
raise "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
|
144
|
+
end
|
145
|
+
methods = current.decorated_methods[method_type][name]
|
146
|
+
|
147
|
+
# this adds support for overloading methods. Here we go through each method and call it with the arguments.
|
148
|
+
# If we get a ContractError, we move to the next function. Otherwise we return the result.
|
149
|
+
# If we run out of functions, we raise the last ContractError.
|
150
|
+
success = false
|
151
|
+
i = 0
|
152
|
+
result = nil
|
153
|
+
expected_error = methods[0].failure_exception
|
154
|
+
while !success
|
155
|
+
method = methods[i]
|
156
|
+
i += 1
|
157
|
+
begin
|
158
|
+
success = true
|
159
|
+
result = method.call_with(self, *args, &blk)
|
160
|
+
rescue expected_error => error
|
161
|
+
success = false
|
162
|
+
unless methods[i]
|
163
|
+
begin
|
164
|
+
::Contract.failure_callback(error.data, false)
|
165
|
+
rescue expected_error => final_error
|
166
|
+
raise final_error.to_contract_error
|
137
167
|
end
|
138
168
|
end
|
139
169
|
end
|
140
|
-
result
|
141
170
|
end
|
142
|
-
|
143
|
-
|
171
|
+
result
|
172
|
+
end
|
144
173
|
|
145
|
-
|
174
|
+
method_reference.make_private(self) if is_private
|
146
175
|
end
|
147
176
|
|
148
177
|
def decorate(klass, *args)
|
178
|
+
if self.singleton_class?
|
179
|
+
return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args)
|
180
|
+
end
|
181
|
+
|
149
182
|
@decorators ||= []
|
150
183
|
@decorators << [klass, args]
|
151
184
|
end
|
@@ -166,7 +199,6 @@ Here's why: Suppose you have this code:
|
|
166
199
|
# inside, `decorate` is called with those params.
|
167
200
|
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
168
201
|
def #{klass}(*args, &blk)
|
169
|
-
return if ENV["NO_CONTRACTS"]
|
170
202
|
decorate(#{klass}, *args, &blk)
|
171
203
|
end
|
172
204
|
ruby_eval
|