contracts 0.8 → 0.9
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.
- 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
|