filename | ../lib/contract/integration.rb |
total coverage | 93.3 |
code coverage | 87.9 |
1 # This file contains code for integrating the contract library with built-in
2 # classes and methods.
3 #
4 # See Contract::Check, Module#signature and Module#fulfills.
5
6
7 class Contract < Test::Unit::TestCase
8 # Implements checks that can for example be used in Module#signature
9 # specifications. They are implemented simply by overriding the === case
10 # equality operator. They can also be nested like this:
11 # # Matches something that is an Enumerable and that responds to
12 # # either :to_ary or :to_a.
13 # signature :x, Contract::Check::All[
14 # Enumerable,
15 # Contract::Check::Any[
16 # Contract::Check::Quack[:to_a],
17 # Contract::Check::Quack[:to_ary]
18 # ]
19 # ]
20 module Check
21 # An abstract Base class for Contract::Check classes.
22 # Contains logic for instantation.
23 class Base # :nodoc:
24 class << self
25 alias :[] :new
26 end
27
28 def initialize(*args, &block)
29 @args, @block = args, block
30 end
31 end
32
33 # Checks that the specified block matches.
34 # Example:
35 # signature :x, Contract::Check.block { |arg| arg > 0 }
36 class Block < Base
37 def ===(other)
38 @block.call(other)
39 end
40 end
41 # Short-cut for creating a Contract::Check::Block.
42 def self.block(&block) # :yields: arg
43 Block.new(&block)
44 end
45
46 # Checks that all the specified methods are answered.
47 # Example:
48 # signature :x, Contract::Check::Quack[:to_sym]
49 class Quack < Base
50 def ===(other)
51 @args.all? { |arg| other.respond_to?(arg) }
52 end
53 end
54
55 # Checks that all the specified conditions match.
56 # Example:
57 # signature :x, Contract::Check::All[Array, Enumerable]
58 class All < Base
59 def ===(other)
60 @args.all? { |arg| arg === other }
61 end
62 end
63 # Alias for Contract::Check::All
64 And = All
65
66 # Checks that at least one of the specified conditions match.
67 # Example:
68 # signature :x, Contract::Check::Any[String, Symbol]
69 class Any < Base
70 def ===(other)
71 @args.any? { |arg| arg === other }
72 end
73 end
74 # Alias for Contract::Check::Any
75 Or = Any
76
77 # Checks that none of the specified conditions match.
78 # Example:
79 # signature :x, Contract::Check::None[Numeric, Symbol]
80 # signature :x, Contract::Check::Not[Comparable]
81 class None < Base
82 def ===(other)
83 not @args.any? { |arg| arg === other }
84 end
85 end
86 # Alias for Contract::Check::None
87 Not = None
88 end
89
90 class << self
91 # Whether signatures should be checked. By default signatures are checked
92 # only when the application is run in $DEBUG mode. (By specifying the -d
93 # switch on the invocation of Ruby.)
94 #
95 # Note: If you want to change this you need to do so before doing any
96 # Module#signature calls or it will not be applied. It's probably best
97 # set right after requiring the contract library.
98 attr_accessor :check_signatures
99 alias :check_signatures? :check_signatures
100
101 # Whether fulfills should be checked. This is enabled by default.
102 #
103 # Note: If you want to change this you need to do so before doing any
104 # Module#fulfills calls or it will not be applied. It's probably best
105 # set right after requiring the contract library.
106 attr_accessor :check_fulfills
107 alias :check_fulfills? :check_fulfills
108
109 # All adaption routes.
110 attr_accessor :adaptions # :nodoc:
111 end
112 self.check_signatures = $DEBUG if self.check_signatures.nil?
113 self.check_fulfills = true if self.check_fulfills.nil?
114 if self.adaptions.nil? then
115 self.adaptions = Hash.new { |hash, key| hash[key] = Array.new }
116 end
117
118 # Tries to adapt the specified object to the specified type.
119 # Returns the old object if no suitable adaption route was found or if
120 # it already is of the specified type.
121 #
122 # This will only use adaptions where the :to part is equal to the specified
123 # type. No multi-step conversion will be performed.
124 def self.adapt(object, type)
125 return object if type === object
126
127 @adaptions[type].each do |adaption|
128 if adaption[:from] === object and
129 (adaption[:if].nil? or adaption[:if] === object)
130 then
131 result = adaption[:via].call(object)
132 return result if type === result
133 end
134 end
135
136 return object
137 end
138 end
139
140
141 class Module
142 # Checks that the arguments and return value of a method match the specified
143 # signature. Checks are only actually done when Contract.check_signatures is
144 # set to true or if the <code>:no_adaption</code> option is +false+. The
145 # method will return +true+ in case it actually inserted the signature check
146 # logic and +nil+ in case it didn't.
147 #
148 # You will usually specify one type specifier (<code>:any</code> which will
149 # allow anything to appear at that position of the argument list or something
150 # that implements the === case equality operator -- samples are Contracts,
151 # Ranges, Classes, Modules, Regexps, Contract::Checks and so on) per argument.
152 # You can also use objects that implement the +call+ method as type specifiers
153 # which includes Methods and Procs.
154 #
155 # If you don't use the <code>:repeated</code> or <code>:allow_trailing</code>
156 # options the method will take exactly as many arguments as there are type
157 # specifiers which means that <code>signature :a_method</code> enforces
158 # +a_method+ having exactly zero arguments.
159 #
160 # The checks are done by wrapping the type checks around the method.
161 # ArgumentError exceptions will be raised in case the signature contract is
162 # not adhered to by your caller.
163 #
164 # An ArgumentError exception will be raised in case the methods natural
165 # argument list size and the signature you specified via Module.signature are
166 # incompatible. (Note that they don't have to be completely equivalent, you
167 # can still have a method taking zero or more arguments and apply a signature
168 # that limits the actual argument count to three arguments.)
169 #
170 # This method can take quite a few options. Here's a complete list:
171 #
172 # <code>:return</code>::
173 # A return type that the method must comply to. Note that this check (if
174 # failed) will actually raise a StandardError instead of an ArgumentError
175 # because the failure likely lies in the method itself and not in what the
176 # caller did.
177 #
178 # <code>:block</code>::
179 # +true+ or +false+ -- whether the method must take a block or not. So
180 # specifying <code>:block => false</code> enforces that the method is not
181 # allowed to have a block supplied.
182 #
183 # <code>:allow_trailing</code>::
184 # +true+ or +false+ -- whether the argument list may contain trailing,
185 # unchecked arguments.
186 #
187 # <code>:repeated</code>::
188 # An Array that specifies arguments of a method that will be repeated over
189 # and over again at the end of the argument list.
190 #
191 # A good sample of this are Array#values_at which takes zero or or more
192 # Numeric arguments and Enumerable#zip which takes zero or more other
193 # Enumerable arguments.
194 #
195 # Note that the Array that was associated with the <code>:repeated</code>
196 # option must not be empty or an ArgumentError exception will be raised.
197 # If there's just one repeated type you can omit the Array and directly
198 # specify the type identifier.
199 #
200 # <code>:no_adaption</code>::
201 # +true+ or +false+ -- whether no type adaption should be performed.
202 #
203 # Usage:
204 # signature(:to_s) # no arguments
205 # signature(:+, :any) # one argument, type unchecked
206 # signature(:+, Fixnum) # one argument, type Fixnum
207 # signature(:+, NumericContract)
208 # signature(:+, 1 .. 10)
209 # signature(:sqrt, lambda { |arg| arg > 0 })
210 #
211 # signature(:each, :block => true) # has to have block
212 # signature(:to_i, :block => false) # not allowed to have block
213 # signature(:to_i, :result => Fixnum) # return value must be Fixnum
214 # signature(:zip, :allow_trailing => true) # unchecked trailing args
215 # signature(:zip, :repeated => [Enumerable]) # repeated trailing args
216 # signature(:zip, :repeated => Enumerable)
217 # # foo(3, 6, 4, 7) works; foo(5), foo(3, 2) etc. don't
218 # signature(:foo, :repeated => [1..4, 5..9])
219 def signature(method, *args)
220 options = {}
221 signature = args.dup
222 options.update(signature.pop) if signature.last.is_a?(Hash)
223
224 return if not Contract.check_signatures? and options[:no_adaption]
225
226 old_method = instance_method(method)
227 remove_method(method) if instance_methods(false).include?(method.to_s)
228
229 arity = old_method.arity
230 if arity != signature.size and
231 (arity >= 0 or signature.size < ~arity) then
232 raise(ArgumentError, "signature isn't compatible with arity")
233 end
234
235 # Normalizes specifiers to Objects that respond to === so that the run-time
236 # checks only have to deal with that case. Also checks that a specifier is
237 # actually valid.
238 convert_specifier = lambda do |item|
239 # Procs, Methods etc.
240 if item.respond_to?(:call) then
241 Contract::Check.block { |arg| item.call(arg) }
242 # Already okay
243 elsif item.respond_to?(:===) or item == :any then
244 item
245 # Unknown specifier
246 else
247 raise(ArgumentError, "unsupported argument specifier #{item.inspect}")
248 end
249 end
250
251 signature.map!(&convert_specifier)
252
253 if options.include?(:repeated) then
254 options[:repeated] = Array(options[:repeated])
255 if options[:repeated].size == 0 then
256 raise(ArgumentError, "repeated arguments may not be an empty Array")
257 else
258 options[:repeated].map!(&convert_specifier)
259 end
260 end
261
262 if options.include?(:return) then
263 options[:return] = convert_specifier.call(options[:return])
264 end
265
266 # We need to keep around references to our arguments because we will
267 # need to access them via ObjectSpace._id2ref so that they do not
268 # get garbage collected.
269 @signatures ||= Hash.new { |hash, key| hash[key] = Array.new }
270 @signatures[method] << [signature, options, old_method]
271
272 adapted = Proc.new do |obj, type, assign_to|
273 if options[:no_adaption] then
274 obj
275 elsif assign_to then
276 %{(#{assign_to} = Contract.adapt(#{obj}, #{type}))}
277 else
278 %{Contract.adapt(#{obj}, #{type})}
279 end
280 end
281
282 # We have to use class_eval so that signatures can be specified for
283 # methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9)
284 # We also make the checks as efficient as we can.
285 code = %{
286 def #{method}(*args, &block)
287 old_args = args.dup
288
289 #{if options.include?(:block) then
290 if options[:block] then
291 %{raise(ArgumentError, "no block given") unless block}
292 else
293 %{raise(ArgumentError, "block given") if block}
294 end
295 end
296 }
297
298 #{if not(options[:allow_trailing] or options.include?(:repeated))
299 msg = "wrong number of arguments (\#{args.size} for " +
300 "#{signature.size})"
301 %{if args.size != #{signature.size} then
302 raise(ArgumentError, "#{msg}")
303 end
304 }
305 elsif signature.size > 0
306 msg = "wrong number of arguments (\#{args.size} for " +
307 "at least #{signature.size}"
308 %{if args.size < #{signature.size} then
309 raise(ArgumentError, "#{msg}")
310 end
311 }
312 end
313 }
314
315 #{index = 0
316 signature.map do |part|
317 next if part == :any
318 index += 1
319 msg = "argument #{index} (\#{arg.inspect}) does not match " +
320 "#{part.inspect}"
321 %{type = ObjectSpace._id2ref(#{part.object_id})
322 arg = args.shift
323 unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]}
324 raise(ArgumentError, "#{msg}")
325 end
326 }
327 end
328 }
329
330 #{if repeated = options[:repeated] then
331 msg = "argument \#{idx + #{signature.size}}" +
332 "(\#{arg.inspect}) does not match \#{part.inspect}"
333 %{parts = ObjectSpace._id2ref(#{repeated.object_id})
334 args.each_with_index do |arg, idx|
335 part = parts[idx % #{repeated.size}]
336 if part != :any and
337 not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]})
338 then
339 raise(ArgumentError, "#{msg}")
340 end
341 end
342 }
343 end
344 }
345
346 result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self).
347 call(*old_args, &block)
348 #{if rt = options[:return] and rt != :any then
349 msg = "return value (\#{result.inspect}) does not match #{rt.inspect}"
350 %{type = ObjectSpace._id2ref(#{rt.object_id})
351 unless type === #{adapted[%{result}, %{type}]}
352 raise(StandardError, "#{msg}")
353 end
354 }
355 end
356 }
357 end
358 }
359 class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})"
360
361 return true
362 end
363
364 # Specifies that this Module/Class fulfills one or more contracts. The contracts
365 # will automatically be verified after an instance has been successfully created.
366 # This only actually does the checks when Contract.check_fulfills is enabled.
367 # The method will return +true+ in case it actually inserted the check logic and
368 # +nil+ in case it didn't.
369 #
370 # Note that this works by overriding the #initialize method which means that you
371 # should either add the fulfills statements after your initialize method or call
372 # the previously defined initialize method from your new one.
373 def fulfills(*contracts)
374 return unless Contract.check_fulfills?
375
376 contracts.each do |contract|
377 contract.implications.each do |implication|
378 include implication
379 end
380 end
381
382 old_method = instance_method(:initialize)
383 remove_method(:initialize) if instance_methods(false).include?("initialize")
384
385 # Keep visible references around so that the GC will not eat these up.
386 @fulfills ||= Array.new
387 @fulfills << [contracts, old_method]
388
389 # Have to use class_eval because define_method does not allow methods to take
390 # blocks. This can be cleaned up when Ruby 1.9 has become current.
391 class_eval %{
392 def initialize(*args, &block)
393 ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block)
394 ObjectSpace._id2ref(#{contracts.object_id}).each do |contract|
395 contract.enforce self
396 end
397 end
398 }, "(post initialization contract check for #{self.inspect})"
399
400 return true
401 end
402 end
403
404
405 module Kernel
406 # Adds an adaption route from the specified type to the specified type.
407 # Basic usage looks like this:
408 # adaption :from => StringIO, :to => String, :via => :read
409 #
410 # This method takes various options. Here's a complete list:
411 #
412 # <code>:from</code>::
413 # The type that can be converted from. Defaults to +self+ meaning you
414 # can safely omit it in Class, Module or Contract context.
415 #
416 # <code>:to</code>::
417 # The type that can be converted to. Defaults to +self+ meaning you
418 # can safely omit it in Class, Module or Contract context.
419 #
420 # Note that you need to specify either <code>:from</code> or
421 # <code>:to</code>.
422 #
423 # <code>:via</code>::
424 # How the <code>:from</code> type will be converted to the
425 # <code>:to</code> type. If this is a Symbol the conversion will be
426 # done by invoking the method identified by that Symbol on the
427 # source object. Otherwise this should be something that responds to
428 # the +call+ method (for example Methods and Procs) which will get
429 # the source object as its argument and which should return the
430 # target object.
431 #
432 # <code>:if</code>::
433 # The conversion can only be performed if this condition is met.
434 # This can either be something that implements the === case
435 # equivalence operator or something that implements the +call+
436 # method. So Methods, Procs, Modules, Classes and Contracts all
437 # make sense in this context. You can also specify a Symbol in
438 # which case the conversion can only be performed if the source
439 # object responds to the method identified by that Symbol.
440 #
441 # Note that the <code>:if</code> option will default to the same
442 # value as the <code>:via</code> option if the <code>:via</code>
443 # option is a Symbol.
444 #
445 # If you invoke this method with a block it will be used instead of
446 # the <code>:via</code> option.
447 #
448 # See Contract.adapt for how conversion look-ups are performed.
449 def adaption(options = {}, &block) # :yield: source_object
450 options = {
451 :from => self,
452 :to => self
453 }.merge(options)
454
455 if block then
456 if options.include?(:via) then
457 raise(ArgumentError, "Can't use both block and :via")
458 else
459 options[:via] = block
460 end
461 end
462
463 if options[:via].respond_to?(:to_sym) then
464 options[:via] = options[:via].to_sym
465 end
466
467 options[:if] ||= options[:via] if options[:via].is_a?(Symbol)
468
469 if options[:via].is_a?(Symbol) then
470 symbol = options[:via]
471 options[:via] = lambda { |obj| obj.send(symbol) }
472 end
473
474 if options[:if].respond_to?(:to_sym) then
475 options[:if] = options[:if].to_sym
476 end
477
478 if options[:if].is_a?(Symbol) then
479 options[:if] = Contract::Check::Quack[options[:if]]
480 elsif options[:if].respond_to?(:call) then
481 callable = options[:if]
482 options[:if] = Contract::Check.block { |obj| callable.call(obj) }
483 end
484
485 if options[:from] == self and options[:to] == self then
486 raise(ArgumentError, "Need to specify either :from or :to")
487 elsif options[:from] == options[:to] then
488 raise(ArgumentError, "Self-adaption: :from and :to both are " +
489 options[:to].inspect)
490 end
491
492 unless options[:via]
493 raise(ArgumentError, "Need to specify how to adapt (use :via or block)")
494 end
495
496 Contract.adaptions[options[:to]] << options
497 end
498
499 # Built-in adaption routes that Ruby already uses in its C code.
500 adaption :to => Symbol, :via => :to_sym
501 adaption :to => String, :via => :to_str
502 adaption :to => Array, :via => :to_ary
503 adaption :to => Integer, :via => :to_int
504 end