contracts 0.8 → 0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +28 -0
- data/Gemfile +1 -1
- data/TUTORIAL.md +2 -1
- data/lib/contracts.rb +26 -13
- data/lib/contracts/builtin_contracts.rb +52 -131
- data/lib/contracts/decorators.rb +35 -2
- data/lib/contracts/errors.rb +6 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +29 -0
- data/spec/contracts_spec.rb +49 -4
- data/spec/fixtures/fixtures.rb +34 -5
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +6 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +18 -0
- metadata +3 -4
- data/Gemfile.lock +0 -49
- data/lib/contracts/testable.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c872d0fc055f6b1ace99f6793c436ee222111992
|
4
|
+
data.tar.gz: 0bc9d2a28a4dd77395932974fe46e9175818fa73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c2eb482b3c7b2e697ff29076734fa9ec74dc93b9f8344514fdfab3a999a1b40dff0d57bbf487dcd82eaf51b7b503c55cc55a6573745b36314f09f3ce2f5778c
|
7
|
+
data.tar.gz: 3535f6d2ceea6e1382b9810679fcb619b94ed9788434022b597f120165a534383c306e6d5ce305ac0808a1b0d34dd0f1b1ca1ec53e3e61f3481a860197e8be5a
|
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
## v0.9
|
2
|
+
- MAJOR fix in pattern-matching: If the return contract for a pattern-matched function fails, it should NOT try the next pattern-match function. Pattern-matching is only for params, not return values.
|
3
|
+
- raise an error if multiple defns have the same contract for pattern matching.
|
4
|
+
|
5
|
+
- New syntax for functions with no input params.
|
6
|
+
Old way:
|
7
|
+
Contract nil => 1
|
8
|
+
def one
|
9
|
+
|
10
|
+
New way:
|
11
|
+
Contract 1
|
12
|
+
def one
|
13
|
+
|
14
|
+
- Prettier HashOf contract can now be written like this: `HashOf[Num => String]`
|
15
|
+
- Add `SetOf` contract
|
16
|
+
- various small fixes
|
17
|
+
|
18
|
+
## v0.8
|
19
|
+
- code refactored (very slight loss of performance, big increase in readability)
|
20
|
+
- fail when defining a contract on a module without `include Contracts::Modules`
|
21
|
+
- fixed several bugs in argument parsing, functions with complex params get contracts applied correctly now.
|
22
|
+
- added rubocop to ci.
|
23
|
+
- if a contract is set on a protected method, it should not become public.
|
24
|
+
- fixed pattern matching when the multiple definitions of functions have different arities.
|
25
|
+
- couple of new built-in contracts: Nat, Eq.
|
26
|
+
- changed `Invariant` to `invariant`: `invariant(:day) { 1 <= day && day <= 31 }`
|
27
|
+
- prettier error messages (`Contracts::Num` is now just `Num`, for example)
|
28
|
+
- support for yard-contracts
|
data/Gemfile
CHANGED
data/TUTORIAL.md
CHANGED
@@ -79,7 +79,8 @@ contracts.ruby comes with a lot of built-in contracts, including the following:
|
|
79
79
|
* [`And`](http://www.rubydoc.info/gems/contracts/Contracts/And) – passes if all contracts pass, e.g. `And[Fixnum, Float]`
|
80
80
|
* [`Not`](http://www.rubydoc.info/gems/contracts/Contracts/Not) – passes if all contracts fail for the given argument, e.g. `Not[nil]`
|
81
81
|
* [`ArrayOf`](http://www.rubydoc.info/gems/contracts/Contracts/ArrayOf) – checks that the argument is an array, and all elements pass the given contract, e.g. `ArrayOf[Num]`
|
82
|
-
* [`
|
82
|
+
* [`SetOf`](http://www.rubydoc.info/gems/contracts/Contracts/SetOf) – checks that the argument is a set, and all elements pass the given contract, e.g. `SetOf[Num]`
|
83
|
+
* [`HashOf`](http://www.rubydoc.info/gems/contracts/Contracts/HashOf) – checks that the argument is a hash, and all keys and values pass the given contract, e.g. `HashOf[Symbol => String]`
|
83
84
|
* [`Maybe`](http://www.rubydoc.info/gems/contracts/Contracts/Maybe) – passes if the argument is `nil`, or if the given contract passes
|
84
85
|
* [`RespondTo`](http://www.rubydoc.info/gems/contracts/Contracts/RespondTo) – checks that the argument responds to all of the given methods, e.g. `RespondTo[:password, :credit_card]`
|
85
86
|
* [`Send`](http://www.rubydoc.info/gems/contracts/Contracts/Send) – checks that all named methods return true, e.g. `Send[:valid?]`
|
data/lib/contracts.rb
CHANGED
@@ -73,22 +73,33 @@ class Contract < Contracts::Decorator
|
|
73
73
|
# to monkey patch #failure_callback only temporary and then switch it back.
|
74
74
|
# First important usage - for specs.
|
75
75
|
DEFAULT_FAILURE_CALLBACK = proc do |data|
|
76
|
-
|
76
|
+
if data[:return_value]
|
77
|
+
# this failed on the return contract
|
78
|
+
fail ReturnContractError.new(failure_msg(data), data)
|
79
|
+
else
|
80
|
+
# this failed for a param contract
|
81
|
+
fail data[:contracts].failure_exception.new(failure_msg(data), data)
|
82
|
+
end
|
77
83
|
end
|
78
84
|
|
79
85
|
attr_reader :args_contracts, :ret_contract, :klass, :method
|
80
86
|
def initialize(klass, method, *contracts)
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
unless contracts.last.is_a?(Hash)
|
88
|
+
unless contracts.one?
|
89
|
+
fail %{
|
90
|
+
It looks like your contract for #{method.name} doesn't have a return
|
91
|
+
value. A contract should be written as `Contract arg1, arg2 =>
|
92
|
+
return_value`.
|
93
|
+
}.strip
|
94
|
+
end
|
95
|
+
contracts = [nil => contracts[-1]]
|
90
96
|
end
|
91
97
|
|
98
|
+
# internally we just convert that return value syntax back to an array
|
99
|
+
@args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
|
100
|
+
|
101
|
+
@ret_contract = contracts[-1].values[0]
|
102
|
+
|
92
103
|
@args_validators = args_contracts.map do |contract|
|
93
104
|
Contract.make_validator(contract)
|
94
105
|
end
|
@@ -307,7 +318,8 @@ class Contract < Contracts::Decorator
|
|
307
318
|
:method => method,
|
308
319
|
:contracts => self,
|
309
320
|
:arg_pos => i+1,
|
310
|
-
:total_args => args.size
|
321
|
+
:total_args => args.size,
|
322
|
+
:return_value => false)
|
311
323
|
end
|
312
324
|
|
313
325
|
if contract.is_a?(Contracts::Func)
|
@@ -339,7 +351,8 @@ class Contract < Contracts::Decorator
|
|
339
351
|
:method => method,
|
340
352
|
:contracts => self,
|
341
353
|
:arg_pos => i-1,
|
342
|
-
:total_args => args.size
|
354
|
+
:total_args => args.size,
|
355
|
+
:return_value => false)
|
343
356
|
end
|
344
357
|
|
345
358
|
if contract.is_a?(Contracts::Func)
|
@@ -377,7 +390,7 @@ class Contract < Contracts::Decorator
|
|
377
390
|
if @pattern_match
|
378
391
|
PatternMatchingError
|
379
392
|
else
|
380
|
-
|
393
|
+
ParamContractError
|
381
394
|
end
|
382
395
|
end
|
383
396
|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require "contracts/testable"
|
2
1
|
require "contracts/formatters"
|
2
|
+
require "set"
|
3
3
|
|
4
4
|
# rdoc
|
5
5
|
# This module contains all the builtin contracts.
|
@@ -24,14 +24,6 @@ module Contracts
|
|
24
24
|
def self.valid? val
|
25
25
|
val.is_a? Numeric
|
26
26
|
end
|
27
|
-
|
28
|
-
def self.testable?
|
29
|
-
true
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.test_data
|
33
|
-
[-1, 0, 1, 1.5, 50_000]
|
34
|
-
end
|
35
27
|
end
|
36
28
|
|
37
29
|
# Check that an argument is a positive number.
|
@@ -39,14 +31,6 @@ module Contracts
|
|
39
31
|
def self.valid? val
|
40
32
|
val > 0
|
41
33
|
end
|
42
|
-
|
43
|
-
def testable?
|
44
|
-
true
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.test_data
|
48
|
-
(0..5).map { rand(999) + 1 }
|
49
|
-
end
|
50
34
|
end
|
51
35
|
|
52
36
|
# Check that an argument is a negative number.
|
@@ -54,28 +38,12 @@ module Contracts
|
|
54
38
|
def self.valid? val
|
55
39
|
val < 0
|
56
40
|
end
|
57
|
-
|
58
|
-
def testable?
|
59
|
-
true
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.test_data
|
63
|
-
(0..5).map { (rand(999) + 1) * -1 }
|
64
|
-
end
|
65
41
|
end
|
66
42
|
|
67
43
|
# Check that an argument is a natural number.
|
68
44
|
class Nat
|
69
45
|
def self.valid? val
|
70
|
-
val >= 0 && val.integer?
|
71
|
-
end
|
72
|
-
|
73
|
-
def testable?
|
74
|
-
true
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.test_data
|
78
|
-
(0..5).map { |n| n * rand(999) }
|
46
|
+
val && val >= 0 && val.integer?
|
79
47
|
end
|
80
48
|
end
|
81
49
|
|
@@ -128,19 +96,6 @@ module Contracts
|
|
128
96
|
InspectWrapper.create(x)
|
129
97
|
end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s
|
130
98
|
end
|
131
|
-
|
132
|
-
# this can only be tested IF all the sub-contracts have a test_data method
|
133
|
-
def testable?
|
134
|
-
@vals.all? do |val|
|
135
|
-
Testable.testable?(val)
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def test_data
|
140
|
-
@vals.map do |val|
|
141
|
-
Testable.test_data(val)
|
142
|
-
end.flatten
|
143
|
-
end
|
144
99
|
end
|
145
100
|
|
146
101
|
# Takes a variable number of contracts.
|
@@ -164,18 +119,6 @@ module Contracts
|
|
164
119
|
InspectWrapper.create(x)
|
165
120
|
end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s
|
166
121
|
end
|
167
|
-
|
168
|
-
def testable?
|
169
|
-
@vals.all? do |val|
|
170
|
-
Testable.testable? val
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def test_data
|
175
|
-
@vals.map do |val|
|
176
|
-
Testable.test_data val
|
177
|
-
end.flatten
|
178
|
-
end
|
179
122
|
end
|
180
123
|
|
181
124
|
# Takes a variable number of contracts.
|
@@ -295,17 +238,20 @@ module Contracts
|
|
295
238
|
end
|
296
239
|
end
|
297
240
|
|
298
|
-
#
|
299
|
-
#
|
241
|
+
# @private
|
242
|
+
# Takes a collection(responds to :each) type and a contract.
|
243
|
+
# The related argument must be of specified collection type.
|
244
|
+
# Checks the contract against every element of the collection.
|
300
245
|
# If it passes for all elements, the contract passes.
|
301
|
-
# Example: <tt>
|
302
|
-
class
|
303
|
-
def initialize(contract)
|
246
|
+
# Example: <tt>CollectionOf[Array, Num]</tt>
|
247
|
+
class CollectionOf < CallableClass
|
248
|
+
def initialize(collection_class, contract)
|
249
|
+
@collection_class = collection_class
|
304
250
|
@contract = contract
|
305
251
|
end
|
306
252
|
|
307
253
|
def valid?(vals)
|
308
|
-
return false unless vals.is_a?(
|
254
|
+
return false unless vals.is_a?(@collection_class)
|
309
255
|
vals.all? do |val|
|
310
256
|
res, _ = Contract.valid?(val, @contract)
|
311
257
|
res
|
@@ -313,22 +259,36 @@ module Contracts
|
|
313
259
|
end
|
314
260
|
|
315
261
|
def to_s
|
316
|
-
"
|
262
|
+
"a collection #{@collection_class} of #{@contract}"
|
317
263
|
end
|
318
264
|
|
319
|
-
|
320
|
-
|
321
|
-
|
265
|
+
class Factory
|
266
|
+
def initialize(collection_class, &before_new)
|
267
|
+
@collection_class = collection_class
|
268
|
+
@before_new = before_new
|
269
|
+
end
|
270
|
+
|
271
|
+
def new(contract)
|
272
|
+
@before_new && @before_new.call
|
273
|
+
CollectionOf.new(@collection_class, contract)
|
274
|
+
end
|
322
275
|
|
323
|
-
|
324
|
-
[
|
325
|
-
[],
|
326
|
-
[Testable.test_data(@contract)],
|
327
|
-
[Testable.test_data(@contract), Testable.test_data(@contract)]
|
328
|
-
]
|
276
|
+
alias_method :[], :new
|
329
277
|
end
|
330
278
|
end
|
331
279
|
|
280
|
+
# Takes a contract. The related argument must be an array.
|
281
|
+
# Checks the contract against every element of the array.
|
282
|
+
# If it passes for all elements, the contract passes.
|
283
|
+
# Example: <tt>ArrayOf[Num]</tt>
|
284
|
+
ArrayOf = CollectionOf::Factory.new(Array)
|
285
|
+
|
286
|
+
# Takes a contract. The related argument must be a set.
|
287
|
+
# Checks the contract against every element of the set.
|
288
|
+
# If it passes for all elements, the contract passes.
|
289
|
+
# Example: <tt>SetOf[Num]</tt>
|
290
|
+
SetOf = CollectionOf::Factory.new(Set)
|
291
|
+
|
332
292
|
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
333
293
|
# and uses it to validate every element passed in
|
334
294
|
# through <tt>*args</tt>.
|
@@ -342,18 +302,6 @@ module Contracts
|
|
342
302
|
def to_s
|
343
303
|
"Args[#{@contract}]"
|
344
304
|
end
|
345
|
-
|
346
|
-
def testable?
|
347
|
-
Testable.testable? @contract
|
348
|
-
end
|
349
|
-
|
350
|
-
def test_data
|
351
|
-
[
|
352
|
-
[],
|
353
|
-
[Testable.test_data(@contract)],
|
354
|
-
[Testable.test_data(@contract), Testable.test_data(@contract)]
|
355
|
-
]
|
356
|
-
end
|
357
305
|
end
|
358
306
|
|
359
307
|
class Bool
|
@@ -366,9 +314,17 @@ module Contracts
|
|
366
314
|
# one for hash keys and one for hash values.
|
367
315
|
# Example: <tt>HashOf[Symbol, String]</tt>
|
368
316
|
class HashOf < CallableClass
|
369
|
-
|
370
|
-
|
371
|
-
|
317
|
+
INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract"
|
318
|
+
|
319
|
+
def initialize(key, value = nil)
|
320
|
+
if value
|
321
|
+
@key = key
|
322
|
+
@value = value
|
323
|
+
else
|
324
|
+
validate_hash(key)
|
325
|
+
@key = key.keys.first
|
326
|
+
@value = key[@key]
|
327
|
+
end
|
372
328
|
end
|
373
329
|
|
374
330
|
def valid?(hash)
|
@@ -381,6 +337,12 @@ module Contracts
|
|
381
337
|
def to_s
|
382
338
|
"Hash<#{@key}, #{@value}>"
|
383
339
|
end
|
340
|
+
|
341
|
+
private
|
342
|
+
|
343
|
+
def validate_hash(hash)
|
344
|
+
fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1
|
345
|
+
end
|
384
346
|
end
|
385
347
|
|
386
348
|
# Takes a Contract.
|
@@ -392,47 +354,6 @@ module Contracts
|
|
392
354
|
end
|
393
355
|
end
|
394
356
|
|
395
|
-
# class ::Hash
|
396
|
-
# def testable?
|
397
|
-
# values.all? do |val|
|
398
|
-
# Testable.testable?(val)
|
399
|
-
# end
|
400
|
-
# end
|
401
|
-
|
402
|
-
# def test_data
|
403
|
-
# keys = self.keys
|
404
|
-
# _vals = keys.map do |key|
|
405
|
-
# ret = Testable.test_data(self[key])
|
406
|
-
# if ret.is_a? Array
|
407
|
-
# ret
|
408
|
-
# else
|
409
|
-
# [ret]
|
410
|
-
# end
|
411
|
-
# end
|
412
|
-
# all_vals = Testable.product(_vals)
|
413
|
-
# hashes = []
|
414
|
-
# all_vals.each do |vals|
|
415
|
-
# hash = {}
|
416
|
-
# keys.zip(vals).each do |key, val|
|
417
|
-
# hash[key] = val
|
418
|
-
# end
|
419
|
-
# hashes << hash
|
420
|
-
# end
|
421
|
-
# hashes
|
422
|
-
# end
|
423
|
-
# end
|
424
|
-
|
425
|
-
# class ::String
|
426
|
-
# def self.testable?
|
427
|
-
# true
|
428
|
-
# end
|
429
|
-
|
430
|
-
# def self.test_data
|
431
|
-
# # send a random string
|
432
|
-
# ("a".."z").to_a.shuffle[0, 10].join
|
433
|
-
# end
|
434
|
-
# end
|
435
|
-
|
436
357
|
# Used to define contracts on functions passed in as arguments.
|
437
358
|
# Example: <tt>Func[Num => Num] # the function should take a number and return a number</tt>
|
438
359
|
class Func < CallableClass
|
data/lib/contracts/decorators.rb
CHANGED
@@ -61,6 +61,24 @@ module Contracts
|
|
61
61
|
|
62
62
|
@decorated_methods[method_type][name] ||= []
|
63
63
|
|
64
|
+
unless decorators.size == 1
|
65
|
+
fail %{
|
66
|
+
Oops, it looks like method '#{name}' has multiple contracts:
|
67
|
+
#{decorators.map { |x| x[1][0].inspect }.join("\n")}
|
68
|
+
|
69
|
+
Did you accidentally put more than one contract on a single function, like so?
|
70
|
+
|
71
|
+
Contract String => String
|
72
|
+
Contract Num => String
|
73
|
+
def foo x
|
74
|
+
end
|
75
|
+
|
76
|
+
If you did NOT, then you have probably discovered a bug in this library.
|
77
|
+
Please file it along with the relevant code at:
|
78
|
+
https://github.com/egonSchiele/contracts.ruby/issues
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
64
82
|
pattern_matching = false
|
65
83
|
decorators.each do |klass, args|
|
66
84
|
# a reference to the method gets passed into the contract here. This is good because
|
@@ -68,6 +86,21 @@ module Contracts
|
|
68
86
|
# now the *only* reference to the old method that exists.
|
69
87
|
# We assume here that the decorator (klass) responds to .new
|
70
88
|
decorator = klass.new(self, method_reference, *args)
|
89
|
+
new_args_contract = decorator.args_contracts
|
90
|
+
matched = @decorated_methods[method_type][name].select do |contract|
|
91
|
+
contract.args_contracts == new_args_contract
|
92
|
+
end
|
93
|
+
unless matched.empty?
|
94
|
+
fail ContractError.new(%{
|
95
|
+
It looks like you are trying to use pattern-matching, but
|
96
|
+
multiple definitions for function '#{name}' have the same
|
97
|
+
contract for input parameters:
|
98
|
+
|
99
|
+
#{(matched + [decorator]).map(&:to_s).join("\n")}
|
100
|
+
|
101
|
+
Each definition needs to have a different contract for the parameters.
|
102
|
+
}, {})
|
103
|
+
end
|
71
104
|
@decorated_methods[method_type][name] << decorator
|
72
105
|
pattern_matching ||= decorator.pattern_match?
|
73
106
|
end
|
@@ -93,14 +126,14 @@ module Contracts
|
|
93
126
|
# Here's why: Suppose you have this code:
|
94
127
|
#
|
95
128
|
# class Foo
|
96
|
-
# Contract
|
129
|
+
# Contract String
|
97
130
|
# def to_s
|
98
131
|
# "Foo"
|
99
132
|
# end
|
100
133
|
# end
|
101
134
|
#
|
102
135
|
# class Bar < Foo
|
103
|
-
# Contract
|
136
|
+
# Contract String
|
104
137
|
# def to_s
|
105
138
|
# super + "Bar"
|
106
139
|
# end
|
data/lib/contracts/errors.rb
CHANGED
@@ -22,6 +22,12 @@ end
|
|
22
22
|
class ContractError < ContractBaseError
|
23
23
|
end
|
24
24
|
|
25
|
+
class ParamContractError < ContractError
|
26
|
+
end
|
27
|
+
|
28
|
+
class ReturnContractError < ContractError
|
29
|
+
end
|
30
|
+
|
25
31
|
# @private
|
26
32
|
# Special contract error used internally to detect pattern failure during pattern matching
|
27
33
|
class PatternMatchingError < ContractBaseError
|
data/lib/contracts/version.rb
CHANGED
@@ -215,6 +215,20 @@ RSpec.describe "Contracts:" do
|
|
215
215
|
end
|
216
216
|
end
|
217
217
|
|
218
|
+
describe "SetOf:" do
|
219
|
+
it "should pass for a set of nums" do
|
220
|
+
expect { @o.product_from_set(Set.new([1, 2, 3])) }.to_not raise_error
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should fail for an array with one non-num" do
|
224
|
+
expect { @o.product_from_set(Set.new([1, 2, 3, "bad"])) }.to raise_error(ContractError)
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should fail for a non-array" do
|
228
|
+
expect { @o.product_from_set(1) }.to raise_error(ContractError)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
218
232
|
describe "Bool:" do
|
219
233
|
it "should pass for an argument that is a boolean" do
|
220
234
|
expect { @o.bool_test(true) }.to_not raise_error
|
@@ -241,12 +255,27 @@ RSpec.describe "Contracts:" do
|
|
241
255
|
end
|
242
256
|
|
243
257
|
describe "HashOf:" do
|
258
|
+
it "doesn't allow to specify multiple key-value pairs with pretty syntax" do
|
259
|
+
expect do
|
260
|
+
Class.new do
|
261
|
+
include Contracts
|
262
|
+
|
263
|
+
Contract Contracts::HashOf[Symbol => String, Contracts::Num => Contracts::Num] => nil
|
264
|
+
def something(hash)
|
265
|
+
# ...
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end.to raise_error(ArgumentError, "You should provide only one key-value pair to HashOf contract")
|
269
|
+
end
|
270
|
+
|
244
271
|
context "given a fulfilled contract" do
|
245
272
|
it { expect(@o.gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
|
273
|
+
it { expect(@o.pretty_gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
|
246
274
|
end
|
247
275
|
|
248
276
|
context "given an unfulfilled contract" do
|
249
277
|
it { expect { @o.gives_max_value(:panda => "1", :bamboo => "2") }.to raise_error(ContractError) }
|
278
|
+
it { expect { @o.pretty_gives_max_value(:panda => "1", :bamboo => "2") }.to raise_error(ContractError) }
|
250
279
|
end
|
251
280
|
|
252
281
|
describe "#to_s" do
|
data/spec/contracts_spec.rb
CHANGED
@@ -15,6 +15,27 @@ RSpec.describe "Contracts:" do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
describe "contracts for functions with no arguments" do
|
19
|
+
it "should work for functions with no args" do
|
20
|
+
expect { @o.no_args }.to_not raise_error
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should still work for old-style contracts for functions with no args" do
|
24
|
+
expect { @o.old_style_no_args }.to_not raise_error
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should not work for a function with a bad contract" do
|
28
|
+
expect do
|
29
|
+
Class.new(GenericExample) do
|
30
|
+
Contract Num, Num
|
31
|
+
def no_args_bad_contract
|
32
|
+
1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end.to raise_error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
18
39
|
describe "pattern matching" do
|
19
40
|
let(:string_with_hello) { "Hello, world" }
|
20
41
|
let(:string_without_hello) { "Hi, world" }
|
@@ -54,6 +75,28 @@ RSpec.describe "Contracts:" do
|
|
54
75
|
).to eq("foo")
|
55
76
|
end
|
56
77
|
|
78
|
+
it "if the return contract for a pattern match fails, it should fail instead of trying the next pattern match" do
|
79
|
+
expect do
|
80
|
+
subject.double(1)
|
81
|
+
end.to raise_error(ContractError)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should fail if multiple methods are defined with the same contract (for pattern-matching)" do
|
85
|
+
expect do
|
86
|
+
Class.new(GenericExample) do
|
87
|
+
Contract Contracts::Num => Contracts::Num
|
88
|
+
def same_param_contract x
|
89
|
+
x + 2
|
90
|
+
end
|
91
|
+
|
92
|
+
Contract Contracts::Num => String
|
93
|
+
def same_param_contract x
|
94
|
+
"sdf"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end.to raise_error(ContractError)
|
98
|
+
end
|
99
|
+
|
57
100
|
context "when failure_callback was overriden" do
|
58
101
|
before do
|
59
102
|
::Contract.override_failure_callback do |_data|
|
@@ -73,6 +116,12 @@ RSpec.describe "Contracts:" do
|
|
73
116
|
).to be_a(PatternMatchingExample::Failure)
|
74
117
|
end
|
75
118
|
|
119
|
+
it "if the return contract for a pattern match fails, it should fail instead of trying the next pattern match, even with the failure callback" do
|
120
|
+
expect do
|
121
|
+
subject.double(1)
|
122
|
+
end.to raise_error(ContractError)
|
123
|
+
end
|
124
|
+
|
76
125
|
it "uses overriden failure_callback when pattern matching fails" do
|
77
126
|
expect do
|
78
127
|
subject.process_request("hello")
|
@@ -273,10 +322,6 @@ RSpec.describe "Contracts:" do
|
|
273
322
|
end
|
274
323
|
end
|
275
324
|
|
276
|
-
it "should work for functions with no args" do
|
277
|
-
expect { @o.no_args }.to_not raise_error
|
278
|
-
end
|
279
|
-
|
280
325
|
describe "classes" do
|
281
326
|
it "should pass for correct input" do
|
282
327
|
expect { @o.hello("calvin") }.to_not raise_error
|
data/spec/fixtures/fixtures.rb
CHANGED
@@ -212,15 +212,29 @@ class GenericExample
|
|
212
212
|
end
|
213
213
|
end
|
214
214
|
|
215
|
+
Contract SetOf[Num] => Num
|
216
|
+
def product_from_set(vals)
|
217
|
+
vals.inject(1) do |acc, x|
|
218
|
+
acc * x
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
215
222
|
Contract Bool => nil
|
216
223
|
def bool_test(x)
|
217
224
|
end
|
218
225
|
|
219
|
-
Contract
|
226
|
+
Contract Num
|
220
227
|
def no_args
|
221
228
|
1
|
222
229
|
end
|
223
230
|
|
231
|
+
# This function has a contract which says it has no args,
|
232
|
+
# but the function does have args.
|
233
|
+
Contract nil => Num
|
234
|
+
def old_style_no_args
|
235
|
+
2
|
236
|
+
end
|
237
|
+
|
224
238
|
Contract ArrayOf[Num], Func[Num => Num] => ArrayOf[Num]
|
225
239
|
def map(arr, func)
|
226
240
|
ret = []
|
@@ -267,18 +281,23 @@ class GenericExample
|
|
267
281
|
hash.values.max
|
268
282
|
end
|
269
283
|
|
284
|
+
Contract HashOf[Symbol => Num] => Num
|
285
|
+
def pretty_gives_max_value(hash)
|
286
|
+
hash.values.max
|
287
|
+
end
|
288
|
+
|
270
289
|
Contract EmptyCont => Any
|
271
290
|
def using_empty_contract(a)
|
272
291
|
a
|
273
292
|
end
|
274
293
|
|
275
|
-
Contract
|
294
|
+
Contract String
|
276
295
|
def a_private_method
|
277
296
|
"works"
|
278
297
|
end
|
279
298
|
private :a_private_method
|
280
299
|
|
281
|
-
Contract
|
300
|
+
Contract String
|
282
301
|
def a_protected_method
|
283
302
|
"works"
|
284
303
|
end
|
@@ -286,14 +305,14 @@ class GenericExample
|
|
286
305
|
|
287
306
|
private
|
288
307
|
|
289
|
-
Contract
|
308
|
+
Contract String
|
290
309
|
def a_really_private_method
|
291
310
|
"works for sure"
|
292
311
|
end
|
293
312
|
|
294
313
|
protected
|
295
314
|
|
296
|
-
Contract
|
315
|
+
Contract String
|
297
316
|
def a_really_protected_method
|
298
317
|
"works for sure"
|
299
318
|
end
|
@@ -394,6 +413,16 @@ class PatternMatchingExample
|
|
394
413
|
def do_stuff(number, string, other_number)
|
395
414
|
"bar"
|
396
415
|
end
|
416
|
+
|
417
|
+
Contract Num => Num
|
418
|
+
def double x
|
419
|
+
"bad"
|
420
|
+
end
|
421
|
+
|
422
|
+
Contract String => String
|
423
|
+
def double x
|
424
|
+
x * 2
|
425
|
+
end
|
397
426
|
end
|
398
427
|
|
399
428
|
# invariant example (silliest implementation ever)
|
@@ -3,6 +3,10 @@ class GenericExample
|
|
3
3
|
def splat_then_optional_named(*vals, repeat: 2)
|
4
4
|
vals.map { |v| v * repeat }
|
5
5
|
end
|
6
|
+
|
7
|
+
Contract ({foo: Nat}) => nil
|
8
|
+
def nat_test_with_kwarg(foo: 10)
|
9
|
+
end
|
6
10
|
end
|
7
11
|
|
8
12
|
RSpec.describe "Contracts:" do
|
@@ -19,4 +23,18 @@ RSpec.describe "Contracts:" do
|
|
19
23
|
expect { @o.splat_then_optional_named("hello", "world", repeat: 3) }.to_not raise_error
|
20
24
|
end
|
21
25
|
end
|
26
|
+
|
27
|
+
describe "Nat:" do
|
28
|
+
it "should pass for keyword args with correct arg given" do
|
29
|
+
expect { @o.nat_test_with_kwarg(foo: 10) }.to_not raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should fail with a ContractError for wrong keyword args input" do
|
33
|
+
expect { @o.nat_test_with_kwarg(foo: -10) }.to raise_error(ContractError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should fail with a ContractError for no input" do
|
37
|
+
expect { @o.nat_test_with_kwarg }.to raise_error(ContractError)
|
38
|
+
end
|
39
|
+
end
|
22
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.9'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aditya Bhargava
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: This library provides contracts for Ruby. Contracts let you clearly express
|
14
14
|
how your code behaves, and free you from writing tons of boilerplate, defensive
|
@@ -22,8 +22,8 @@ files:
|
|
22
22
|
- ".rspec"
|
23
23
|
- ".rubocop.yml"
|
24
24
|
- ".travis.yml"
|
25
|
+
- CHANGELOG.markdown
|
25
26
|
- Gemfile
|
26
|
-
- Gemfile.lock
|
27
27
|
- README.md
|
28
28
|
- TODO.markdown
|
29
29
|
- TUTORIAL.md
|
@@ -42,7 +42,6 @@ files:
|
|
42
42
|
- lib/contracts/method_reference.rb
|
43
43
|
- lib/contracts/modules.rb
|
44
44
|
- lib/contracts/support.rb
|
45
|
-
- lib/contracts/testable.rb
|
46
45
|
- lib/contracts/version.rb
|
47
46
|
- script/rubocop.rb
|
48
47
|
- spec/builtin_contracts_spec.rb
|
data/Gemfile.lock
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
contracts (0.7)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: http://rubygems.org/
|
8
|
-
specs:
|
9
|
-
ast (2.0.0)
|
10
|
-
astrolabe (1.3.0)
|
11
|
-
parser (>= 2.2.0.pre.3, < 3.0)
|
12
|
-
diff-lcs (1.2.5)
|
13
|
-
hirb (0.7.2)
|
14
|
-
method_profiler (2.0.1)
|
15
|
-
hirb (>= 0.6.0)
|
16
|
-
parser (2.2.0.3)
|
17
|
-
ast (>= 1.1, < 3.0)
|
18
|
-
powerpack (0.1.0)
|
19
|
-
rainbow (2.0.0)
|
20
|
-
rspec (3.1.0)
|
21
|
-
rspec-core (~> 3.1.0)
|
22
|
-
rspec-expectations (~> 3.1.0)
|
23
|
-
rspec-mocks (~> 3.1.0)
|
24
|
-
rspec-core (3.1.7)
|
25
|
-
rspec-support (~> 3.1.0)
|
26
|
-
rspec-expectations (3.1.2)
|
27
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
-
rspec-support (~> 3.1.0)
|
29
|
-
rspec-mocks (3.1.3)
|
30
|
-
rspec-support (~> 3.1.0)
|
31
|
-
rspec-support (3.1.2)
|
32
|
-
rubocop (0.29.1)
|
33
|
-
astrolabe (~> 1.3)
|
34
|
-
parser (>= 2.2.0.1, < 3.0)
|
35
|
-
powerpack (~> 0.1)
|
36
|
-
rainbow (>= 1.99.1, < 3.0)
|
37
|
-
ruby-progressbar (~> 1.4)
|
38
|
-
ruby-prof (0.15.2)
|
39
|
-
ruby-progressbar (1.7.5)
|
40
|
-
|
41
|
-
PLATFORMS
|
42
|
-
ruby
|
43
|
-
|
44
|
-
DEPENDENCIES
|
45
|
-
contracts!
|
46
|
-
method_profiler
|
47
|
-
rspec
|
48
|
-
rubocop (~> 0.29)
|
49
|
-
ruby-prof
|
data/lib/contracts/testable.rb
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
module Contracts
|
2
|
-
class Testable
|
3
|
-
# Given an array-of-arrays of arguments,
|
4
|
-
# gives you the product of those arguments so that
|
5
|
-
# each possible combination is tried.
|
6
|
-
# Example: <tt>[[1, 2], [3, 4]]</tt> would give you:
|
7
|
-
#
|
8
|
-
# [[1, 3], [1, 4], [2, 3], [2, 4]]
|
9
|
-
def self.product(arrays)
|
10
|
-
arrays.inject do |acc, x|
|
11
|
-
acc.product(x)
|
12
|
-
end.flatten(arrays.size - 2)
|
13
|
-
end
|
14
|
-
|
15
|
-
# Given a contract, tells if you it's testable
|
16
|
-
def self.testable?(contract)
|
17
|
-
if contract.respond_to?(:testable?)
|
18
|
-
contract.testable?
|
19
|
-
else
|
20
|
-
contract.respond_to?(:new) && contract.method(:new).arity == 0
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Given a contract, returns the test data associated with that contract
|
25
|
-
def self.test_data(contract)
|
26
|
-
if contract.respond_to?(:testable?)
|
27
|
-
contract.test_data
|
28
|
-
else
|
29
|
-
contract.new
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# TODO: Should work on whatever class it was invoked on, no?
|
34
|
-
def self.check_all
|
35
|
-
o = Object.new
|
36
|
-
Object.decorated_methods.each do |name, _contracts|
|
37
|
-
check(o.method(name))
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.check(meth)
|
42
|
-
contracts = meth.owner.decorated_methods[meth.name.to_sym][0].contracts
|
43
|
-
arg_contracts = contracts[0, contracts.size - 1]
|
44
|
-
return_val = contracts[-1]
|
45
|
-
checkable = arg_contracts.all? do |arg_contract|
|
46
|
-
Testable.testable?(arg_contract)
|
47
|
-
end
|
48
|
-
|
49
|
-
if checkable
|
50
|
-
print "Checking #{meth.name}..."
|
51
|
-
_test_data = arg_contracts.map do |arg_contract|
|
52
|
-
data = Testable.test_data(arg_contract)
|
53
|
-
data.is_a?(Array) ? data : [data]
|
54
|
-
end
|
55
|
-
test_data = Testable.product _test_data
|
56
|
-
test_data.each do |args|
|
57
|
-
if args.is_a? Hash
|
58
|
-
# because *hash destroys the hash
|
59
|
-
res = meth.call(args)
|
60
|
-
else
|
61
|
-
res = meth.call(*args)
|
62
|
-
end
|
63
|
-
Contract.valid?(res, return_val)
|
64
|
-
end
|
65
|
-
puts "#{test_data.size} tests run."
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|