contracts 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/builtin_contracts.rb +72 -0
- data/lib/contracts.rb +105 -72
- data/lib/test.rb +1 -3
- metadata +3 -3
data/lib/builtin_contracts.rb
CHANGED
@@ -1,40 +1,73 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
This module contains all the builtin contracts.
|
3
|
+
If you want to use them, first:
|
4
|
+
|
5
|
+
import Contracts
|
6
|
+
|
7
|
+
And then use these or write your own!
|
8
|
+
|
9
|
+
A simple example:
|
10
|
+
|
11
|
+
Contract Num, Num, Num
|
12
|
+
def add(a, b)
|
13
|
+
a + b
|
14
|
+
end
|
15
|
+
|
16
|
+
The contract is <tt>Contract Num, Num, Num</tt>. That says that the +add+ function takes two numbers and returns a number.
|
17
|
+
=end
|
1
18
|
module Contracts
|
19
|
+
# Check that an argument is +Numeric+.
|
2
20
|
class Num
|
3
21
|
def self.valid? val
|
4
22
|
val.is_a? Numeric
|
5
23
|
end
|
6
24
|
end
|
7
25
|
|
26
|
+
# Check that an argument is a positive number.
|
8
27
|
class Pos
|
9
28
|
def self.valid? val
|
10
29
|
val > 0
|
11
30
|
end
|
12
31
|
end
|
13
32
|
|
33
|
+
# Check that an argument is a negative number.
|
14
34
|
class Neg
|
15
35
|
def self.valid? val
|
16
36
|
val < 0
|
17
37
|
end
|
18
38
|
end
|
19
39
|
|
40
|
+
# Passes for any argument.
|
20
41
|
class Any
|
21
42
|
def self.valid? val
|
22
43
|
true
|
23
44
|
end
|
24
45
|
end
|
25
46
|
|
47
|
+
# Fails for any argument.
|
26
48
|
class None
|
27
49
|
def self.valid? val
|
28
50
|
false
|
29
51
|
end
|
30
52
|
end
|
31
53
|
|
54
|
+
# Use this when you are writing your own contract classes.
|
55
|
+
# Allows your contract to be called with <tt>[]</tt> instead of <tt>.new</tt>:
|
56
|
+
#
|
57
|
+
# Old: <tt>Or.new(param1, param2)</tt>
|
58
|
+
#
|
59
|
+
# New: <tt>Or[param1, param2]</tt>
|
60
|
+
#
|
61
|
+
# Of course, <tt>.new</tt> still works.
|
32
62
|
class CallableClass
|
33
63
|
def self.[](*vals)
|
34
64
|
self.new(*vals)
|
35
65
|
end
|
36
66
|
end
|
37
67
|
|
68
|
+
# Takes a variable number of contracts.
|
69
|
+
# The contract passes if any of the contracts pass.
|
70
|
+
# Example: <tt>Or[Fixnum, Float]</tt>
|
38
71
|
class Or < CallableClass
|
39
72
|
def initialize(*vals)
|
40
73
|
@vals = vals
|
@@ -52,6 +85,9 @@ module Contracts
|
|
52
85
|
end
|
53
86
|
end
|
54
87
|
|
88
|
+
# Takes a variable number of contracts.
|
89
|
+
# The contract passes if exactly one of those contracts pass.
|
90
|
+
# Example: <tt>Xor[Fixnum, Float]</tt>
|
55
91
|
class Xor < CallableClass
|
56
92
|
def initialize(*vals)
|
57
93
|
@vals = vals
|
@@ -70,6 +106,9 @@ module Contracts
|
|
70
106
|
end
|
71
107
|
end
|
72
108
|
|
109
|
+
# Takes a variable number of contracts.
|
110
|
+
# The contract passes if all contracts pass.
|
111
|
+
# Example: <tt>And[Fixnum, Float]</tt>
|
73
112
|
class And < CallableClass
|
74
113
|
def initialize(*vals)
|
75
114
|
@vals = vals
|
@@ -87,6 +126,10 @@ module Contracts
|
|
87
126
|
end
|
88
127
|
end
|
89
128
|
|
129
|
+
# Takes a variable number of method names as symbols.
|
130
|
+
# The contract passes if the argument responds to all
|
131
|
+
# of those methods.
|
132
|
+
# Example: <tt>RespondsTo[:password, :credit_card]</tt>
|
90
133
|
class RespondsTo < CallableClass
|
91
134
|
def initialize(*meths)
|
92
135
|
@meths = meths
|
@@ -103,6 +146,11 @@ module Contracts
|
|
103
146
|
end
|
104
147
|
end
|
105
148
|
|
149
|
+
# Takes a variable number of method names as symbols.
|
150
|
+
# Given an argument, all of those methods are called
|
151
|
+
# on the argument one by one. If they all return true,
|
152
|
+
# the contract passes.
|
153
|
+
# Example: <tt>Send[:valid?]</tt>
|
106
154
|
class Send < CallableClass
|
107
155
|
def initialize(*meths)
|
108
156
|
@meths = meths
|
@@ -119,6 +167,8 @@ module Contracts
|
|
119
167
|
end
|
120
168
|
end
|
121
169
|
|
170
|
+
# Takes a class +A+. If argument.is_a? +A+, the contract passes.
|
171
|
+
# Example: <tt>IsA[Numeric]</tt>
|
122
172
|
class IsA < CallableClass
|
123
173
|
def initialize(cls)
|
124
174
|
@cls = cls
|
@@ -133,6 +183,9 @@ module Contracts
|
|
133
183
|
end
|
134
184
|
end
|
135
185
|
|
186
|
+
# Takes a variable number of contracts. The contract
|
187
|
+
# passes if all of those contracts fail for the given argument.
|
188
|
+
# Example: <tt>Not[nil]</tt>
|
136
189
|
class Not < CallableClass
|
137
190
|
def initialize(*vals)
|
138
191
|
@vals = vals
|
@@ -150,6 +203,10 @@ module Contracts
|
|
150
203
|
end
|
151
204
|
end
|
152
205
|
|
206
|
+
# Takes a contract. The related argument must be an array.
|
207
|
+
# Checks the contract against every element of the array.
|
208
|
+
# If it passes for all elements, the contract passes.
|
209
|
+
# Example: <tt>ArrayOf[Num]</tt>
|
153
210
|
class ArrayOf < CallableClass
|
154
211
|
def initialize(contract)
|
155
212
|
@contract = contract
|
@@ -166,4 +223,19 @@ module Contracts
|
|
166
223
|
"an array of #{@contract}"
|
167
224
|
end
|
168
225
|
end
|
226
|
+
|
227
|
+
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
228
|
+
# and uses it to validate every element passed in
|
229
|
+
# through <tt>*args</tt>.
|
230
|
+
# Example: <tt>Args[Or[String, Num]]</tt>
|
231
|
+
class Args < CallableClass
|
232
|
+
attr_reader :contract
|
233
|
+
def initialize(contract)
|
234
|
+
@contract = contract
|
235
|
+
end
|
236
|
+
|
237
|
+
def to_s
|
238
|
+
"Args[#{@contract}]"
|
239
|
+
end
|
240
|
+
end
|
169
241
|
end
|
data/lib/contracts.rb
CHANGED
@@ -5,7 +5,12 @@ class Class
|
|
5
5
|
include MethodDecorators
|
6
6
|
end
|
7
7
|
|
8
|
-
|
8
|
+
# This is the main Contract class. When you write a new contract, you'll
|
9
|
+
# write it as:
|
10
|
+
#
|
11
|
+
# Contract [contract names]
|
12
|
+
#
|
13
|
+
# This class also provides useful callbacks and a validation method.
|
9
14
|
class Contract < Decorator
|
10
15
|
attr_accessor :contracts, :klass, :method
|
11
16
|
decorator_name :contract
|
@@ -13,87 +18,59 @@ class Contract < Decorator
|
|
13
18
|
@klass, @method, @contracts = klass, method, contracts
|
14
19
|
end
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
else
|
20
|
-
[false, { :arg => arg, :contract => contract }]
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
21
|
+
# Given a hash, prints out a failure message.
|
22
|
+
# This function is used by the default #failure_callback method
|
23
|
+
# and uses the hash passed into the failure_callback method.
|
24
24
|
def self.failure_msg(data)
|
25
|
-
# TODO __file__ and __line__ won't work in Ruby 1.9.
|
26
|
-
# It provides a source_location method instead.
|
27
25
|
expected = if data[:contract].to_s == ""
|
28
26
|
data[:contract].inspect
|
29
27
|
else
|
30
28
|
data[:contract].to_s
|
31
29
|
end
|
30
|
+
|
31
|
+
if RUBY_VERSION =~ /^1\.8/
|
32
|
+
position = data[:method].__file__ + ":" + data[:method].__line__.to_s
|
33
|
+
else
|
34
|
+
file, line = data[:method].source_location
|
35
|
+
position = file + ":" + line.to_s
|
36
|
+
end
|
37
|
+
|
32
38
|
%{Contract violation:
|
33
39
|
Expected: #{expected},
|
34
40
|
Actual: #{data[:arg].inspect}
|
35
41
|
Value guarded in: #{data[:class]}::#{data[:method].name}
|
36
42
|
With Contract: #{data[:contracts].map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ") }
|
37
|
-
At: #{
|
43
|
+
At: #{position} }
|
38
44
|
end
|
45
|
+
|
46
|
+
# Callback for when a contract fails. By default it raises
|
47
|
+
# an error and prints detailed info about the contract that
|
48
|
+
# failed. You can also monkeypatch this callback to do whatever
|
49
|
+
# you want...log the error, send you an email, print an error
|
50
|
+
# message, etc.
|
51
|
+
#
|
52
|
+
# Example of monkeypatching:
|
53
|
+
#
|
54
|
+
# Contract.failure_callback(data)
|
55
|
+
# puts "You had an error!"
|
56
|
+
# puts failure_msg(data)
|
57
|
+
# exit
|
58
|
+
# end
|
39
59
|
def self.failure_callback(data)
|
40
60
|
raise failure_msg(data)
|
41
61
|
end
|
42
62
|
|
63
|
+
# Callback for when a contract succeeds. Does nothing by default.
|
43
64
|
def self.success_callback(data)
|
44
65
|
end
|
45
66
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
def self.validate_proc(arg, contract)
|
54
|
-
mkerror(contract[arg], arg, contract)
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.validate_class(arg, contract)
|
58
|
-
valid = if contract.respond_to? :valid?
|
59
|
-
contract.valid? arg
|
60
|
-
else
|
61
|
-
contract == arg.class
|
62
|
-
end
|
63
|
-
mkerror(valid, arg, contract)
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.validate_all(args, contracts, klass, method)
|
67
|
-
if args.size > contracts.size - 1
|
68
|
-
# *args
|
69
|
-
if contracts[-2].is_a? Args
|
70
|
-
while contracts.size < args.size + 1
|
71
|
-
contracts.insert(-2, contracts[-2].dup)
|
72
|
-
end
|
73
|
-
else
|
74
|
-
raise %{The number of arguments doesn't match the number of contracts.
|
75
|
-
Did you forget to write a contract for the return value of the function?
|
76
|
-
Or if you want a variable number of arguments using *args, use the Args contract.
|
77
|
-
Args: #{args.inspect}
|
78
|
-
Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ")}}
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
args.zip(contracts).each do |arg, contract|
|
83
|
-
validate(arg, contract, klass, method, contracts)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.validate(arg, contract, klass, method, contracts)
|
88
|
-
result, _ = valid?(arg, contract)
|
89
|
-
if result
|
90
|
-
success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
91
|
-
else
|
92
|
-
failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# arg to method -> contract it should satisfy -> (Boolean, metadata)
|
67
|
+
# Used to verify if an argument satisfies a contract.
|
68
|
+
#
|
69
|
+
# Takes: an argument and a contract.
|
70
|
+
#
|
71
|
+
# Returns: a tuple: [Boolean, metadata]. The boolean indicates
|
72
|
+
# whether the contract was valid or not. If it wasn't, metadata
|
73
|
+
# contains some useful information about the failure.
|
97
74
|
def self.valid?(arg, contract)
|
98
75
|
case contract
|
99
76
|
when Class
|
@@ -111,7 +88,7 @@ Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join("
|
|
111
88
|
# e.g. { :a => Num, :b => String }
|
112
89
|
return mkerror(false, arg, contract) unless arg.is_a?(Hash)
|
113
90
|
validate_hash(arg, contract)
|
114
|
-
when Args
|
91
|
+
when Contracts::Args
|
115
92
|
valid? arg, contract.contract
|
116
93
|
else
|
117
94
|
if contract.respond_to? :valid?
|
@@ -123,22 +100,78 @@ Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join("
|
|
123
100
|
end
|
124
101
|
|
125
102
|
def call(this, *args, &blk)
|
126
|
-
|
103
|
+
_args = blk ? args + [blk] : args
|
104
|
+
if _args.size != @contracts.size - 1
|
105
|
+
# so it's not *args
|
106
|
+
if !@contracts[-2].is_a? Contracts::Args
|
107
|
+
raise %{The number of arguments doesn't match the number of contracts.
|
108
|
+
Did you forget to write a contract for the return value of the function?
|
109
|
+
Or if you want a variable number of arguments using *args, use the Args contract.
|
110
|
+
Args: #{args.inspect}
|
111
|
+
Contracts: #{@contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ")}}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
Contract.validate_all(_args, @contracts[0, @contracts.size - 1], @klass, @method)
|
115
|
+
|
127
116
|
result = @method.bind(this).call(*args, &blk)
|
117
|
+
|
128
118
|
if args.size == @contracts.size - 1
|
129
119
|
Contract.validate(result, @contracts[-1], @klass, @method, @contracts)
|
130
120
|
end
|
131
121
|
result
|
132
122
|
end
|
133
|
-
end
|
134
123
|
|
135
|
-
|
136
|
-
|
137
|
-
def
|
138
|
-
|
124
|
+
private
|
125
|
+
|
126
|
+
def self.mkerror(validates, arg, contract)
|
127
|
+
if validates
|
128
|
+
[true, {}]
|
129
|
+
else
|
130
|
+
[false, { :arg => arg, :contract => contract }]
|
131
|
+
end
|
139
132
|
end
|
140
133
|
|
141
|
-
def
|
142
|
-
|
134
|
+
def self.validate_hash(arg, contract)
|
135
|
+
arg.keys.each do |k|
|
136
|
+
result, info = validate(arg[k], contract[k])
|
137
|
+
return [result, info] unless result
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.validate_proc(arg, contract)
|
142
|
+
mkerror(contract[arg], arg, contract)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.validate_class(arg, contract)
|
146
|
+
valid = if contract.respond_to? :valid?
|
147
|
+
contract.valid? arg
|
148
|
+
else
|
149
|
+
contract == arg.class
|
150
|
+
end
|
151
|
+
mkerror(valid, arg, contract)
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.validate_all(args, contracts, klass, method)
|
155
|
+
# we assume that any mismatch in # of args/contracts
|
156
|
+
# has been checked befoer this point.
|
157
|
+
if args.size != contracts.size
|
158
|
+
# assumed: contracts[-1].is_a? Args
|
159
|
+
while contracts.size < args.size
|
160
|
+
contracts << contracts[-1].dup
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
args.zip(contracts).each do |arg, contract|
|
165
|
+
validate(arg, contract, klass, method, contracts)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.validate(arg, contract, klass, method, contracts)
|
170
|
+
result, _ = valid?(arg, contract)
|
171
|
+
if result
|
172
|
+
success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
173
|
+
else
|
174
|
+
failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
175
|
+
end
|
143
176
|
end
|
144
177
|
end
|
data/lib/test.rb
CHANGED
@@ -24,8 +24,6 @@ class Object
|
|
24
24
|
func.call
|
25
25
|
end
|
26
26
|
|
27
|
-
# thinks there are too many args, throws error
|
28
|
-
# sidenote: there should be a check to make sure the # of args and contracts match up.
|
29
27
|
Contract Args[Num], Num
|
30
28
|
def sum(*vals)
|
31
29
|
vals.inject(0) do |acc, v|
|
@@ -38,4 +36,4 @@ run {
|
|
38
36
|
puts "hi!"
|
39
37
|
}
|
40
38
|
|
41
|
-
puts
|
39
|
+
puts sum(1, 2, 3, 4)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 3
|
10
|
+
version: 0.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Aditya Bhargava
|