Module: Handshake::ClassMethods

Defined in:
lib/handshake.rb

Overview

This module contains methods that are mixed into any class that includes Handshake. They allow you to define constraints on that class and its methods. Subclasses will inherit the contracts and invariants of its superclass, but Handshake contracts currently can’t be mixed-in via a module.

This module defines three kinds of contracts: class invariants, method signature constraints, and more general method pre- and post-conditions. Invariants accept a block which should return a boolean. Pre- and post- conditions expect you to use assertions (all of Test::Unit’s standard assertions are available) and will pass unless an assertion fails. Method signature contracts map inputs clauses to output clauses. A “clause” is defined as any object that implements the === method.

All method contracts are defined on the method defined immediately after their declaration unless a method name is specified. For example,

contract :foo, String => Integer

is equivalent to

contract String => Integer
def foo ...

Method signature contracts

contract String => Integer
contract [ String, 1..5, [ Integer ], Block ] => [ String, String ]
contract clause("must equal 'foo'") { |o| o == "foo" } => anything
contract Block(String => Integer) => Symbol

A method signature contract is defined as a mapping from valid inputs to to valid outputs. A clause here is any object which implements the === method. Classes, ranges, regexes, and other useful objects are thus all valid values for a method signature contract.

Multiple arguments are specified as an array. To specify that a method accepts varargs, define a nested array as the last or second-to-last item in the array. To specify that a method accepts a block, use the Block clause, which accepts a method signature hash as an argument, as above. To specify that a method should accept a block, but that the block should be unchecked, simply use Block instead of Block(… => …).

New clauses may be created easily with the Handshake::ClauseMethods#clause method. Handshake::ClauseMethods also provides a number of useful contract combinators for specifying rich input and output contracts.

Contract-checked accessors

contract_reader :foo => String, :bar => Integer
contract_writer ...
contract_accessor ...

Defines contract-checked accessors. Method names and clauses are specified in a hash. Hash values are any valid clause.

Invariants

invariant(optional_message) { returns true }

Aliased as always. Has access to instance variables and methods of object but calls to same are unchecked.

Pre/post-conditions

before(optional_message) { |arg1, ...| assert condition }
after(optional_message)  { |arg1, ..., returned| assert condition }
around(optional_message) { |arg1, ...| assert condition }

Check a set of conditions, using assertions, before and after method invocation. before and after are aliased as requires and ensures respectively. around currently throws a block argument warning; this should be fixed soon. Same scope rules as invariants, so you can check instance variables and local methods. All Test::Unit::Assertions are available for use, but any such AssertionFailed errors encountered are re-raised by Handshake as Handshake::AssertionFailed errors to avoid confusion with test case execution.

Abstract class decorator

class SuperDuperContract
  include Handshake; abstract!
  ...
end

To define a class as non-instantiable and have Handshake raise a ContractViolation if a caller attempts to do so, call abstract! at the top of the class definition. This attribute is not inherited by subclasses, but is useful if you would like to define a pure-contract superclass that isn’t intended to be instantiated directly.

Instance Method Summary collapse

Instance Method Details

#===(other) ⇒ Object

In order for contract clauses to work in conjunction with Handshake proxy objects, the === method must be redefined in terms of is_a?.



184
185
186
# File 'lib/handshake.rb', line 184

def ===(other)
  other.is_a? self
end

#abstract!Object

Define this class as non-instantiable. Subclasses do not inherit this attribute.



171
172
173
# File 'lib/handshake.rb', line 171

def abstract!
  @non_instantiable = true
end

#after(meth_or_mesg = nil, mesg = nil, &block) ⇒ Object Also known as: ensures

Specify a postcondition.



207
208
209
# File 'lib/handshake.rb', line 207

def after(meth_or_mesg=nil, mesg=nil, &block)
  condition(:after, meth_or_mesg, mesg, &block)
end

#around(meth_or_mesg = nil, mesg = nil, &block) ⇒ Object

Specify a bothcondition.



213
214
215
# File 'lib/handshake.rb', line 213

def around(meth_or_mesg=nil, mesg=nil, &block)
  condition(:around, meth_or_mesg, mesg, &block)
end

#before(meth_or_mesg = nil, mesg = nil, &block) ⇒ Object Also known as: requires

Specify a precondition.



201
202
203
# File 'lib/handshake.rb', line 201

def before(meth_or_mesg=nil, mesg=nil, &block)
  condition(:before, meth_or_mesg, mesg, &block)
end

#contract(meth_or_hash, contract_hash = nil) ⇒ Object

Specify an argument contract, with argument clauses on one side of the hash arrow and returned values on the other. Each clause must implement the === method or have been created with the assert method. This method should generally not be called directly.



192
193
194
195
196
197
198
# File 'lib/handshake.rb', line 192

def contract(meth_or_hash, contract_hash=nil)
  if meth_or_hash.is_a? Hash
    defer :contract, meth_or_hash
  else
    define_contract(meth_or_hash, contract_hash)
  end
end

#contract_accessor(meth_to_clause) ⇒ Object

Defines contract-checked attribute accessors for the given hash of method name to clause.



254
255
256
257
# File 'lib/handshake.rb', line 254

def contract_accessor(meth_to_clause)
  contract_reader meth_to_clause
  contract_writer meth_to_clause
end

#contract_defined?(method) ⇒ Boolean

Returns true if a contract is defined for the named method.

Returns:

  • (Boolean)


230
231
232
# File 'lib/handshake.rb', line 230

def contract_defined?(method)
  method_contracts.has_key?(method)
end

#contract_for(method) ⇒ Object

Returns the MethodContract for the given method name. Side effect: creates one if none defined.



219
220
221
222
223
224
225
226
227
# File 'lib/handshake.rb', line 219

def contract_for(method)
  if contract_defined?(method)
    method_contracts[method]
  else
    contract = MethodContract.new("#{self}##{method}")
    write_inheritable_hash :method_contracts, { method => contract }
    contract
  end
end

#contract_reader(meth_to_clause) ⇒ Object

Defines contract-checked attribute readers with the given hash of method name to clause.



236
237
238
239
240
241
# File 'lib/handshake.rb', line 236

def contract_reader(meth_to_clause)
  attr_reader *(meth_to_clause.keys)
  meth_to_clause.each do |meth, cls|
    contract meth, nil => cls
  end
end

#contract_writer(meth_to_clause) ⇒ Object

Defines contract-checked attribute writers with the given hash of method name to clause.



245
246
247
248
249
250
# File 'lib/handshake.rb', line 245

def contract_writer(meth_to_clause)
  attr_writer *(meth_to_clause.keys)
  meth_to_clause.each do |meth, cls|
    contract "#{meth}=".to_sym, cls => anything
  end
end

#invariant(mesg = nil, &block) ⇒ Object Also known as: always

Specify an invariant, with a block and an optional error message.



176
177
178
179
# File 'lib/handshake.rb', line 176

def invariant(mesg=nil, &block) # :yields:
  write_inheritable_array(:invariants, [ Invariant.new(mesg, &block) ] )
  nil
end

#method_added(meth_name) ⇒ Object

Callback from method add event. If a previous method contract declaration was deferred, complete it now with the name of the newly- added method.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/handshake.rb', line 262

def method_added(meth_name)
  @deferred ||= {}
  unless @deferred.empty?
    @deferred.each do |k, v|
      case k
      when :before, :after, :around
        define_condition meth_name, k, v
      when :contract
        define_contract meth_name, v
      end
    end
    @deferred.clear
  end
end