contracts 0.0.2 → 0.0.3
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.
- 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
|