contracts 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/contracts.rb +88 -107
- data/lib/decorators.rb +15 -18
- data/lib/decorators_aliased.rb +102 -0
- data/lib/decorators_old.rb +97 -0
- metadata +3 -1
data/lib/contracts.rb
CHANGED
@@ -19,10 +19,10 @@ module Contracts
|
|
19
19
|
|
20
20
|
def functype(funcname)
|
21
21
|
contracts = self.class.decorated_methods[funcname]
|
22
|
-
if contracts.nil?
|
22
|
+
if contracts.nil?
|
23
23
|
"No contract for #{self.class}.#{funcname}"
|
24
24
|
else
|
25
|
-
"#{funcname} :: #{contracts
|
25
|
+
"#{funcname} :: #{contracts}"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -36,20 +36,34 @@ end
|
|
36
36
|
#
|
37
37
|
# This class also provides useful callbacks and a validation method.
|
38
38
|
class Contract < Decorator
|
39
|
-
|
39
|
+
attr_reader :args_contracts, :ret_contract, :klass, :method
|
40
40
|
# decorator_name :contract
|
41
41
|
def initialize(klass, method, *contracts)
|
42
42
|
if contracts[-1].is_a? Hash
|
43
43
|
# internally we just convert that return value syntax back to an array
|
44
|
-
|
44
|
+
@args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
|
45
|
+
@ret_contract = contracts[-1].values[0]
|
46
|
+
@args_validators = @args_contracts.map do |contract|
|
47
|
+
Contract.make_validator(contract)
|
48
|
+
end
|
49
|
+
@ret_validator = Contract.make_validator(@ret_contract)
|
45
50
|
else
|
46
51
|
fail "It looks like your contract for #{method} doesn't have a return value. A contract should be written as `Contract arg1, arg2 => return_value`."
|
47
52
|
end
|
48
|
-
@klass, @method
|
53
|
+
@klass, @method= klass, method
|
54
|
+
@has_func_contracts = args_contracts.index do |contract|
|
55
|
+
contract.is_a? Contracts::Func
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def pretty_contract c
|
60
|
+
c.is_a?(Class) ? c.name : c.class.name
|
49
61
|
end
|
50
62
|
|
51
63
|
def to_s
|
52
|
-
|
64
|
+
args = @args_contracts.map { |c| pretty_contract(c) }.join(", ")
|
65
|
+
ret = pretty_contract(@ret_contract)
|
66
|
+
("#{args} => #{ret}").gsub("Contracts::", "")
|
53
67
|
end
|
54
68
|
|
55
69
|
# Given a hash, prints out a failure message.
|
@@ -73,7 +87,7 @@ class Contract < Decorator
|
|
73
87
|
Expected: #{expected},
|
74
88
|
Actual: #{data[:arg].inspect}
|
75
89
|
Value guarded in: #{data[:class]}::#{method_name}
|
76
|
-
With Contract: #{data[:contracts]
|
90
|
+
With Contract: #{data[:contracts]}
|
77
91
|
At: #{position} }
|
78
92
|
end
|
79
93
|
|
@@ -94,10 +108,6 @@ class Contract < Decorator
|
|
94
108
|
raise failure_msg(data)
|
95
109
|
end
|
96
110
|
|
97
|
-
# Callback for when a contract succeeds. Does nothing by default.
|
98
|
-
def self.success_callback(data)
|
99
|
-
end
|
100
|
-
|
101
111
|
# Used to verify if an argument satisfies a contract.
|
102
112
|
#
|
103
113
|
# Takes: an argument and a contract.
|
@@ -106,37 +116,56 @@ class Contract < Decorator
|
|
106
116
|
# whether the contract was valid or not. If it wasn't, metadata
|
107
117
|
# contains some useful information about the failure.
|
108
118
|
def self.valid?(arg, contract)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
119
|
+
make_validator(contract)[arg]
|
120
|
+
end
|
121
|
+
|
122
|
+
# This is a little weird. For each contract
|
123
|
+
# we pre-make a proc to validate it so we
|
124
|
+
# don't have to go through this decision tree every time.
|
125
|
+
# Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
|
126
|
+
def self.make_validator(contract)
|
127
|
+
# if is faster than case!
|
128
|
+
klass = contract.class
|
129
|
+
if klass == Proc
|
114
130
|
# e.g. lambda {true}
|
115
|
-
|
116
|
-
|
131
|
+
contract
|
132
|
+
elsif klass == Array
|
117
133
|
# e.g. [Num, String]
|
118
134
|
# TODO account for these errors too
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
135
|
+
lambda { |arg|
|
136
|
+
return false unless arg.is_a?(Array)
|
137
|
+
arg.zip(contract).all? do |_arg, _contract|
|
138
|
+
Contract.valid?(_arg, _contract)
|
139
|
+
end
|
140
|
+
}
|
141
|
+
elsif klass == Hash
|
125
142
|
# e.g. { :a => Num, :b => String }
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
143
|
+
lambda { |arg|
|
144
|
+
return false unless arg.is_a?(Hash)
|
145
|
+
contract.keys.all? do |k|
|
146
|
+
Contract.valid?(arg[k], contract[k])
|
147
|
+
end
|
148
|
+
}
|
149
|
+
elsif klass == Contracts::Args
|
150
|
+
lambda { |arg|
|
151
|
+
Contract.valid?(arg, contract.contract)
|
152
|
+
}
|
153
|
+
elsif klass == Contracts::Func
|
154
|
+
lambda { |arg|
|
155
|
+
arg.is_a?(Method) || arg.is_a?(Proc)
|
156
|
+
}
|
132
157
|
else
|
158
|
+
# classes and everything else
|
159
|
+
# e.g. Fixnum, Num
|
133
160
|
if contract.respond_to? :valid?
|
134
|
-
|
161
|
+
lambda { |arg| contract.valid?(arg) }
|
162
|
+
elsif klass == Class
|
163
|
+
lambda { |arg| contract == arg.class }
|
135
164
|
else
|
136
|
-
|
165
|
+
lambda { |arg| contract == arg }
|
137
166
|
end
|
138
167
|
end
|
139
|
-
end
|
168
|
+
end
|
140
169
|
|
141
170
|
def [](*args, &blk)
|
142
171
|
call(*args, &blk)
|
@@ -148,88 +177,40 @@ class Contract < Decorator
|
|
148
177
|
|
149
178
|
def call_with(this, *args, &blk)
|
150
179
|
_args = blk ? args + [blk] : args
|
151
|
-
res = Contract.validate_all(_args, @contracts[0, @contracts.size - 1], @klass, @method)
|
152
|
-
return if res == false
|
153
180
|
|
154
|
-
# contracts on
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
181
|
+
# check contracts on arguments
|
182
|
+
# fun fact! This is significantly faster than .zip (3.7 secs vs 4.7 secs). Why??
|
183
|
+
last_index = @args_validators.size - 1
|
184
|
+
# times is faster than (0..args.size).each
|
185
|
+
_args.size.times do |i|
|
186
|
+
# this is done to account for extra args (for *args)
|
187
|
+
j = i < last_index ? i : last_index
|
188
|
+
#unless true #@args_contracts[i].valid?(args[i])
|
189
|
+
unless @args_validators[j][_args[i]]
|
190
|
+
call_function = Contract.failure_callback({:arg => _args[i], :contract => @args_contracts[j], :class => @klass, :method => @method, :contracts => self})
|
191
|
+
return unless call_function
|
159
192
|
end
|
160
|
-
end
|
161
|
-
|
162
|
-
if @method.respond_to? :bind
|
163
|
-
# instance method
|
164
|
-
result = @method.bind(this).call(*args, &blk)
|
165
|
-
else
|
166
|
-
# class method
|
167
|
-
result = @method.call(*args, &blk)
|
168
|
-
end
|
169
|
-
|
170
|
-
Contract.validate(result, @contracts[-1], @klass, @method, @contracts)
|
171
|
-
result
|
172
|
-
end
|
173
|
-
|
174
|
-
private
|
175
|
-
|
176
|
-
def self.mkerror(validates, arg, contract)
|
177
|
-
if validates
|
178
|
-
[true, {}]
|
179
|
-
else
|
180
|
-
[false, { :arg => arg, :contract => contract }]
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def self.validate_hash(arg, contract)
|
185
|
-
contract.keys.each do |k|
|
186
|
-
result, info = valid?(arg[k], contract[k])
|
187
|
-
return [result, info] unless result
|
188
193
|
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def self.validate_proc(arg, contract)
|
192
|
-
mkerror(contract[arg], arg, contract)
|
193
|
-
end
|
194
194
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
mkerror(valid, arg, contract)
|
202
|
-
end
|
203
|
-
|
204
|
-
def self.validate_all(params, contracts, klass, method)
|
205
|
-
# we assume that any mismatch in # of params/contracts
|
206
|
-
# has been checked befoer this point.
|
207
|
-
args_index = contracts.index do |contract|
|
208
|
-
contract.is_a? Contracts::Args
|
209
|
-
end
|
210
|
-
if args_index
|
211
|
-
# there is a *args at this index.
|
212
|
-
# Now we need to see how many arguments this contract
|
213
|
-
# accounts for and just duplicate the contract for all
|
214
|
-
# of those args.
|
215
|
-
args_contract = contracts[args_index]
|
216
|
-
while contracts.size < params.size
|
217
|
-
contracts.insert(args_index, args_contract.dup)
|
195
|
+
if @has_func_contracts
|
196
|
+
# contracts on methods
|
197
|
+
contracts.each_with_index do |contract, i|
|
198
|
+
if contract.is_a? Contracts::Func
|
199
|
+
args[i] = Contract.new(@klass, args[i], *contract.contracts)
|
200
|
+
end
|
218
201
|
end
|
219
202
|
end
|
220
203
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
def self.validate(arg, contract, klass, method, contracts)
|
228
|
-
result, _ = valid?(arg, contract)
|
229
|
-
if result
|
230
|
-
success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
204
|
+
result = if @method.respond_to? :bind
|
205
|
+
# instance method
|
206
|
+
@method.bind(this).call(*args, &blk)
|
231
207
|
else
|
232
|
-
|
208
|
+
# class method
|
209
|
+
@method.call(*args, &blk)
|
233
210
|
end
|
211
|
+
unless @ret_validator[result]
|
212
|
+
Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self})
|
213
|
+
end
|
214
|
+
result
|
234
215
|
end
|
235
216
|
end
|
data/lib/decorators.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
module MethodDecorators
|
2
2
|
def self.extended(klass)
|
3
|
-
|
4
|
-
|
3
|
+
klass.class_eval do
|
4
|
+
@@__decorated_methods ||= {}
|
5
|
+
def self.__decorated_methods
|
6
|
+
@@__decorated_methods
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.__decorated_methods_set(k, v)
|
10
|
+
@@__decorated_methods[k] = v
|
11
|
+
end
|
5
12
|
end
|
6
13
|
end
|
7
14
|
|
@@ -15,6 +22,7 @@ module MethodDecorators
|
|
15
22
|
super
|
16
23
|
end
|
17
24
|
|
25
|
+
# For Ruby 1.9
|
18
26
|
def singleton_method_added name
|
19
27
|
common_method_added name, true
|
20
28
|
super
|
@@ -25,10 +33,6 @@ module MethodDecorators
|
|
25
33
|
|
26
34
|
decorators = @decorators.dup
|
27
35
|
@decorators = nil
|
28
|
-
@decorated_methods ||= Hash.new {|h,k| h[k] = []}
|
29
|
-
|
30
|
-
# attr_accessor on the class variable decorated_methods
|
31
|
-
class << self; attr_accessor :decorated_methods; end
|
32
36
|
|
33
37
|
decorators.each do |klass, args|
|
34
38
|
# a reference to the method gets passed into the contract here. This is good because
|
@@ -43,26 +47,19 @@ module MethodDecorators
|
|
43
47
|
else
|
44
48
|
decorator = klass
|
45
49
|
end
|
46
|
-
|
50
|
+
__decorated_methods_set(name, decorator)
|
47
51
|
end
|
48
52
|
|
49
53
|
# in place of this method, we are going to define our own method. This method
|
50
54
|
# just calls the decorator passing in all args that were to be passed into the method.
|
51
55
|
# The decorator in turn has a reference to the actual method, so it can call it
|
52
56
|
# on its own, after doing it's decorating of course.
|
53
|
-
class_eval
|
57
|
+
class_eval %{
|
54
58
|
def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
|
55
|
-
ret = nil
|
56
59
|
this = self#{is_class_method ? "" : ".class"}
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
this.decorated_methods[#{name.inspect}].each do |decorator|
|
61
|
-
ret = decorator.call_with(self, *args, &blk)
|
62
|
-
end
|
63
|
-
ret
|
64
|
-
end
|
65
|
-
ruby_eval
|
60
|
+
return this.__decorated_methods[#{name.inspect}].call_with(self, *args, &blk)
|
61
|
+
end
|
62
|
+
}, __FILE__, __LINE__ + 1
|
66
63
|
end
|
67
64
|
|
68
65
|
def decorate(klass, *args)
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module MethodDecorators
|
2
|
+
def self.extended(klass)
|
3
|
+
klass.class_eval do
|
4
|
+
@@__decorated_methods ||= {}
|
5
|
+
def self.__decorated_methods
|
6
|
+
@@__decorated_methods
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.__decorated_methods_set(k, v)
|
10
|
+
@@__decorated_methods[k] = v
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# first, when you write a contract, the decorate method gets called which
|
16
|
+
# sets the @decorators variable. Then when the next method after the contract
|
17
|
+
# is defined, method_added is called and we look at the @decorators variable
|
18
|
+
# to find the decorator for that method. This is how we associate decorators
|
19
|
+
# with methods.
|
20
|
+
def method_added(name)
|
21
|
+
common_method_added name, false
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
# For Ruby 1.9
|
26
|
+
def singleton_method_added name
|
27
|
+
common_method_added name, true
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def common_method_added name, is_class_method
|
32
|
+
return unless @decorators
|
33
|
+
|
34
|
+
decorators = @decorators.dup
|
35
|
+
@decorators = nil
|
36
|
+
|
37
|
+
decorators.each do |klass, args|
|
38
|
+
# a reference to the method gets passed into the contract here. This is good because
|
39
|
+
# we are going to redefine this method with a new name below...so this reference is
|
40
|
+
# now the *only* reference to the old method that exists.
|
41
|
+
if klass.respond_to? :new
|
42
|
+
if is_class_method
|
43
|
+
decorator = klass.new(*args)
|
44
|
+
else
|
45
|
+
decorator = klass.new(*args)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
decorator = klass
|
49
|
+
end
|
50
|
+
__decorated_methods_set(name, decorator)
|
51
|
+
end
|
52
|
+
|
53
|
+
# in place of this method, we are going to define our own method. This method
|
54
|
+
# just calls the decorator passing in all args that were to be passed into the method.
|
55
|
+
# The decorator in turn has a reference to the actual method, so it can call it
|
56
|
+
# on its own, after doing it's decorating of course.
|
57
|
+
alias_method :"original_#{name}", name
|
58
|
+
class_eval %{
|
59
|
+
def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
|
60
|
+
this = self#{is_class_method ? "" : ".class"}
|
61
|
+
#this.__decorated_methods[#{name.inspect}].check_args(*args, &blk)
|
62
|
+
raise unless args[0].is_a?(Numeric)
|
63
|
+
raise unless args[1].is_a?(Numeric)
|
64
|
+
|
65
|
+
res = self.send :"original_#{name}", *args, &blk
|
66
|
+
raise unless res.is_a?(Numeric)
|
67
|
+
#this.__decorated_methods[#{name.inspect}].check_res(res)
|
68
|
+
res
|
69
|
+
end
|
70
|
+
}, __FILE__, __LINE__ + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def decorate(klass, *args)
|
74
|
+
@decorators ||= []
|
75
|
+
@decorators << [klass, args]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Decorator
|
80
|
+
# an attr_accessor for a class variable:
|
81
|
+
class << self; attr_accessor :decorators; end
|
82
|
+
|
83
|
+
def self.inherited(klass)
|
84
|
+
name = klass.name.gsub(/^./) {|m| m.downcase}
|
85
|
+
|
86
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
87
|
+
|
88
|
+
# the file and line parameters set the text for error messages
|
89
|
+
# make a new method that is the name of your decorator.
|
90
|
+
# that method accepts random args and a block.
|
91
|
+
# inside, `decorate` is called with those params.
|
92
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
93
|
+
def #{klass}(*args, &blk)
|
94
|
+
decorate(#{klass}, *args, &blk)
|
95
|
+
end
|
96
|
+
ruby_eval
|
97
|
+
end
|
98
|
+
|
99
|
+
def initialize(klass, method)
|
100
|
+
@method = method
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module MethodDecorators
|
2
|
+
def self.extended(klass)
|
3
|
+
class << klass
|
4
|
+
attr_accessor :decorated_methods
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
# first, when you write a contract, the decorate method gets called which
|
9
|
+
# sets the @decorators variable. Then when the next method after the contract
|
10
|
+
# is defined, method_added is called and we look at the @decorators variable
|
11
|
+
# to find the decorator for that method. This is how we associate decorators
|
12
|
+
# with methods.
|
13
|
+
def method_added(name)
|
14
|
+
common_method_added name, false
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def singleton_method_added name
|
19
|
+
common_method_added name, true
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def common_method_added name, is_class_method
|
24
|
+
return unless @decorators
|
25
|
+
|
26
|
+
decorators = @decorators.dup
|
27
|
+
@decorators = nil
|
28
|
+
@decorated_methods ||= Hash.new {|h,k| h[k] = []}
|
29
|
+
|
30
|
+
# attr_accessor on the class variable decorated_methods
|
31
|
+
class << self; attr_accessor :decorated_methods; end
|
32
|
+
|
33
|
+
decorators.each do |klass, args|
|
34
|
+
# a reference to the method gets passed into the contract here. This is good because
|
35
|
+
# we are going to redefine this method with a new name below...so this reference is
|
36
|
+
# now the *only* reference to the old method that exists.
|
37
|
+
if klass.respond_to? :new
|
38
|
+
if is_class_method
|
39
|
+
decorator = klass.new(self, method(name), *args)
|
40
|
+
else
|
41
|
+
decorator = klass.new(self, instance_method(name), *args)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
decorator = klass
|
45
|
+
end
|
46
|
+
@decorated_methods[name] << decorator
|
47
|
+
end
|
48
|
+
|
49
|
+
# in place of this method, we are going to define our own method. This method
|
50
|
+
# just calls the decorator passing in all args that were to be passed into the method.
|
51
|
+
# The decorator in turn has a reference to the actual method, so it can call it
|
52
|
+
# on its own, after doing it's decorating of course.
|
53
|
+
class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
54
|
+
def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
|
55
|
+
ret = nil
|
56
|
+
this = self#{is_class_method ? "" : ".class"}
|
57
|
+
unless this.respond_to?(:decorated_methods) && !this.decorated_methods.nil?
|
58
|
+
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."
|
59
|
+
end
|
60
|
+
this.decorated_methods[#{name.inspect}].each do |decorator|
|
61
|
+
ret = decorator.call_with(self, *args, &blk)
|
62
|
+
end
|
63
|
+
ret
|
64
|
+
end
|
65
|
+
ruby_eval
|
66
|
+
end
|
67
|
+
|
68
|
+
def decorate(klass, *args)
|
69
|
+
@decorators ||= []
|
70
|
+
@decorators << [klass, args]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Decorator
|
75
|
+
# an attr_accessor for a class variable:
|
76
|
+
class << self; attr_accessor :decorators; end
|
77
|
+
|
78
|
+
def self.inherited(klass)
|
79
|
+
name = klass.name.gsub(/^./) {|m| m.downcase}
|
80
|
+
|
81
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
82
|
+
|
83
|
+
# the file and line parameters set the text for error messages
|
84
|
+
# make a new method that is the name of your decorator.
|
85
|
+
# that method accepts random args and a block.
|
86
|
+
# inside, `decorate` is called with those params.
|
87
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
88
|
+
def #{klass}(*args, &blk)
|
89
|
+
decorate(#{klass}, *args, &blk)
|
90
|
+
end
|
91
|
+
ruby_eval
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize(klass, method)
|
95
|
+
@method = method
|
96
|
+
end
|
97
|
+
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.1.
|
5
|
+
version: 0.1.4
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Aditya Bhargava
|
@@ -25,6 +25,8 @@ files:
|
|
25
25
|
- lib/builtin_contracts.rb
|
26
26
|
- lib/contracts.rb
|
27
27
|
- lib/decorators.rb
|
28
|
+
- lib/decorators_aliased.rb
|
29
|
+
- lib/decorators_old.rb
|
28
30
|
- lib/testable.rb
|
29
31
|
homepage: http://github.com/egonSchiele/contracts.ruby
|
30
32
|
licenses: []
|