contracts 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +34 -0
- data/README.md +75 -0
- data/TODO.markdown +6 -0
- data/TUTORIAL.md +485 -0
- data/benchmarks/bench.rb +67 -0
- data/benchmarks/invariants.rb +81 -0
- data/benchmarks/wrap_test.rb +59 -0
- data/contracts.gemspec +13 -0
- data/lib/contracts.rb +108 -23
- data/lib/{builtin_contracts.rb → contracts/builtin_contracts.rb} +1 -1
- data/lib/contracts/decorators.rb +179 -0
- data/lib/contracts/invariants.rb +75 -0
- data/lib/contracts/support.rb +22 -0
- data/lib/{testable.rb → contracts/testable.rb} +0 -0
- data/lib/contracts/version.rb +3 -0
- data/spec/builtin_contracts_spec.rb +216 -0
- data/spec/contracts_spec.rb +273 -0
- data/spec/fixtures/fixtures.rb +276 -0
- data/spec/invariants_spec.rb +19 -0
- data/spec/module_spec.rb +17 -0
- data/spec/spec_helper.rb +94 -0
- metadata +45 -43
- data/lib/decorators.rb +0 -164
data/benchmarks/bench.rb
ADDED
@@ -0,0 +1,67 @@
|
|
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 a, b
|
10
|
+
a + b
|
11
|
+
end
|
12
|
+
|
13
|
+
Contract Num, Num => Num
|
14
|
+
def contracts_add a, b
|
15
|
+
a + b
|
16
|
+
end
|
17
|
+
|
18
|
+
def explicit_add a, b
|
19
|
+
raise unless a.is_a?(Numeric)
|
20
|
+
raise unless b.is_a?(Numeric)
|
21
|
+
c = a + b
|
22
|
+
raise unless c.is_a?(Numeric)
|
23
|
+
c
|
24
|
+
end
|
25
|
+
|
26
|
+
def benchmark
|
27
|
+
Benchmark.bm 30 do |x|
|
28
|
+
x.report 'testing add' do
|
29
|
+
1000000.times do |_|
|
30
|
+
add(rand(1000), rand(1000))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
x.report 'testing contracts add' do
|
34
|
+
1000000.times do |_|
|
35
|
+
contracts_add(rand(1000), rand(1000))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def profile
|
42
|
+
profilers = []
|
43
|
+
profilers << MethodProfiler.observe(Contract)
|
44
|
+
profilers << MethodProfiler.observe(Object)
|
45
|
+
profilers << MethodProfiler.observe(Contracts::MethodDecorators)
|
46
|
+
profilers << MethodProfiler.observe(Contracts::Decorator)
|
47
|
+
profilers << MethodProfiler.observe(Contracts::Support)
|
48
|
+
profilers << MethodProfiler.observe(UnboundMethod)
|
49
|
+
10000.times do |_|
|
50
|
+
contracts_add(rand(1000), rand(1000))
|
51
|
+
end
|
52
|
+
profilers.each { |p| puts p.report }
|
53
|
+
end
|
54
|
+
|
55
|
+
def ruby_prof
|
56
|
+
RubyProf.start
|
57
|
+
100000.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
|
+
end
|
64
|
+
|
65
|
+
benchmark
|
66
|
+
profile
|
67
|
+
ruby_prof if ENV["FULL_BENCH"] # takes some time
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require './lib/contracts'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'method_profiler'
|
5
|
+
require 'ruby-prof'
|
6
|
+
|
7
|
+
class Obj < Struct.new(:value)
|
8
|
+
include Contracts
|
9
|
+
|
10
|
+
Contract Num, Num => Num
|
11
|
+
def contracts_add a, b
|
12
|
+
a + b
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ObjWithInvariants < Struct.new(:value)
|
17
|
+
include Contracts
|
18
|
+
include Contracts::Invariants
|
19
|
+
|
20
|
+
Invariant(:value_not_nil) { value != nil }
|
21
|
+
Invariant(:value_not_string) { !value.is_a?(String) }
|
22
|
+
|
23
|
+
Contract Num, Num => Num
|
24
|
+
def contracts_add a, b
|
25
|
+
a + b
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def benchmark
|
30
|
+
obj = Obj.new(3)
|
31
|
+
obj_with_invariants = ObjWithInvariants.new(3)
|
32
|
+
|
33
|
+
Benchmark.bm 30 do |x|
|
34
|
+
x.report 'testing contracts add' do
|
35
|
+
1000000.times do |_|
|
36
|
+
obj.contracts_add(rand(1000), rand(1000))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
x.report 'testing contracts add with invariants' do
|
40
|
+
1000000.times do |_|
|
41
|
+
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def profile
|
48
|
+
obj_with_invariants = ObjWithInvariants.new(3)
|
49
|
+
|
50
|
+
profilers = []
|
51
|
+
profilers << MethodProfiler.observe(Contract)
|
52
|
+
profilers << MethodProfiler.observe(Object)
|
53
|
+
profilers << MethodProfiler.observe(Contracts::Support)
|
54
|
+
profilers << MethodProfiler.observe(Contracts::Invariants)
|
55
|
+
profilers << MethodProfiler.observe(Contracts::Invariants::InvariantExtension)
|
56
|
+
profilers << MethodProfiler.observe(UnboundMethod)
|
57
|
+
|
58
|
+
10000.times do |_|
|
59
|
+
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
60
|
+
end
|
61
|
+
|
62
|
+
profilers.each { |p| puts p.report }
|
63
|
+
end
|
64
|
+
|
65
|
+
def ruby_prof
|
66
|
+
RubyProf.start
|
67
|
+
|
68
|
+
obj_with_invariants = ObjWithInvariants.new(3)
|
69
|
+
|
70
|
+
100000.times do |_|
|
71
|
+
obj_with_invariants.contracts_add(rand(1000), rand(1000))
|
72
|
+
end
|
73
|
+
|
74
|
+
result = RubyProf.stop
|
75
|
+
printer = RubyProf::FlatPrinter.new(result)
|
76
|
+
printer.print(STDOUT)
|
77
|
+
end
|
78
|
+
|
79
|
+
benchmark
|
80
|
+
profile
|
81
|
+
ruby_prof if ENV["FULL_BENCH"] # takes some time
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
module Wrapper
|
4
|
+
def self.extended(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
@@methods = {}
|
7
|
+
def self.methods
|
8
|
+
@@methods
|
9
|
+
end
|
10
|
+
def self.set_method k, v
|
11
|
+
@@methods[k] = v
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_added name
|
17
|
+
return if methods.include?(name)
|
18
|
+
puts "#{name} added"
|
19
|
+
set_method(name, instance_method(name))
|
20
|
+
class_eval %{
|
21
|
+
def #{name}(*args)
|
22
|
+
self.class.methods[#{name.inspect}].bind(self).call(*args)
|
23
|
+
end
|
24
|
+
}, __FILE__, __LINE__ + 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NotWrapped
|
29
|
+
def add a, b
|
30
|
+
a + b
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Wrapped
|
35
|
+
extend ::Wrapper
|
36
|
+
def add a, b
|
37
|
+
a + b
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
w = Wrapped.new
|
43
|
+
nw = NotWrapped.new
|
44
|
+
#p w.add(1, 4)
|
45
|
+
#exit
|
46
|
+
# 30 is the width of the output column
|
47
|
+
Benchmark.bm 30 do |x|
|
48
|
+
x.report 'wrapped' do
|
49
|
+
100000.times do |_|
|
50
|
+
w.add(rand(1000), rand(1000))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
x.report 'not wrapped' do
|
54
|
+
100000.times do |_|
|
55
|
+
nw.add(rand(1000), rand(1000))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
data/contracts.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.join(__FILE__, '../lib/contracts/version'))
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "contracts"
|
5
|
+
s.version = Contracts::VERSION
|
6
|
+
s.date = "2014-05-08"
|
7
|
+
s.summary = "Contracts for Ruby."
|
8
|
+
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
|
+
s.author = "Aditya Bhargava"
|
10
|
+
s.email = "bluemangroupie@gmail.com"
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.homepage = "http://github.com/egonSchiele/contracts.ruby"
|
13
|
+
end
|
data/lib/contracts.rb
CHANGED
@@ -1,7 +1,39 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
1
|
+
require 'contracts/support'
|
2
|
+
require 'contracts/decorators'
|
3
|
+
require 'contracts/builtin_contracts'
|
4
|
+
require 'contracts/invariants'
|
3
5
|
|
4
|
-
|
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
|
5
37
|
end
|
6
38
|
|
7
39
|
module Contracts
|
@@ -14,6 +46,8 @@ module Contracts
|
|
14
46
|
end
|
15
47
|
|
16
48
|
def self.common base
|
49
|
+
return if base.respond_to?(:Contract)
|
50
|
+
|
17
51
|
base.extend MethodDecorators
|
18
52
|
base.instance_eval do
|
19
53
|
def functype(funcname)
|
@@ -27,6 +61,7 @@ module Contracts
|
|
27
61
|
end
|
28
62
|
base.class_eval do
|
29
63
|
def Contract(*args)
|
64
|
+
return if ENV["NO_CONTRACTS"]
|
30
65
|
self.class.Contract(*args)
|
31
66
|
end
|
32
67
|
|
@@ -39,7 +74,7 @@ module Contracts
|
|
39
74
|
end
|
40
75
|
end
|
41
76
|
end
|
42
|
-
end
|
77
|
+
end
|
43
78
|
end
|
44
79
|
|
45
80
|
# This is the main Contract class. When you write a new contract, you'll
|
@@ -48,7 +83,14 @@ end
|
|
48
83
|
# Contract [contract names] => return_value
|
49
84
|
#
|
50
85
|
# This class also provides useful callbacks and a validation method.
|
51
|
-
class Contract < Decorator
|
86
|
+
class Contract < Contracts::Decorator
|
87
|
+
# Default implementation of failure_callback. Provided as a block to be able
|
88
|
+
# to monkey patch #failure_callback only temporary and then switch it back.
|
89
|
+
# First important usage - for specs.
|
90
|
+
DEFAULT_FAILURE_CALLBACK = Proc.new do |data|
|
91
|
+
raise data[:contracts].failure_exception.new(failure_msg(data), data)
|
92
|
+
end
|
93
|
+
|
52
94
|
attr_reader :args_contracts, :ret_contract, :klass, :method
|
53
95
|
# decorator_name :contract
|
54
96
|
def initialize(klass, method, *contracts)
|
@@ -89,17 +131,8 @@ class Contract < Decorator
|
|
89
131
|
data[:contract].to_s
|
90
132
|
end
|
91
133
|
|
92
|
-
|
93
|
-
|
94
|
-
position = data[:method].__file__ + ":" + data[:method].__line__.to_s
|
95
|
-
else
|
96
|
-
position = data[:method].inspect
|
97
|
-
end
|
98
|
-
else
|
99
|
-
file, line = data[:method].source_location
|
100
|
-
position = file + ":" + line.to_s
|
101
|
-
end
|
102
|
-
method_name = data[:method].is_a?(Proc) ? "Proc" : data[:method].name
|
134
|
+
position = Support.method_position(data[:method])
|
135
|
+
method_name = Support.method_name(data[:method])
|
103
136
|
|
104
137
|
header = if data[:return_value]
|
105
138
|
"Contract violation for return value:"
|
@@ -123,13 +156,41 @@ class Contract < Decorator
|
|
123
156
|
#
|
124
157
|
# Example of monkeypatching:
|
125
158
|
#
|
126
|
-
# Contract.failure_callback(data)
|
159
|
+
# def Contract.failure_callback(data)
|
127
160
|
# puts "You had an error!"
|
128
161
|
# puts failure_msg(data)
|
129
162
|
# exit
|
130
163
|
# end
|
131
|
-
def self.failure_callback(data)
|
132
|
-
|
164
|
+
def self.failure_callback(data, use_pattern_matching=true)
|
165
|
+
if data[:contracts].pattern_match? && use_pattern_matching
|
166
|
+
return DEFAULT_FAILURE_CALLBACK.call(data)
|
167
|
+
end
|
168
|
+
|
169
|
+
fetch_failure_callback.call(data)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Used to override failure_callback without monkeypatching.
|
173
|
+
#
|
174
|
+
# Takes: block parameter, that should accept one argument - data.
|
175
|
+
#
|
176
|
+
# Example usage:
|
177
|
+
#
|
178
|
+
# Contract.override_failure_callback do |data|
|
179
|
+
# puts "You had an error"
|
180
|
+
# puts failure_msg(data)
|
181
|
+
# exit
|
182
|
+
# end
|
183
|
+
def self.override_failure_callback(&blk)
|
184
|
+
@failure_callback = blk
|
185
|
+
end
|
186
|
+
|
187
|
+
# Used to restore default failure callback
|
188
|
+
def self.restore_failure_callback
|
189
|
+
@failure_callback = DEFAULT_FAILURE_CALLBACK
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.fetch_failure_callback
|
193
|
+
@failure_callback ||= DEFAULT_FAILURE_CALLBACK
|
133
194
|
end
|
134
195
|
|
135
196
|
# Used to verify if an argument satisfies a contract.
|
@@ -157,7 +218,7 @@ class Contract < Decorator
|
|
157
218
|
# e.g. [Num, String]
|
158
219
|
# TODO account for these errors too
|
159
220
|
lambda { |arg|
|
160
|
-
return false unless arg.is_a?(Array)
|
221
|
+
return false unless arg.is_a?(Array) && arg.length == contract.length
|
161
222
|
arg.zip(contract).all? do |_arg, _contract|
|
162
223
|
Contract.valid?(_arg, _contract)
|
163
224
|
end
|
@@ -186,10 +247,10 @@ class Contract < Decorator
|
|
186
247
|
elsif klass == Class
|
187
248
|
lambda { |arg| arg.is_a?(contract) }
|
188
249
|
else
|
189
|
-
lambda { |arg| contract == arg }
|
250
|
+
lambda { |arg| contract == arg }
|
190
251
|
end
|
191
252
|
end
|
192
|
-
end
|
253
|
+
end
|
193
254
|
|
194
255
|
def [](*args, &blk)
|
195
256
|
call(*args, &blk)
|
@@ -200,6 +261,7 @@ class Contract < Decorator
|
|
200
261
|
end
|
201
262
|
|
202
263
|
def call_with(this, *args, &blk)
|
264
|
+
|
203
265
|
_args = blk ? args + [blk] : args
|
204
266
|
|
205
267
|
# check contracts on arguments
|
@@ -234,7 +296,30 @@ class Contract < Decorator
|
|
234
296
|
end
|
235
297
|
unless @ret_validator[result]
|
236
298
|
Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self, :return_value => true})
|
237
|
-
end
|
299
|
+
end
|
300
|
+
|
301
|
+
this.verify_invariants!(@method) if this.respond_to?(:verify_invariants!)
|
302
|
+
|
238
303
|
result
|
239
304
|
end
|
305
|
+
|
306
|
+
# Used to determine type of failure exception this contract should raise in case of failure
|
307
|
+
def failure_exception
|
308
|
+
if @pattern_match
|
309
|
+
PatternMatchingError
|
310
|
+
else
|
311
|
+
ContractError
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# @private
|
316
|
+
# Used internally to mark contract as pattern matching contract
|
317
|
+
def pattern_match!
|
318
|
+
@pattern_match = true
|
319
|
+
end
|
320
|
+
|
321
|
+
# Used to determine if contract is a pattern matching contract
|
322
|
+
def pattern_match?
|
323
|
+
@pattern_match
|
324
|
+
end
|
240
325
|
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Contracts
|
2
|
+
module MethodDecorators
|
3
|
+
def self.extended(klass)
|
4
|
+
return if klass.respond_to?(:decorated_methods=)
|
5
|
+
|
6
|
+
class << klass
|
7
|
+
attr_accessor :decorated_methods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# first, when you write a contract, the decorate method gets called which
|
12
|
+
# sets the @decorators variable. Then when the next method after the contract
|
13
|
+
# is defined, method_added is called and we look at the @decorators variable
|
14
|
+
# to find the decorator for that method. This is how we associate decorators
|
15
|
+
# with methods.
|
16
|
+
def method_added(name)
|
17
|
+
common_method_added name, false
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def singleton_method_added name
|
22
|
+
common_method_added name, true
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def common_method_added name, is_class_method
|
27
|
+
return unless @decorators
|
28
|
+
|
29
|
+
decorators = @decorators.dup
|
30
|
+
@decorators = nil
|
31
|
+
@decorated_methods ||= {:class_methods => {}, :instance_methods => {}}
|
32
|
+
|
33
|
+
if is_class_method
|
34
|
+
method_reference = method(name)
|
35
|
+
method_type = :class_methods
|
36
|
+
# private_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
37
|
+
is_private = self.private_methods.include?(name) || self.private_methods.include?(name.to_s)
|
38
|
+
else
|
39
|
+
method_reference = instance_method(name)
|
40
|
+
method_type = :instance_methods
|
41
|
+
# private_instance_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
42
|
+
is_private = self.private_instance_methods.include?(name) || self.private_instance_methods.include?(name.to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
@decorated_methods[method_type][name] ||= []
|
46
|
+
|
47
|
+
decorators.each do |klass, args|
|
48
|
+
# a reference to the method gets passed into the contract here. This is good because
|
49
|
+
# we are going to redefine this method with a new name below...so this reference is
|
50
|
+
# now the *only* reference to the old method that exists.
|
51
|
+
# We assume here that the decorator (klass) responds to .new
|
52
|
+
decorator = klass.new(self, method_reference, *args)
|
53
|
+
@decorated_methods[method_type][name] << decorator
|
54
|
+
end
|
55
|
+
|
56
|
+
if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
|
57
|
+
@decorated_methods[method_type][name].each do |decorator|
|
58
|
+
decorator.pattern_match!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# in place of this method, we are going to define our own method. This method
|
63
|
+
# just calls the decorator passing in all args that were to be passed into the method.
|
64
|
+
# The decorator in turn has a reference to the actual method, so it can call it
|
65
|
+
# on its own, after doing it's decorating of course.
|
66
|
+
|
67
|
+
=begin
|
68
|
+
Very important: THe line `current = #{self}` in the start is crucial.
|
69
|
+
Not having it means that any method that used contracts could NOT use `super`
|
70
|
+
(see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
|
71
|
+
Here's why: Suppose you have this code:
|
72
|
+
|
73
|
+
class Foo
|
74
|
+
Contract nil => String
|
75
|
+
def to_s
|
76
|
+
"Foo"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Bar < Foo
|
81
|
+
Contract nil => String
|
82
|
+
def to_s
|
83
|
+
super + "Bar"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
b = Bar.new
|
88
|
+
p b.to_s
|
89
|
+
|
90
|
+
`to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
|
91
|
+
we have overwritten the function (that's what this next defn is). So it gets a
|
92
|
+
reference to the function to call by looking at `decorated_methods`.
|
93
|
+
|
94
|
+
Now, this line used to read something like:
|
95
|
+
|
96
|
+
current = self#{is_class_method ? "" : ".class"}
|
97
|
+
|
98
|
+
In that case, `self` would always be `Bar`, regardless of whether you were calling
|
99
|
+
Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
|
100
|
+
means you would always call Bar's to_s...infinite recursion! Instead, you want to
|
101
|
+
call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
|
102
|
+
=end
|
103
|
+
method_def = %{
|
104
|
+
def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
|
105
|
+
current = #{self}
|
106
|
+
ancestors = current.ancestors
|
107
|
+
ancestors.shift # first one is just the class itself
|
108
|
+
while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
109
|
+
current = ancestors.shift
|
110
|
+
end
|
111
|
+
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
112
|
+
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."
|
113
|
+
end
|
114
|
+
methods = current.decorated_methods[#{is_class_method ? ":class_methods" : ":instance_methods"}][#{name.inspect}]
|
115
|
+
|
116
|
+
# this adds support for overloading methods. Here we go through each method and call it with the arguments.
|
117
|
+
# If we get a ContractError, we move to the next function. Otherwise we return the result.
|
118
|
+
# If we run out of functions, we raise the last ContractError.
|
119
|
+
success = false
|
120
|
+
i = 0
|
121
|
+
result = nil
|
122
|
+
expected_error = methods[0].failure_exception
|
123
|
+
while !success
|
124
|
+
method = methods[i]
|
125
|
+
i += 1
|
126
|
+
begin
|
127
|
+
success = true
|
128
|
+
result = method.call_with(self, *args, &blk)
|
129
|
+
rescue expected_error => error
|
130
|
+
success = false
|
131
|
+
unless methods[i]
|
132
|
+
begin
|
133
|
+
::Contract.failure_callback(error.data, false)
|
134
|
+
rescue expected_error => final_error
|
135
|
+
raise final_error.to_contract_error
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
result
|
141
|
+
end
|
142
|
+
#{is_private ? "private #{name.inspect}" : ""}
|
143
|
+
}
|
144
|
+
|
145
|
+
class_eval method_def, __FILE__, __LINE__ + 1
|
146
|
+
end
|
147
|
+
|
148
|
+
def decorate(klass, *args)
|
149
|
+
@decorators ||= []
|
150
|
+
@decorators << [klass, args]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class Decorator
|
155
|
+
# an attr_accessor for a class variable:
|
156
|
+
class << self; attr_accessor :decorators; end
|
157
|
+
|
158
|
+
def self.inherited(klass)
|
159
|
+
name = klass.name.gsub(/^./) {|m| m.downcase}
|
160
|
+
|
161
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
162
|
+
|
163
|
+
# the file and line parameters set the text for error messages
|
164
|
+
# make a new method that is the name of your decorator.
|
165
|
+
# that method accepts random args and a block.
|
166
|
+
# inside, `decorate` is called with those params.
|
167
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
168
|
+
def #{klass}(*args, &blk)
|
169
|
+
return if ENV["NO_CONTRACTS"]
|
170
|
+
decorate(#{klass}, *args, &blk)
|
171
|
+
end
|
172
|
+
ruby_eval
|
173
|
+
end
|
174
|
+
|
175
|
+
def initialize(klass, method)
|
176
|
+
@method = method
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|