# This file contains code for integrating the contract library with built-in
# classes and methods.
#
# See Contract::Check, Module#signature and Module#fulfills.
class Contract < Test::Unit::TestCase
# Implements checks that can for example be used in Module#signature
# specifications. They are implemented simply by overriding the === case
# equality operator. They can also be nested like this:
# # Matches something that is an Enumerable and that responds to
# # either :to_ary or :to_a.
# signature :x, Contract::Check::All[
# Enumerable,
# Contract::Check::Any[
# Contract::Check::Quack[:to_a],
# Contract::Check::Quack[:to_ary]
# ]
# ]
module Check
# An abstract Base class for Contract::Check classes.
# Contains logic for instantation.
class Base # :nodoc:
class << self
alias :[] :new
end
def initialize(*args, &block)
@args, @block = args, block
end
end
# Checks that the specified block matches.
# Example:
# signature :x, Contract::Check.block { |arg| arg > 0 }
class Block < Base
def ===(other)
@block.call(other)
end
end
# Short-cut for creating a Contract::Check::Block.
def self.block(&block) # :yields: arg
Block.new(&block)
end
# Checks that all the specified methods are answered.
# Example:
# signature :x, Contract::Check::Quack[:to_sym]
class Quack < Base
def ===(other)
@args.all? { |arg| other.respond_to?(arg) }
end
end
# Checks that all the specified conditions match.
# Example:
# signature :x, Contract::Check::All[Array, Enumerable]
class All < Base
def ===(other)
@args.all? { |arg| arg === other }
end
end
# Alias for Contract::Check::All
And = All unless defined?(And)
# Checks that at least one of the specified conditions match.
# Example:
# signature :x, Contract::Check::Any[String, Symbol]
class Any < Base
def ===(other)
@args.any? { |arg| arg === other }
end
end
# Alias for Contract::Check::Any
Or = Any unless defined?(Or)
# Checks that none of the specified conditions match.
# Example:
# signature :x, Contract::Check::None[Numeric, Symbol]
# signature :x, Contract::Check::Not[Comparable]
class None < Base
def ===(other)
not @args.any? { |arg| arg === other }
end
end
# Alias for Contract::Check::None
Not = None unless defined?(Not)
end
class << self
# Whether signatures should be checked. By default signatures are checked
# only when the application is run in $DEBUG mode. (By specifying the -d
# switch on the invocation of Ruby.)
#
# Note: If you want to change this you need to do so before doing any
# Module#signature calls or it will not be applied. It's probably best
# set right after requiring the contract library.
attr_accessor :check_signatures
alias :check_signatures? :check_signatures
# Whether fulfills should be checked. This is enabled by default.
#
# Note: If you want to change this you need to do so before doing any
# Module#fulfills calls or it will not be applied. It's probably best
# set right after requiring the contract library.
attr_accessor :check_fulfills
alias :check_fulfills? :check_fulfills
# All adaption routes.
attr_accessor :adaptions # :nodoc:
end
self.check_signatures = $DEBUG if self.check_signatures.nil?
self.check_fulfills = true if self.check_fulfills.nil?
if self.adaptions.nil? then
self.adaptions = Hash.new { |hash, key| hash[key] = Array.new }
end
# Tries to adapt the specified object to the specified type.
# Returns the old object if no suitable adaption route was found or if
# it already is of the specified type.
#
# This will only use adaptions where the :to part is equal to the specified
# type. No multi-step conversion will be performed.
def self.adapt(object, type)
return object if type === object
@adaptions[type].each do |adaption|
if adaption[:from] === object and
(adaption[:if].nil? or adaption[:if] === object)
then
result = adaption[:via].call(object)
return result if type === result
end
end
return object
end
end
class Module
# 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
def signature(method, *args)
options = {}
signature = args.dup
options.update(signature.pop) if signature.last.is_a?(Hash)
method = method.to_sym
return if not Contract.check_signatures? and options[:no_adaption]
old_method = instance_method(method)
remove_method(method) if instance_methods(false).include?(method.to_s)
arity = old_method.arity
if arity != signature.size and
(arity >= 0 or signature.size < ~arity) then
raise(ArgumentError, "signature isn't compatible with arity")
end
# Normalizes specifiers to Objects that respond to === so that the run-time
# checks only have to deal with that case. Also checks that a specifier is
# actually valid.
convert_specifier = lambda do |item|
# Procs, Methods etc.
if item.respond_to?(:call) then
Contract::Check.block { |arg| item.call(arg) }
# Already okay
elsif item.respond_to?(:===) or item == :any then
item
# Unknown specifier
else
raise(ArgumentError, "unsupported argument specifier #{item.inspect}")
end
end
signature.map!(&convert_specifier)
if options.include?(:optional) then
options[:optional] = Array(options[:optional])
options[:optional].map!(&convert_specifier)
options.delete(:optional) if options[:optional].empty?
end
if options.include?(:repeated) then
options[:repeated] = Array(options[:repeated])
if options[:repeated].size == 0 then
raise(ArgumentError, "repeated arguments may not be an empty Array")
else
options[:repeated].map!(&convert_specifier)
end
end
if options.include?(:return) then
options[:return] = convert_specifier.call(options[:return])
end
# We need to keep around references to our arguments because we will
# need to access them via ObjectSpace._id2ref so that they do not
# get garbage collected.
@signatures ||= Hash.new { |hash, key| hash[key] = Array.new }
@signatures[method] << [signature, options, old_method]
adapted = Proc.new do |obj, type, assign_to|
if options[:no_adaption] then
obj
elsif assign_to then
%{(#{assign_to} = Contract.adapt(#{obj}, #{type}))}
else
%{Contract.adapt(#{obj}, #{type})}
end
end
# We have to use class_eval so that signatures can be specified for
# methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9)
# We also make the checks as efficient as we can.
code = %{
def #{method}(*args, &block)
old_args = args.dup
#{if options.include?(:block) then
if options[:block] then
%{raise(ArgumentError, "no block given") unless block}
else
%{raise(ArgumentError, "block given") if block}
end
end
}
#{if not (options[:allow_trailing] or
options.include?(:repeated) or options.include?(:optional))
then
msg = "wrong number of arguments (\#{args.size} for " +
"#{signature.size})"
%{if args.size != #{signature.size} then
raise(ArgumentError, "#{msg}")
end
}
elsif options.include?(:optional) and
not options.include?(:allow_trailing) and
not options.include?(:repeated)
then
min = signature.size
max = signature.size + options[:optional].size
msg = "wrong number of arguments (\#{args.size} for " +
"#{min} upto #{max})"
%{unless args.size.between?(#{min}, #{max})
raise(ArgumentError, "#{msg}")
end
}
elsif signature.size > 0 then
msg = "wrong number of arguments (\#{args.size} for " +
"at least #{signature.size}"
%{if args.size < #{signature.size} then
raise(ArgumentError, "#{msg}")
end
}
end
}
#{index = 0
signature.map do |part|
next if part == :any
index += 1
msg = "argument #{index} (\#{arg.inspect}) does not match " +
"#{part.inspect}"
%{type = ObjectSpace._id2ref(#{part.object_id})
arg = args.shift
unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]}
raise(ArgumentError, "#{msg}")
end
}
end
}
#{%{catch(:args_exhausted) do} if options.include?(:optional)}
#{if optional = options[:optional] then
index = 0
optional.map do |part|
next if part == :any
index += 1
msg = "argument #{index + signature.size} " +
"(\#{arg.inspect}) does not match #{part.inspect}"
oa_index = index + signature.size - 1
%{throw(:args_exhausted) if args.empty?
type = ObjectSpace._id2ref(#{part.object_id})
arg = args.shift
unless type === #{adapted[%{arg}, %{type}, %{old_args[#{oa_index}]}]}
raise(ArgumentError, "#{msg}")
end
}
end
end
}
#{if repeated = options[:repeated] then
arg_off = 1 + signature.size
arg_off += options[:optional].size if options.include?(:optional)
msg = "argument \#{idx + #{arg_off}} " +
"(\#{arg.inspect}) does not match \#{part.inspect}"
%{parts = ObjectSpace._id2ref(#{repeated.object_id})
args.each_with_index do |arg, idx|
part = parts[idx % #{repeated.size}]
if part != :any and
not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]})
then
raise(ArgumentError, "#{msg}")
end
end
}
end
}
#{%{end} if options.include?(:optional)}
result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self).
call(*old_args, &block)
#{if rt = options[:return] and rt != :any then
msg = "return value (\#{result.inspect}) does not match #{rt.inspect}"
%{type = ObjectSpace._id2ref(#{rt.object_id})
unless type === #{adapted[%{result}, %{type}]}
raise(StandardError, "#{msg}")
end
}
end
}
end
}
class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})"
return true
end
# 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.
def fulfills(*contracts)
return unless Contract.check_fulfills?
contracts.each do |contract|
contract.implications.each do |implication|
include implication
end
end
old_method = instance_method(:initialize)
remove_method(:initialize) if instance_methods(false).include?("initialize")
# Keep visible references around so that the GC will not eat these up.
@fulfills ||= Array.new
@fulfills << [contracts, old_method]
# Have to use class_eval because define_method does not allow methods to take
# blocks. This can be cleaned up when Ruby 1.9 has become current.
class_eval %{
def initialize(*args, &block)
ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block)
ObjectSpace._id2ref(#{contracts.object_id}).each do |contract|
contract.enforce self
end
end
}, "(post initialization contract check for #{self.inspect})"
return true
end
end
module Kernel
# Adds an adaption route from the specified type to the specified type.
# Basic usage looks like this:
# adaption :from => StringIO, :to => String, :via => :read
#
# This method takes various options. Here's a complete list:
#
# :from
::
# The type that can be converted from. Defaults to +self+ meaning you
# can safely omit it in Class, Module or Contract context.
#
# :to
::
# The type that can be converted to. Defaults to +self+ meaning you
# can safely omit it in Class, Module or Contract context.
#
# Note that you need to specify either :from
or
# :to
.
#
# :via
::
# How the :from
type will be converted to the
# :to
type. If this is a Symbol the conversion will be
# done by invoking the method identified by that Symbol on the
# source object. Otherwise this should be something that responds to
# the +call+ method (for example Methods and Procs) which will get
# the source object as its argument and which should return the
# target object.
#
# :if
::
# The conversion can only be performed if this condition is met.
# This can either be something that implements the === case
# equivalence operator or something that implements the +call+
# method. So Methods, Procs, Modules, Classes and Contracts all
# make sense in this context. You can also specify a Symbol in
# which case the conversion can only be performed if the source
# object responds to the method identified by that Symbol.
#
# Note that the :if
option will default to the same
# value as the :via
option if the :via
# option is a Symbol.
#
# If you invoke this method with a block it will be used instead of
# the :via
option.
#
# See Contract.adapt for how conversion look-ups are performed.
def adaption(options = {}, &block) # :yield: source_object
options = {
:from => self,
:to => self
}.merge(options)
if block then
if options.include?(:via) then
raise(ArgumentError, "Can't use both block and :via")
else
options[:via] = block
end
end
if options[:via].respond_to?(:to_sym) then
options[:via] = options[:via].to_sym
end
options[:if] ||= options[:via] if options[:via].is_a?(Symbol)
if options[:via].is_a?(Symbol) then
symbol = options[:via]
options[:via] = lambda { |obj| obj.send(symbol) }
end
if options[:if].respond_to?(:to_sym) then
options[:if] = options[:if].to_sym
end
if options[:if].is_a?(Symbol) then
options[:if] = Contract::Check::Quack[options[:if]]
elsif options[:if].respond_to?(:call) then
callable = options[:if]
options[:if] = Contract::Check.block { |obj| callable.call(obj) }
end
if options[:from] == self and options[:to] == self then
raise(ArgumentError, "Need to specify either :from or :to")
elsif options[:from] == options[:to] then
raise(ArgumentError, "Self-adaption: :from and :to both are " +
options[:to].inspect)
end
unless options[:via]
raise(ArgumentError, "Need to specify how to adapt (use :via or block)")
end
Contract.adaptions[options[:to]] << options
end
# Built-in adaption routes that Ruby already uses in its C code.
adaption :to => Symbol, :via => :to_sym
adaption :to => String, :via => :to_str
adaption :to => Array, :via => :to_ary
adaption :to => Integer, :via => :to_int
end
# Modifies Method and UnboundMethod so that signatures set by
# Module.signatures can be retrieved.
#
# Note that this can only work when the method origin and definition name
# are known which is the reason for ruby-contract currently overloading
# all methods that return a method.
#
# This could be greatly simplified it http://www.rcrchive.net/rcr/show/292
# were to be accepted. You can help the development of ruby-contract by
# voting for that RCR.
module MethodSignatureMixin
attr_reader :origin # :nodoc:
attr_reader :name # :nodoc:
def initialize(origin = nil, name = nil) # :nodoc:
@origin, @name = origin, name
@signature = nil
@has_signature = false
signatures = origin.instance_variable_get(:@signatures)
@signature = if signatures and signatures.include?(name) then
@has_signature = true
signatures[name].last[0, 2]
elsif self.arity >= 0 then
[[:any] * self.arity, {}]
else
[[:any] * ~self.arity, { :allow_trailing => true }]
end
end
# Returns the signature of this method in the form of
# [fixed types, options]
. If no signature was specified via
# Module#signature it will still return something useful.
#
# This information can be useful in meta programming.
def signature() @signature end
# Returns whether a signatue for this method was defined via
# Module#signature.
def has_signature?() @has_signature end
end
class Method; include MethodSignatureMixin; end # :nodoc:
class UnboundMethod; include MethodSignatureMixin; end # :nodoc:
# Wrap all places where (Unbound)Methods can be created so that they
# carry around the origin and name meta data.
orig_instance_method = Module.instance_method(:instance_method)
class UnboundMethod # :nodoc:
alias :old_bind :bind # :nodoc:
end
{ UnboundMethod => [:forward, [:bind, :clone, :dup]],
Method => [:forward, [:unbind, :clone, :dup]],
Object => [:create_obj, [:method]],
Module => [:create_mod, [:instance_method]]
}.each do |mod, (type, methods)|
methods.each do |method|
old_method = orig_instance_method.old_bind(mod).call(method)
case type
when :forward then
mod.send(:define_method, method) do |*args|
result = old_method.old_bind(self).call(*args)
result.send(:initialize, @origin, @name)
result
end
when :create_mod then
mod.send(:define_method, method) do |name|
result = old_method.old_bind(self).call(name)
result.send(:initialize, self, name)
result
end
when :create_obj then
mod.send(:define_method, method) do |name|
result = old_method.old_bind(self).call(name)
meta_origin = result.inspect["."]
origin = if meta_origin then
class << self; self; end
else
origin_str = result.inspect[/[( ](.+?)\)?#/, 1]
self.class.ancestors.find do |mod|
mod.inspect == origin_str
end
end
result.send(:initialize, origin, name)
result
end
end
mod.send(:public, method)
end
end