# from http://blog.mauricecodik.com/projects/ruby/contracts.rb # See http://blog.mauricecodik.com/2005/10/ruby-meta-programming-software.html #! /usr/bin/ruby # Contracts - A small library that lets users define software contracts on their methods. # Example: # require "contracts" # class TestContracts # extend Contracts # define_data :writable => lambda {|x| x.respond_to?("write") and x.respond_to?("closed?") and not x.closed? }, # :positive => lambda {|x| x >= 0 } # contract :hello, [:positive, :string, :writable] # def hello(n, s, f) # n.times { f.write "hello #{s}!\n" } # end # end # tc = TestContracts.new # tc.hello(2, "world", $stdout) # -> hello world! # -> hello world! # tc.hello(2, 3, $stdout) # -> test-contracts.rb:22: argument 2 of method 'hello' must satisfy the 'string' contract (Contracts::ContractViolation) # Copyright (C) 2005 Maurice Codik - maurice.codik@gmail.com # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and # associated documentation files (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, publish, distribute, # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or substantial # portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT # LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module Contracts def valid_contract(input) if @user_defined and @user_defined[input] @user_defined[input] else case input when :number lambda {|x| x.is_a? Numeric } when :string lambda {|x| x.is_a? String } when :anything lambda {|x| true } else lambda {|x| false } end end end class ContractViolation < StandardError end # Now, we create a method that allows users to define custom contracts. # The new contracts are stored in the @user_defined hash, which is checked by valid_contract above. def define_data (inputs={}) @user_defined ||= {} inputs.each do |name, cnt| @user_defined[name] = cnt if cnt.respond_to?("call") end end def contract(method, *inputs) @contracts ||= {} @contracts[method] = inputs end # Finally, we create a method that does all of the real work: it generates the required contract checking code # and inserts it into the class def setup_contract(method, inputs) @contracts[method] = nil method_renamed = "__#{method}".intern conditions = "" inputs.flatten.each_with_index do |input, i| conditions << %{ if not TestContracts.valid_contract(#{input.inspect}).call(args[#{i}]) raise ContractViolation, "argument #{i+1} of method '#{method}' must satisfy the '#{input}' contract", caller end } end class_eval %{ alias_method #{method_renamed.inspect}, #{method.inspect} def #{method}(*args) #{conditions} return #{method_renamed}(*args) end } end # Ruby calls this every time a method is defined in the class. # Here, we call setup_contract if there was a contract defined for this method def method_added(method) inputs = @contracts[method] setup_contract(method, inputs) if inputs end end