contracts 0.9 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 && val.integer?
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
@@ -1,208 +1,18 @@
1
1
  module Contracts
2
2
  module MethodDecorators
3
3
  def self.extended(klass)
4
- return if klass.respond_to?(:decorated_methods=)
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
- common_method_added name, false
8
+ MethodHandler.new(name, false, self).handle
32
9
  super
33
10
  end
34
11
 
35
12
  def singleton_method_added(name)
36
- common_method_added name, true
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