contracts 0.9 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +5 -2
- data/TUTORIAL.md +136 -21
- data/benchmarks/hash.rb +69 -0
- data/lib/contracts.rb +20 -156
- data/lib/contracts/builtin_contracts.rb +100 -3
- data/lib/contracts/call_with.rb +96 -0
- data/lib/contracts/decorators.rb +4 -194
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +46 -0
- data/lib/contracts/engine/target.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/support.rb +45 -30
- data/lib/contracts/validators.rb +127 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +78 -2
- data/spec/contracts_spec.rb +64 -1
- data/spec/fixtures/fixtures.rb +77 -5
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +10 -2
- metadata +12 -6
- data/lib/contracts/eigenclass.rb +0 -38
- data/lib/contracts/modules.rb +0 -17
@@ -29,21 +29,21 @@ module Contracts
|
|
29
29
|
# Check that an argument is a positive number.
|
30
30
|
class Pos
|
31
31
|
def self.valid? val
|
32
|
-
val > 0
|
32
|
+
val && val.is_a?(Numeric) && val > 0
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
# Check that an argument is a negative number.
|
37
37
|
class Neg
|
38
38
|
def self.valid? val
|
39
|
-
val < 0
|
39
|
+
val && val.is_a?(Numeric) && val < 0
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
# Check that an argument is a natural number.
|
44
44
|
class Nat
|
45
45
|
def self.valid? val
|
46
|
-
val && val >= 0
|
46
|
+
val && val.is_a?(Integer) && val >= 0
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
@@ -310,6 +310,24 @@ module Contracts
|
|
310
310
|
end
|
311
311
|
end
|
312
312
|
|
313
|
+
# Use this to specify a Range object of a particular datatype.
|
314
|
+
# Example: <tt>RangeOf[Nat]</tt>, <tt>RangeOf[Date]</tt>, ...
|
315
|
+
class RangeOf < CallableClass
|
316
|
+
def initialize(contract)
|
317
|
+
@contract = contract
|
318
|
+
end
|
319
|
+
|
320
|
+
def valid?(val)
|
321
|
+
val.is_a?(Range) &&
|
322
|
+
Contract.valid?(val.first, @contract) &&
|
323
|
+
Contract.valid?(val.last, @contract)
|
324
|
+
end
|
325
|
+
|
326
|
+
def to_s
|
327
|
+
"a range of #{@contract}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
313
331
|
# Use this to specify the Hash characteristics. Takes two contracts,
|
314
332
|
# one for hash keys and one for hash values.
|
315
333
|
# Example: <tt>HashOf[Symbol, String]</tt>
|
@@ -345,6 +363,81 @@ module Contracts
|
|
345
363
|
end
|
346
364
|
end
|
347
365
|
|
366
|
+
# Use this for specifying contracts for keyword arguments
|
367
|
+
# Example: <tt>KeywordArgs[ e: Range, f: Optional[Num] ]</tt>
|
368
|
+
class KeywordArgs < CallableClass
|
369
|
+
def initialize(options)
|
370
|
+
@options = options
|
371
|
+
end
|
372
|
+
|
373
|
+
def valid?(hash)
|
374
|
+
options.all? do |key, contract|
|
375
|
+
Optional._valid?(hash, key, contract)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def to_s
|
380
|
+
"KeywordArgs[#{options}]"
|
381
|
+
end
|
382
|
+
|
383
|
+
def inspect
|
384
|
+
to_s
|
385
|
+
end
|
386
|
+
|
387
|
+
private
|
388
|
+
|
389
|
+
attr_reader :options
|
390
|
+
end
|
391
|
+
|
392
|
+
# Use this for specifying optional keyword argument
|
393
|
+
# Example: <tt>Optional[Num]</tt>
|
394
|
+
class Optional < CallableClass
|
395
|
+
UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH =
|
396
|
+
"Unable to use Optional contract outside of KeywordArgs contract"
|
397
|
+
|
398
|
+
def self._valid?(hash, key, contract)
|
399
|
+
return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
|
400
|
+
contract.within_opt_hash!
|
401
|
+
!hash.key?(key) || Contract.valid?(hash[key], contract)
|
402
|
+
end
|
403
|
+
|
404
|
+
def initialize(contract)
|
405
|
+
@contract = contract
|
406
|
+
@within_opt_hash = false
|
407
|
+
end
|
408
|
+
|
409
|
+
def within_opt_hash!
|
410
|
+
@within_opt_hash = true
|
411
|
+
self
|
412
|
+
end
|
413
|
+
|
414
|
+
def valid?(value)
|
415
|
+
ensure_within_opt_hash
|
416
|
+
Contract.valid?(value, contract)
|
417
|
+
end
|
418
|
+
|
419
|
+
def to_s
|
420
|
+
"Optional[#{formatted_contract}]"
|
421
|
+
end
|
422
|
+
|
423
|
+
def inspect
|
424
|
+
to_s
|
425
|
+
end
|
426
|
+
|
427
|
+
private
|
428
|
+
|
429
|
+
attr_reader :contract, :within_opt_hash
|
430
|
+
|
431
|
+
def ensure_within_opt_hash
|
432
|
+
return if within_opt_hash
|
433
|
+
fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
|
434
|
+
end
|
435
|
+
|
436
|
+
def formatted_contract
|
437
|
+
Formatters::InspectWrapper.create(contract)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
348
441
|
# Takes a Contract.
|
349
442
|
# The contract passes if the contract passes or the given value is nil.
|
350
443
|
# Maybe(foo) is equivalent to Or[foo, nil].
|
@@ -352,6 +445,10 @@ module Contracts
|
|
352
445
|
def initialize(*vals)
|
353
446
|
super(*(vals + [nil]))
|
354
447
|
end
|
448
|
+
|
449
|
+
def include_proc?
|
450
|
+
@vals.include? Proc
|
451
|
+
end
|
355
452
|
end
|
356
453
|
|
357
454
|
# Used to define contracts on functions passed in as arguments.
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Contracts
|
2
|
+
module CallWith
|
3
|
+
def call_with(this, *args, &blk)
|
4
|
+
args << blk if blk
|
5
|
+
|
6
|
+
# Explicitly append blk=nil if nil != Proc contract violation anticipated
|
7
|
+
maybe_append_block!(args, blk)
|
8
|
+
|
9
|
+
# Explicitly append options={} if Hash contract is present
|
10
|
+
maybe_append_options!(args, blk)
|
11
|
+
|
12
|
+
# Loop forward validating the arguments up to the splat (if there is one)
|
13
|
+
(@args_contract_index || args.size).times do |i|
|
14
|
+
contract = args_contracts[i]
|
15
|
+
arg = args[i]
|
16
|
+
validator = @args_validators[i]
|
17
|
+
|
18
|
+
unless validator && validator[arg]
|
19
|
+
return unless Contract.failure_callback(:arg => arg,
|
20
|
+
:contract => contract,
|
21
|
+
:class => klass,
|
22
|
+
:method => method,
|
23
|
+
:contracts => self,
|
24
|
+
:arg_pos => i+1,
|
25
|
+
:total_args => args.size,
|
26
|
+
:return_value => false)
|
27
|
+
end
|
28
|
+
|
29
|
+
if contract.is_a?(Contracts::Func)
|
30
|
+
args[i] = Contract.new(klass, arg, *contract.contracts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# If there is a splat loop backwards to the lower index of the splat
|
35
|
+
# Once we hit the splat in this direction set its upper index
|
36
|
+
# Keep validating but use this upper index to get the splat validator.
|
37
|
+
if @args_contract_index
|
38
|
+
splat_upper_index = @args_contract_index
|
39
|
+
(args.size - @args_contract_index).times do |i|
|
40
|
+
arg = args[args.size - 1 - i]
|
41
|
+
|
42
|
+
if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
|
43
|
+
splat_upper_index = i
|
44
|
+
end
|
45
|
+
|
46
|
+
# Each arg after the spat is found must use the splat validator
|
47
|
+
j = i < splat_upper_index ? i : splat_upper_index
|
48
|
+
contract = args_contracts[args_contracts.size - 1 - j]
|
49
|
+
validator = @args_validators[args_contracts.size - 1 - j]
|
50
|
+
|
51
|
+
unless validator && validator[arg]
|
52
|
+
return unless Contract.failure_callback(:arg => arg,
|
53
|
+
:contract => contract,
|
54
|
+
:class => klass,
|
55
|
+
:method => method,
|
56
|
+
:contracts => self,
|
57
|
+
:arg_pos => i-1,
|
58
|
+
:total_args => args.size,
|
59
|
+
:return_value => false)
|
60
|
+
end
|
61
|
+
|
62
|
+
if contract.is_a?(Contracts::Func)
|
63
|
+
args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# If we put the block into args for validating, restore the args
|
69
|
+
args.slice!(-1) if blk
|
70
|
+
result = if method.respond_to?(:call)
|
71
|
+
# proc, block, lambda, etc
|
72
|
+
method.call(*args, &blk)
|
73
|
+
else
|
74
|
+
# original method name referrence
|
75
|
+
method.send_to(this, *args, &blk)
|
76
|
+
end
|
77
|
+
|
78
|
+
unless @ret_validator[result]
|
79
|
+
Contract.failure_callback(:arg => result,
|
80
|
+
:contract => ret_contract,
|
81
|
+
:class => klass,
|
82
|
+
:method => method,
|
83
|
+
:contracts => self,
|
84
|
+
:return_value => true)
|
85
|
+
end
|
86
|
+
|
87
|
+
this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
|
88
|
+
|
89
|
+
if ret_contract.is_a?(Contracts::Func)
|
90
|
+
result = Contract.new(klass, result, *ret_contract.contracts)
|
91
|
+
end
|
92
|
+
|
93
|
+
result
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/contracts/decorators.rb
CHANGED
@@ -1,208 +1,18 @@
|
|
1
1
|
module Contracts
|
2
2
|
module MethodDecorators
|
3
3
|
def self.extended(klass)
|
4
|
-
|
5
|
-
|
6
|
-
class << klass
|
7
|
-
attr_accessor :decorated_methods
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
module EigenclassWithOwner
|
12
|
-
def self.lift(eigenclass)
|
13
|
-
fail Contracts::ContractsNotIncluded unless with_owner?(eigenclass)
|
14
|
-
|
15
|
-
eigenclass
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def self.with_owner?(eigenclass)
|
21
|
-
eigenclass.respond_to?(:owner_class) && eigenclass.owner_class
|
22
|
-
end
|
4
|
+
Engine.apply(klass)
|
23
5
|
end
|
24
6
|
|
25
|
-
# first, when you write a contract, the decorate method gets called which
|
26
|
-
# sets the @decorators variable. Then when the next method after the contract
|
27
|
-
# is defined, method_added is called and we look at the @decorators variable
|
28
|
-
# to find the decorator for that method. This is how we associate decorators
|
29
|
-
# with methods.
|
30
7
|
def method_added(name)
|
31
|
-
|
8
|
+
MethodHandler.new(name, false, self).handle
|
32
9
|
super
|
33
10
|
end
|
34
11
|
|
35
12
|
def singleton_method_added(name)
|
36
|
-
|
13
|
+
MethodHandler.new(name, true, self).handle
|
37
14
|
super
|
38
15
|
end
|
39
|
-
|
40
|
-
def pop_decorators
|
41
|
-
Array(@decorators).tap { @decorators = nil }
|
42
|
-
end
|
43
|
-
|
44
|
-
def fetch_decorators
|
45
|
-
pop_decorators + Eigenclass.lift(self).pop_decorators
|
46
|
-
end
|
47
|
-
|
48
|
-
def common_method_added(name, is_class_method)
|
49
|
-
decorators = fetch_decorators
|
50
|
-
return if decorators.empty?
|
51
|
-
|
52
|
-
@decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
|
53
|
-
|
54
|
-
if is_class_method
|
55
|
-
method_reference = SingletonMethodReference.new(name, method(name))
|
56
|
-
method_type = :class_methods
|
57
|
-
else
|
58
|
-
method_reference = MethodReference.new(name, instance_method(name))
|
59
|
-
method_type = :instance_methods
|
60
|
-
end
|
61
|
-
|
62
|
-
@decorated_methods[method_type][name] ||= []
|
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
|
-
|
82
|
-
pattern_matching = false
|
83
|
-
decorators.each do |klass, args|
|
84
|
-
# a reference to the method gets passed into the contract here. This is good because
|
85
|
-
# we are going to redefine this method with a new name below...so this reference is
|
86
|
-
# now the *only* reference to the old method that exists.
|
87
|
-
# We assume here that the decorator (klass) responds to .new
|
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
|
104
|
-
@decorated_methods[method_type][name] << decorator
|
105
|
-
pattern_matching ||= decorator.pattern_match?
|
106
|
-
end
|
107
|
-
|
108
|
-
if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
|
109
|
-
@decorated_methods[method_type][name].each(&:pattern_match!)
|
110
|
-
|
111
|
-
pattern_matching = true
|
112
|
-
end
|
113
|
-
|
114
|
-
method_reference.make_alias(self)
|
115
|
-
|
116
|
-
return if ENV["NO_CONTRACTS"] && !pattern_matching
|
117
|
-
|
118
|
-
# in place of this method, we are going to define our own method. This method
|
119
|
-
# just calls the decorator passing in all args that were to be passed into the method.
|
120
|
-
# The decorator in turn has a reference to the actual method, so it can call it
|
121
|
-
# on its own, after doing it's decorating of course.
|
122
|
-
|
123
|
-
# Very important: THe line `current = #{self}` in the start is crucial.
|
124
|
-
# Not having it means that any method that used contracts could NOT use `super`
|
125
|
-
# (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
|
126
|
-
# Here's why: Suppose you have this code:
|
127
|
-
#
|
128
|
-
# class Foo
|
129
|
-
# Contract String
|
130
|
-
# def to_s
|
131
|
-
# "Foo"
|
132
|
-
# end
|
133
|
-
# end
|
134
|
-
#
|
135
|
-
# class Bar < Foo
|
136
|
-
# Contract String
|
137
|
-
# def to_s
|
138
|
-
# super + "Bar"
|
139
|
-
# end
|
140
|
-
# end
|
141
|
-
#
|
142
|
-
# b = Bar.new
|
143
|
-
# p b.to_s
|
144
|
-
#
|
145
|
-
# `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
|
146
|
-
# we have overwritten the function (that's what this next defn is). So it gets a
|
147
|
-
# reference to the function to call by looking at `decorated_methods`.
|
148
|
-
#
|
149
|
-
# Now, this line used to read something like:
|
150
|
-
#
|
151
|
-
# current = self#{is_class_method ? "" : ".class"}
|
152
|
-
#
|
153
|
-
# In that case, `self` would always be `Bar`, regardless of whether you were calling
|
154
|
-
# Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
|
155
|
-
# means you would always call Bar's to_s...infinite recursion! Instead, you want to
|
156
|
-
# call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
|
157
|
-
|
158
|
-
current = self
|
159
|
-
method_reference.make_definition(self) do |*args, &blk|
|
160
|
-
ancestors = current.ancestors
|
161
|
-
ancestors.shift # first one is just the class itself
|
162
|
-
while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
163
|
-
current = ancestors.shift
|
164
|
-
end
|
165
|
-
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
166
|
-
fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
|
167
|
-
end
|
168
|
-
methods = current.decorated_methods[method_type][name]
|
169
|
-
|
170
|
-
# this adds support for overloading methods. Here we go through each method and call it with the arguments.
|
171
|
-
# If we get a ContractError, we move to the next function. Otherwise we return the result.
|
172
|
-
# If we run out of functions, we raise the last ContractError.
|
173
|
-
success = false
|
174
|
-
i = 0
|
175
|
-
result = nil
|
176
|
-
expected_error = methods[0].failure_exception
|
177
|
-
until success
|
178
|
-
method = methods[i]
|
179
|
-
i += 1
|
180
|
-
begin
|
181
|
-
success = true
|
182
|
-
result = method.call_with(self, *args, &blk)
|
183
|
-
rescue expected_error => error
|
184
|
-
success = false
|
185
|
-
unless methods[i]
|
186
|
-
begin
|
187
|
-
::Contract.failure_callback(error.data, false)
|
188
|
-
rescue expected_error => final_error
|
189
|
-
raise final_error.to_contract_error
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
result
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def decorate(klass, *args)
|
199
|
-
if Support.eigenclass? self
|
200
|
-
return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args)
|
201
|
-
end
|
202
|
-
|
203
|
-
@decorators ||= []
|
204
|
-
@decorators << [klass, args]
|
205
|
-
end
|
206
16
|
end
|
207
17
|
|
208
18
|
class Decorator
|
@@ -220,7 +30,7 @@ Each definition needs to have a different contract for the parameters.
|
|
220
30
|
# inside, `decorate` is called with those params.
|
221
31
|
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
222
32
|
def #{klass}(*args, &blk)
|
223
|
-
decorate(#{klass}, *args, &blk)
|
33
|
+
::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk)
|
224
34
|
end
|
225
35
|
ruby_eval
|
226
36
|
end
|