# 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