contracts-lite 0.14.0
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 +7 -0
- data/CHANGELOG.markdown +80 -0
- data/Gemfile +16 -0
- data/LICENSE +23 -0
- data/README.md +102 -0
- data/TODO.markdown +6 -0
- data/TUTORIAL.md +747 -0
- data/benchmarks/bench.rb +67 -0
- data/benchmarks/hash.rb +69 -0
- data/benchmarks/invariants.rb +91 -0
- data/benchmarks/io.rb +62 -0
- data/benchmarks/wrap_test.rb +57 -0
- data/contracts.gemspec +13 -0
- data/lib/contracts.rb +231 -0
- data/lib/contracts/builtin_contracts.rb +541 -0
- data/lib/contracts/call_with.rb +97 -0
- data/lib/contracts/core.rb +52 -0
- data/lib/contracts/decorators.rb +47 -0
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +50 -0
- data/lib/contracts/engine/target.rb +70 -0
- data/lib/contracts/error_formatter.rb +121 -0
- data/lib/contracts/errors.rb +71 -0
- data/lib/contracts/formatters.rb +134 -0
- data/lib/contracts/invariants.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/method_reference.rb +100 -0
- data/lib/contracts/support.rb +59 -0
- data/lib/contracts/validators.rb +139 -0
- data/lib/contracts/version.rb +3 -0
- data/script/rubocop +7 -0
- data/spec/builtin_contracts_spec.rb +461 -0
- data/spec/contracts_spec.rb +748 -0
- data/spec/error_formatter_spec.rb +68 -0
- data/spec/fixtures/fixtures.rb +710 -0
- data/spec/invariants_spec.rb +17 -0
- data/spec/module_spec.rb +18 -0
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/support.rb +10 -0
- data/spec/support_spec.rb +21 -0
- data/spec/validators_spec.rb +47 -0
- metadata +94 -0
@@ -0,0 +1,541 @@
|
|
1
|
+
require "contracts/formatters"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
# rdoc
|
5
|
+
# This module contains all the builtin contracts.
|
6
|
+
# If you want to use them, first:
|
7
|
+
#
|
8
|
+
# import Contracts
|
9
|
+
#
|
10
|
+
# And then use these or write your own!
|
11
|
+
#
|
12
|
+
# A simple example:
|
13
|
+
#
|
14
|
+
# Contract Num, Num => Num
|
15
|
+
# def add(a, b)
|
16
|
+
# a + b
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# The contract is <tt>Contract Num, Num, Num</tt>.
|
20
|
+
# That says that the +add+ function takes two numbers and returns a number.
|
21
|
+
module Contracts
|
22
|
+
module Builtin
|
23
|
+
# Check that an argument is +Numeric+.
|
24
|
+
class Num
|
25
|
+
def self.valid? val
|
26
|
+
val.is_a? Numeric
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check that an argument is a positive number.
|
31
|
+
class Pos
|
32
|
+
def self.valid? val
|
33
|
+
val && val.is_a?(Numeric) && val > 0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check that an argument is a negative number.
|
38
|
+
class Neg
|
39
|
+
def self.valid? val
|
40
|
+
val && val.is_a?(Numeric) && val < 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check that an argument is an +Integer+.
|
45
|
+
class Int
|
46
|
+
def self.valid? val
|
47
|
+
val && val.is_a?(Integer)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check that an argument is a natural number (includes zero).
|
52
|
+
class Nat
|
53
|
+
def self.valid? val
|
54
|
+
val && val.is_a?(Integer) && val >= 0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check that an argument is a positive natural number (excludes zero).
|
59
|
+
class NatPos
|
60
|
+
def self.valid? val
|
61
|
+
val && val.is_a?(Integer) && val > 0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Passes for any argument.
|
66
|
+
class Any
|
67
|
+
def self.valid? val
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fails for any argument.
|
73
|
+
class None
|
74
|
+
def self.valid? val
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Use this when you are writing your own contract classes.
|
80
|
+
# Allows your contract to be called with <tt>[]</tt> instead of <tt>.new</tt>:
|
81
|
+
#
|
82
|
+
# Old: <tt>Or.new(param1, param2)</tt>
|
83
|
+
#
|
84
|
+
# New: <tt>Or[param1, param2]</tt>
|
85
|
+
#
|
86
|
+
# Of course, <tt>.new</tt> still works.
|
87
|
+
class CallableClass
|
88
|
+
include ::Contracts::Formatters
|
89
|
+
def self.[](*vals)
|
90
|
+
new(*vals)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Takes a variable number of contracts.
|
95
|
+
# The contract passes if any of the contracts pass.
|
96
|
+
# Example: <tt>Or[Fixnum, Float]</tt>
|
97
|
+
class Or < CallableClass
|
98
|
+
def initialize(*vals)
|
99
|
+
@vals = vals
|
100
|
+
end
|
101
|
+
|
102
|
+
def valid?(val)
|
103
|
+
@vals.any? do |contract|
|
104
|
+
res, _ = Contract.valid?(val, contract)
|
105
|
+
res
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
@vals[0, @vals.size-1].map do |x|
|
111
|
+
InspectWrapper.create(x)
|
112
|
+
end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Takes a variable number of contracts.
|
117
|
+
# The contract passes if exactly one of those contracts pass.
|
118
|
+
# Example: <tt>Xor[Fixnum, Float]</tt>
|
119
|
+
class Xor < CallableClass
|
120
|
+
def initialize(*vals)
|
121
|
+
@vals = vals
|
122
|
+
end
|
123
|
+
|
124
|
+
def valid?(val)
|
125
|
+
results = @vals.map do |contract|
|
126
|
+
res, _ = Contract.valid?(val, contract)
|
127
|
+
res
|
128
|
+
end
|
129
|
+
results.count(true) == 1
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_s
|
133
|
+
@vals[0, @vals.size-1].map do |x|
|
134
|
+
InspectWrapper.create(x)
|
135
|
+
end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Takes a variable number of contracts.
|
140
|
+
# The contract passes if all contracts pass.
|
141
|
+
# Example: <tt>And[Fixnum, Float]</tt>
|
142
|
+
class And < CallableClass
|
143
|
+
def initialize(*vals)
|
144
|
+
@vals = vals
|
145
|
+
end
|
146
|
+
|
147
|
+
def valid?(val)
|
148
|
+
@vals.all? do |contract|
|
149
|
+
res, _ = Contract.valid?(val, contract)
|
150
|
+
res
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_s
|
155
|
+
@vals[0, @vals.size-1].map do |x|
|
156
|
+
InspectWrapper.create(x)
|
157
|
+
end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Takes a variable number of method names as symbols.
|
162
|
+
# The contract passes if the argument responds to all
|
163
|
+
# of those methods.
|
164
|
+
# Example: <tt>RespondTo[:password, :credit_card]</tt>
|
165
|
+
class RespondTo < CallableClass
|
166
|
+
def initialize(*meths)
|
167
|
+
@meths = meths
|
168
|
+
end
|
169
|
+
|
170
|
+
def valid?(val)
|
171
|
+
@meths.all? do |meth|
|
172
|
+
val.respond_to? meth
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_s
|
177
|
+
"a value that responds to #{@meths.inspect}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Takes a variable number of method names as symbols.
|
182
|
+
# Given an argument, all of those methods are called
|
183
|
+
# on the argument one by one. If they all return true,
|
184
|
+
# the contract passes.
|
185
|
+
# Example: <tt>Send[:valid?]</tt>
|
186
|
+
class Send < CallableClass
|
187
|
+
def initialize(*meths)
|
188
|
+
@meths = meths
|
189
|
+
end
|
190
|
+
|
191
|
+
def valid?(val)
|
192
|
+
@meths.all? do |meth|
|
193
|
+
val.send(meth)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def to_s
|
198
|
+
"a value that returns true for all of #{@meths.inspect}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Takes a class +A+. If argument is object of type +A+, the contract passes.
|
203
|
+
# If it is a subclass of A (or not related to A in any way), it fails.
|
204
|
+
# Example: <tt>Exactly[Numeric]</tt>
|
205
|
+
class Exactly < CallableClass
|
206
|
+
def initialize(cls)
|
207
|
+
@cls = cls
|
208
|
+
end
|
209
|
+
|
210
|
+
def valid?(val)
|
211
|
+
val.class == @cls
|
212
|
+
end
|
213
|
+
|
214
|
+
def to_s
|
215
|
+
"exactly #{@cls.inspect}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Takes a list of values, e.g. +[:a, :b, :c]+. If argument is included in
|
220
|
+
# the list, the contract passes.
|
221
|
+
#
|
222
|
+
# Example: <tt>Enum[:a, :b, :c]</tt>?
|
223
|
+
class Enum < CallableClass
|
224
|
+
def initialize(*vals)
|
225
|
+
@vals = vals
|
226
|
+
end
|
227
|
+
|
228
|
+
def valid?(val)
|
229
|
+
@vals.include? val
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Takes a value +v+. If the argument is +.equal+ to +v+, the contract passes,
|
234
|
+
# otherwise the contract fails.
|
235
|
+
# Example: <tt>Eq[Class]</tt>
|
236
|
+
class Eq < CallableClass
|
237
|
+
def initialize(value)
|
238
|
+
@value = value
|
239
|
+
end
|
240
|
+
|
241
|
+
def valid?(val)
|
242
|
+
@value.equal?(val)
|
243
|
+
end
|
244
|
+
|
245
|
+
def to_s
|
246
|
+
"to be equal to #{@value.inspect}"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Takes a variable number of contracts. The contract
|
251
|
+
# passes if all of those contracts fail for the given argument.
|
252
|
+
# Example: <tt>Not[nil]</tt>
|
253
|
+
class Not < CallableClass
|
254
|
+
def initialize(*vals)
|
255
|
+
@vals = vals
|
256
|
+
end
|
257
|
+
|
258
|
+
def valid?(val)
|
259
|
+
@vals.all? do |contract|
|
260
|
+
res, _ = Contract.valid?(val, contract)
|
261
|
+
!res
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def to_s
|
266
|
+
"a value that is none of #{@vals.inspect}"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# @private
|
271
|
+
# Takes a collection(responds to :each) type and a contract.
|
272
|
+
# The related argument must be of specified collection type.
|
273
|
+
# Checks the contract against every element of the collection.
|
274
|
+
# If it passes for all elements, the contract passes.
|
275
|
+
# Example: <tt>CollectionOf[Array, Num]</tt>
|
276
|
+
class CollectionOf < CallableClass
|
277
|
+
def initialize(collection_class, contract)
|
278
|
+
@collection_class = collection_class
|
279
|
+
@contract = contract
|
280
|
+
end
|
281
|
+
|
282
|
+
def valid?(vals)
|
283
|
+
return false unless vals.is_a?(@collection_class)
|
284
|
+
vals.all? do |val|
|
285
|
+
res, _ = Contract.valid?(val, @contract)
|
286
|
+
res
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def to_s
|
291
|
+
"a collection #{@collection_class} of #{@contract}"
|
292
|
+
end
|
293
|
+
|
294
|
+
class Factory
|
295
|
+
def initialize(collection_class, &before_new)
|
296
|
+
@collection_class = collection_class
|
297
|
+
@before_new = before_new
|
298
|
+
end
|
299
|
+
|
300
|
+
def new(contract)
|
301
|
+
@before_new && @before_new.call
|
302
|
+
CollectionOf.new(@collection_class, contract)
|
303
|
+
end
|
304
|
+
|
305
|
+
alias_method :[], :new
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Takes a contract. The related argument must be an array.
|
310
|
+
# Checks the contract against every element of the array.
|
311
|
+
# If it passes for all elements, the contract passes.
|
312
|
+
# Example: <tt>ArrayOf[Num]</tt>
|
313
|
+
ArrayOf = CollectionOf::Factory.new(Array)
|
314
|
+
|
315
|
+
# Takes a contract. The related argument must be a set.
|
316
|
+
# Checks the contract against every element of the set.
|
317
|
+
# If it passes for all elements, the contract passes.
|
318
|
+
# Example: <tt>SetOf[Num]</tt>
|
319
|
+
SetOf = CollectionOf::Factory.new(Set)
|
320
|
+
|
321
|
+
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
322
|
+
# and uses it to validate every element passed in
|
323
|
+
# through <tt>*args</tt>.
|
324
|
+
# Example: <tt>Args[Or[String, Num]]</tt>
|
325
|
+
class Args < CallableClass
|
326
|
+
attr_reader :contract
|
327
|
+
def initialize(contract)
|
328
|
+
@contract = contract
|
329
|
+
end
|
330
|
+
|
331
|
+
def to_s
|
332
|
+
"Args[#{@contract}]"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
class Bool
|
337
|
+
def self.valid? val
|
338
|
+
val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Use this to specify a Range object of a particular datatype.
|
343
|
+
# Example: <tt>RangeOf[Nat]</tt>, <tt>RangeOf[Date]</tt>, ...
|
344
|
+
class RangeOf < CallableClass
|
345
|
+
def initialize(contract)
|
346
|
+
@contract = contract
|
347
|
+
end
|
348
|
+
|
349
|
+
def valid?(val)
|
350
|
+
val.is_a?(Range) &&
|
351
|
+
Contract.valid?(val.first, @contract) &&
|
352
|
+
Contract.valid?(val.last, @contract)
|
353
|
+
end
|
354
|
+
|
355
|
+
def to_s
|
356
|
+
"a range of #{@contract}"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Use this to specify the Hash characteristics. Takes two contracts,
|
361
|
+
# one for hash keys and one for hash values.
|
362
|
+
# Example: <tt>HashOf[Symbol, String]</tt>
|
363
|
+
class HashOf < CallableClass
|
364
|
+
INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract"
|
365
|
+
|
366
|
+
def initialize(key, value = nil)
|
367
|
+
if value
|
368
|
+
@key = key
|
369
|
+
@value = value
|
370
|
+
else
|
371
|
+
validate_hash(key)
|
372
|
+
@key = key.keys.first
|
373
|
+
@value = key[@key]
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def valid?(hash)
|
378
|
+
return false unless hash.is_a?(Hash)
|
379
|
+
keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all?
|
380
|
+
vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all?
|
381
|
+
|
382
|
+
[keys_match, vals_match].all?
|
383
|
+
end
|
384
|
+
|
385
|
+
def to_s
|
386
|
+
"Hash<#{@key}, #{@value}>"
|
387
|
+
end
|
388
|
+
|
389
|
+
private
|
390
|
+
|
391
|
+
def validate_hash(hash)
|
392
|
+
fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Use this to specify the Hash characteristics. This contracts fails
|
397
|
+
# if there are any extra keys that don't have contracts on them.
|
398
|
+
# Example: <tt>StrictHash[{ a: String, b: Bool }]</tt>
|
399
|
+
class StrictHash < CallableClass
|
400
|
+
attr_reader :contract_hash
|
401
|
+
|
402
|
+
def initialize(contract_hash)
|
403
|
+
@contract_hash = contract_hash
|
404
|
+
end
|
405
|
+
|
406
|
+
def valid?(arg)
|
407
|
+
return false unless arg.is_a?(Hash)
|
408
|
+
|
409
|
+
contract_hash.all? do |key, _v|
|
410
|
+
contract_hash.key?(key) && Contract.valid?(arg[key], contract_hash[key])
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Use this for specifying contracts for keyword arguments
|
416
|
+
# Example: <tt>KeywordArgs[ e: Range, f: Optional[Num] ]</tt>
|
417
|
+
class KeywordArgs < CallableClass
|
418
|
+
def initialize(options)
|
419
|
+
@options = options
|
420
|
+
end
|
421
|
+
|
422
|
+
def valid?(hash)
|
423
|
+
return false unless hash.is_a?(Hash)
|
424
|
+
return false unless hash.keys - options.keys == []
|
425
|
+
options.all? do |key, contract|
|
426
|
+
Optional._valid?(hash, key, contract)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def to_s
|
431
|
+
"KeywordArgs[#{options}]"
|
432
|
+
end
|
433
|
+
|
434
|
+
def inspect
|
435
|
+
to_s
|
436
|
+
end
|
437
|
+
|
438
|
+
private
|
439
|
+
|
440
|
+
attr_reader :options
|
441
|
+
end
|
442
|
+
|
443
|
+
# Use this for specifying contracts for class arguments
|
444
|
+
# Example: <tt>DescendantOf[ e: Range, f: Optional[Num] ]</tt>
|
445
|
+
class DescendantOf < CallableClass
|
446
|
+
def initialize(parent_class)
|
447
|
+
@parent_class = parent_class
|
448
|
+
end
|
449
|
+
|
450
|
+
def valid?(given_class)
|
451
|
+
given_class.is_a?(Class) && given_class.ancestors.include?(parent_class)
|
452
|
+
end
|
453
|
+
|
454
|
+
def to_s
|
455
|
+
"DescendantOf[#{parent_class}]"
|
456
|
+
end
|
457
|
+
|
458
|
+
def inspect
|
459
|
+
to_s
|
460
|
+
end
|
461
|
+
|
462
|
+
private
|
463
|
+
|
464
|
+
attr_reader :parent_class
|
465
|
+
end
|
466
|
+
|
467
|
+
# Use this for specifying optional keyword argument
|
468
|
+
# Example: <tt>Optional[Num]</tt>
|
469
|
+
class Optional < CallableClass
|
470
|
+
UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH =
|
471
|
+
"Unable to use Optional contract outside of KeywordArgs contract"
|
472
|
+
|
473
|
+
def self._valid?(hash, key, contract)
|
474
|
+
return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
|
475
|
+
contract.within_opt_hash!
|
476
|
+
!hash.key?(key) || Contract.valid?(hash[key], contract)
|
477
|
+
end
|
478
|
+
|
479
|
+
def initialize(contract)
|
480
|
+
@contract = contract
|
481
|
+
@within_opt_hash = false
|
482
|
+
end
|
483
|
+
|
484
|
+
def within_opt_hash!
|
485
|
+
@within_opt_hash = true
|
486
|
+
self
|
487
|
+
end
|
488
|
+
|
489
|
+
def valid?(value)
|
490
|
+
ensure_within_opt_hash
|
491
|
+
Contract.valid?(value, contract)
|
492
|
+
end
|
493
|
+
|
494
|
+
def to_s
|
495
|
+
"Optional[#{formatted_contract}]"
|
496
|
+
end
|
497
|
+
|
498
|
+
def inspect
|
499
|
+
to_s
|
500
|
+
end
|
501
|
+
|
502
|
+
private
|
503
|
+
|
504
|
+
attr_reader :contract, :within_opt_hash
|
505
|
+
|
506
|
+
def ensure_within_opt_hash
|
507
|
+
return if within_opt_hash
|
508
|
+
fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
|
509
|
+
end
|
510
|
+
|
511
|
+
def formatted_contract
|
512
|
+
Formatters::InspectWrapper.create(contract)
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
# Takes a Contract.
|
517
|
+
# The contract passes if the contract passes or the given value is nil.
|
518
|
+
# Maybe(foo) is equivalent to Or[foo, nil].
|
519
|
+
class Maybe < Or
|
520
|
+
def initialize(*vals)
|
521
|
+
super(*(vals + [nil]))
|
522
|
+
end
|
523
|
+
|
524
|
+
def include_proc?
|
525
|
+
@vals.include? Proc
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Used to define contracts on functions passed in as arguments.
|
530
|
+
# Example: <tt>Func[Num => Num] # the function should take a number and return a number</tt>
|
531
|
+
class Func < CallableClass
|
532
|
+
attr_reader :contracts
|
533
|
+
def initialize(*contracts)
|
534
|
+
@contracts = contracts
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Users can still include `Contracts::Core` & `Contracts::Builtin`
|
540
|
+
include Builtin
|
541
|
+
end
|