Class Module
In: lib/contract/integration.rb
Parent: Object
Method Contract Module UnboundMethod Contract::Check::Any Contract::Check::All Contract::Check::None Contract::Check::Block Contract::Check::Base Contract::Check::Quack MethodSignatureMixin Kernel lib/contract/integration.rb

Methods

Public Instance methods

Specifies that this Module/Class fulfills one or more contracts. The contracts will automatically be verified after an instance has been successfully created. This only actually does the checks when Contract.check_fulfills is enabled. The method will return true in case it actually inserted the check logic and nil in case it didn’t.

Note that this works by overriding the initialize method which means that you should either add the fulfills statements after your initialize method or call the previously defined initialize method from your new one.

[Source]

     # File lib/contract/integration.rb, line 436
436:   def fulfills(*contracts)
437:     return unless Contract.check_fulfills?
438: 
439:     contracts.each do |contract|
440:       contract.implications.each do |implication|
441:         include implication
442:       end
443:     end
444: 
445:     old_method = instance_method(:initialize)
446:     remove_method(:initialize) if instance_methods(false).include?("initialize")
447: 
448:     # Keep visible references around so that the GC will not eat these up.
449:     @fulfills ||= Array.new
450:     @fulfills << [contracts, old_method]
451: 
452:     # Have to use class_eval because define_method does not allow methods to take
453:     # blocks. This can be cleaned up when Ruby 1.9 has become current.
454:     class_eval %{
455:       def initialize(*args, &block)
456:         ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block)
457:         ObjectSpace._id2ref(#{contracts.object_id}).each do |contract|
458:           contract.enforce self
459:         end
460:       end
461:     }, "(post initialization contract check for #{self.inspect})"
462: 
463:     return true
464:   end

Checks that the arguments and return value of a method match the specified signature. Checks are only actually done when Contract.check_signatures is set to true or if the :no_adaption option is false. The method will return true in case it actually inserted the signature check logic and nil in case it didn’t.

You will usually specify one type specifier (:any which will allow anything to appear at that position of the argument list or something that implements the === case equality operator — samples are Contracts, Ranges, Classes, Modules, Regexps, Contract::Checks and so on) per argument. You can also use objects that implement the call method as type specifiers which includes Methods and Procs.

If you don’t use the :repeated or :allow_trailing options the method will take exactly as many arguments as there are type specifiers which means that signature :a_method enforces a_method having exactly zero arguments.

The checks are done by wrapping the type checks around the method. ArgumentError exceptions will be raised in case the signature contract is not adhered to by your caller.

An ArgumentError exception will be raised in case the methods natural argument list size and the signature you specified via Module.signature are incompatible. (Note that they don’t have to be completely equivalent, you can still have a method taking zero or more arguments and apply a signature that limits the actual argument count to three arguments.)

This method can take quite a few options. Here’s a complete list:

:return:A return type that the method must comply to. Note that this check (if failed) will actually raise a StandardError instead of an ArgumentError because the failure likely lies in the method itself and not in what the caller did.
:block:true or false — whether the method must take a block or not. So specifying :block => false enforces that the method is not allowed to have a block supplied.
:allow_trailing:true or false — whether the argument list may contain trailing, unchecked arguments.
:optional:An Array specifying optional arguments. These arguments are assumed to be after regular arguments, but before repeated ones. They will be checked if they are present, but don’t actually have to be present.

This could for example be useful for File.open(name, mode) where mode is optional, but has to be either an Integer or String.

Note that all optional arguments will have to be specified if you want to use optional and repeated arguments.

Specifying an empty Array is like not supplying the option at all.

:repeated:An Array that specifies arguments of a method that will be repeated over and over again at the end of the argument list.

A good sample of this are Array#values_at which takes zero or or more Numeric arguments and Enumerable#zip which takes zero or more other Enumerable arguments.

Note that the Array that was associated with the :repeated option must not be empty or an ArgumentError exception will be raised. If there’s just one repeated type you can omit the Array and directly specify the type identifier.

