filename../lib/contract/integration.rb
total coverage93.3
code coverage87.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

Valid XHTML 1.1! Valid CSS!