The :repeated option overrides the :allow_trailing option. Combining them is thus quite meaningless.

:no_adaption:true or false — whether no type adaption should be performed.

Usage:

  signature(:to_s) # no arguments
  signature(:+, :any) # one argument, type unchecked
  signature(:+, Fixnum) # one argument, type Fixnum
  signature(:+, NumericContract)
  signature(:+, 1 .. 10)
  signature(:sqrt, lambda { |arg| arg > 0 })

  signature(:each, :block => true) # has to have block
  signature(:to_i, :block => false) # not allowed to have block
  signature(:to_i, :result => Fixnum) # return value must be Fixnum
  signature(:zip, :allow_trailing => true) # unchecked trailing args
  signature(:zip, :repeated => [Enumerable]) # repeated trailing args
  signature(:zip, :repeated => Enumerable)
  # foo(3, 6, 4, 7) works; foo(5), foo(3, 2) etc. don't
  signature(:foo, :repeated => [1..4, 5..9])
  signature(:foo, :optional => [Numeric, String]) # two optional args

[Source]

     # File lib/contract/integration.rb, line 237
237:   def signature(method, *args)
238:     options = {}
239:     signature = args.dup
240:     options.update(signature.pop) if signature.last.is_a?(Hash)
241:     method = method.to_sym
242: 
243:     return if not Contract.check_signatures? and options[:no_adaption]
244: 
245:     old_method = instance_method(method)
246:     remove_method(method) if instance_methods(false).include?(method.to_s)
247: 
248:     arity = old_method.arity
249:     if arity != signature.size and
250:       (arity >= 0 or signature.size < ~arity) then
251:       raise(ArgumentError, "signature isn't compatible with arity")
252:     end
253: 
254:     # Normalizes specifiers to Objects that respond to === so that the run-time
255:     # checks only have to deal with that case. Also checks that a specifier is
256:     # actually valid.
257:     convert_specifier = lambda do |item|
258:       # Procs, Methods etc.
259:       if item.respond_to?(:call) then
260:         Contract::Check.block { |arg| item.call(arg) }
261:       # Already okay
262:       elsif item.respond_to?(:===) or item == :any then
263:         item
264:       # Unknown specifier
265:       else
266:         raise(ArgumentError, "unsupported argument specifier #{item.inspect}")
267:       end
268:     end
269: 
270:     signature.map!(&convert_specifier)
271: 
272:     if options.include?(:optional) then
273:       options[:optional] = Array(options[:optional])
274:       options[:optional].map!(&convert_specifier)
275:       options.delete(:optional) if options[:optional].empty?
276:     end
277: 
278:     if options.include?(:repeated) then
279:       options[:repeated] = Array(options[:repeated])
280:       if options[:repeated].size == 0 then
281:         raise(ArgumentError, "repeated arguments may not be an empty Array")
282:       else
283:         options[:repeated].map!(&convert_specifier)
284:       end
285:     end
286: 
287:     if options.include?(:return) then
288:       options[:return] = convert_specifier.call(options[:return])
289:     end
290: 
291:     # We need to keep around references to our arguments because we will
292:     # need to access them via ObjectSpace._id2ref so that they do not
293:     # get garbage collected.
294:     @signatures ||= Hash.new { |hash, key| hash[key] = Array.new }
295:     @signatures[method] << [signature, options, old_method]
296: 
297:     adapted = Proc.new do |obj, type, assign_to|
298:       if options[:no_adaption] then
299:         obj
300:       elsif assign_to then
301:         %{(#{assign_to} = Contract.adapt(#{obj}, #{type}))}
302:       else
303:         %{Contract.adapt(#{obj}, #{type})}
304:       end
305:     end
306: 
307:     # We have to use class_eval so that signatures can be specified for
308:     # methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9)
309:     # We also make the checks as efficient as we can.
310:     code = %{
311:       def #{method}(*args, &block)
312:         old_args = args.dup
313: 
314:         #{if options.include?(:block) then
315:             if options[:block] then
316:               %{raise(ArgumentError, "no block given") unless block}
317:             else
318:               %{raise(ArgumentError, "block given") if block}
319:             end
320:           end
321:         }
322: 
323:         #{if not (options[:allow_trailing] or
324:             options.include?(:repeated) or options.include?(:optional))
325:           then
326:             msg = "wrong number of arguments (\#{args.size} for " +
327:               "#{signature.size})"
328:             %{if args.size != #{signature.size} then
329:                 raise(ArgumentError, "#{msg}")
330:               end
331:             }
332:           elsif options.include?(:optional) and
333:             not options.include?(:allow_trailing) and
334:             not options.include?(:repeated)
335:           then
336:             min = signature.size
337:             max = signature.size + options[:optional].size
338:             msg = "wrong number of arguments (\#{args.size} for " +
339:               "#{min} upto #{max})"
340:             %{unless args.size.between?(#{min}, #{max})
341:                 raise(ArgumentError, "#{msg}")
342:               end
343:             }
344:           elsif signature.size > 0 then
345:             msg = "wrong number of arguments (\#{args.size} for " +
346:               "at least #{signature.size}"
347:             %{if args.size < #{signature.size} then
348:                 raise(ArgumentError, "#{msg}")
349:               end
350:             }
351:           end
352:         }
353: 
354:         #{index = 0
355:           signature.map do |part|
356:             next if part == :any
357:             index += 1
358:             msg = "argument #{index} (\#{arg.inspect}) does not match " +
359:               "#{part.inspect}"
360:             %{type = ObjectSpace._id2ref(#{part.object_id})
361:               arg = args.shift
362:               unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]}
363:                 raise(ArgumentError, "#{msg}")
364:               end
365:             }
366:           end
367:         }
368: 
369:         #{%{catch(:args_exhausted) do} if options.include?(:optional)}
370:           #{if optional = options[:optional] then
371:               index = 0
372:               optional.map do |part|
373:                 next if part == :any
374:                 index += 1
375:                 msg = "argument #{index + signature.size} " +
376:                   "(\#{arg.inspect}) does not match #{part.inspect}"
377:                 oa_index = index + signature.size - 1
378: 
379:                 %{throw(:args_exhausted) if args.empty?
380:                   type = ObjectSpace._id2ref(#{part.object_id})
381:                   arg = args.shift
382:                   unless type === #{adapted[%{arg}, %{type}, %{old_args[#{oa_index}]}]}
383:                     raise(ArgumentError, "#{msg}")
384:                   end
385:                 }
386:               end
387:             end
388:           }
389: 
390:           #{if repeated = options[:repeated] then
391:               arg_off = 1 + signature.size
392:               arg_off += options[:optional].size if options.include?(:optional)
393:               msg = "argument \#{idx + #{arg_off}} " +
394:                 "(\#{arg.inspect}) does not match \#{part.inspect}"
395:               %{parts = ObjectSpace._id2ref(#{repeated.object_id})
396:                 args.each_with_index do |arg, idx|
397:                   part = parts[idx % #{repeated.size}]
398:                   if part != :any and
399:                     not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]})
400:                   then
401:                     raise(ArgumentError, "#{msg}")
402:                   end
403:                 end
404:               }
405:             end
406:           }
407:         #{%{end} if options.include?(:optional)}
408: 
409:         result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self).
410:           call(*old_args, &block)
411:         #{if rt = options[:return] and rt != :any then
412:             msg = "return value (\#{result.inspect}) does not match #{rt.inspect}"
413:             %{type = ObjectSpace._id2ref(#{rt.object_id})
414:               unless type === #{adapted[%{result}, %{type}]}
415:                 raise(StandardError, "#{msg}")
416:               end
417:             }
418:           end
419:         }
420:       end
421:     }
422:     class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})"
423: 
424:     return true
425:   end

[Validate]