handshake 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +4 -2
- data/README.txt +110 -0
- data/Rakefile +2 -1
- data/lib/handshake/block_contract.rb +53 -0
- data/lib/handshake/clause_methods.rb +170 -0
- data/lib/handshake/proxy_self.rb +19 -0
- data/lib/handshake/version.rb +1 -1
- data/lib/handshake.rb +589 -0
- data/test/tc_handshake.rb +54 -0
- metadata +6 -4
- data/README +0 -33
- data/lib/handshake/handshake.rb +0 -737
data/Manifest.txt
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
Manifest.txt
|
2
|
-
README
|
2
|
+
README.txt
|
3
3
|
MIT-LICENSE
|
4
4
|
Rakefile
|
5
5
|
|
6
6
|
lib/handshake.rb
|
7
|
-
lib/handshake/
|
7
|
+
lib/handshake/block_contract.rb
|
8
|
+
lib/handshake/clause_methods.rb
|
8
9
|
lib/handshake/inheritable_attributes.rb
|
10
|
+
lib/handshake/proxy_self.rb
|
9
11
|
lib/handshake/version.rb
|
10
12
|
test/tc_handshake.rb
|
data/README.txt
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
= Handshake
|
2
|
+
|
3
|
+
Handshake is an informal design-by-contract system written in pure Ruby.
|
4
|
+
It's intended to allow Ruby developers to apply simple, clear constraints
|
5
|
+
to their methods and classes. Handshake is written by Brian Guthrie
|
6
|
+
(btguthrie@gmail.com) and lives at http://handshake.rubyforge.org.
|
7
|
+
|
8
|
+
=== Features
|
9
|
+
|
10
|
+
* Method signature contracts
|
11
|
+
* Contracts on blocks and procs
|
12
|
+
* Method pre- and post-conditions
|
13
|
+
* Class invariants
|
14
|
+
* Define a class as abstract
|
15
|
+
|
16
|
+
=== Examples
|
17
|
+
|
18
|
+
Here's an example of Handshake in action:
|
19
|
+
|
20
|
+
# An array that can never be empty.
|
21
|
+
class NonEmptyArray < Array
|
22
|
+
include Handshake
|
23
|
+
invariant { not empty? }
|
24
|
+
end
|
25
|
+
|
26
|
+
# An array to which only strings may be added.
|
27
|
+
class NonEmptyStringArray < NonEmptyArray
|
28
|
+
contract :initialize, [[ String ]] => anything
|
29
|
+
contract :<<, String => self
|
30
|
+
contract :+, many?(String) => self
|
31
|
+
contract :each, Block(String => anything) => self
|
32
|
+
end
|
33
|
+
|
34
|
+
Handshake can also define pre- and post-conditions on your methods.
|
35
|
+
|
36
|
+
class Foo
|
37
|
+
before do
|
38
|
+
assert( not @widget.nil? )
|
39
|
+
end
|
40
|
+
def something_that_requires_widget
|
41
|
+
...
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
See Handshake::ClassMethods for more documentation on exact syntax and
|
46
|
+
capabilities. Handshake::ClauseMethods contains a number of helper and
|
47
|
+
combinator clauses for defining contract signatures.
|
48
|
+
|
49
|
+
=== Caveats
|
50
|
+
|
51
|
+
Handshake works by wrapping any class that includes it with a proxy object
|
52
|
+
that performs the relevant contract checks. It acts as a barrier between
|
53
|
+
an object and its callers. Unfortunately, this means that internal calls,
|
54
|
+
for example to private methods, that do not pass across this barrier, are
|
55
|
+
unchecked. Here's an example:
|
56
|
+
|
57
|
+
class UncheckedCall
|
58
|
+
include Handshake
|
59
|
+
|
60
|
+
contract String => Numeric
|
61
|
+
def checked_public(str); str.to_i; end
|
62
|
+
|
63
|
+
def checked_public_delegates(str)
|
64
|
+
checked_private(str)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
contract String => Numeric
|
69
|
+
def checked_private(str); str.to_i; end
|
70
|
+
end
|
71
|
+
|
72
|
+
In this example, we have a public checked method protected by a contract. Any
|
73
|
+
external call to this method will be checked. The method marked as
|
74
|
+
checked_public_delegates calls a private method that is itself protected by a
|
75
|
+
contract. But because the call to that private method is internal, and does not
|
76
|
+
pass across the contract barrier, no contract will be applied.
|
77
|
+
|
78
|
+
You can get around this problem by calling private methods on the special
|
79
|
+
private method +checked_self+:
|
80
|
+
|
81
|
+
class UncheckedCall
|
82
|
+
...
|
83
|
+
def checked_public_delegates(str)
|
84
|
+
checked_self.checked_private(str)
|
85
|
+
end
|
86
|
+
...
|
87
|
+
end
|
88
|
+
|
89
|
+
=== License (MIT)
|
90
|
+
|
91
|
+
Copyright (c) 2007 Brian Guthrie
|
92
|
+
|
93
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
94
|
+
a copy of this software and associated documentation files (the
|
95
|
+
"Software"), to deal in the Software without restriction, including
|
96
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
97
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
98
|
+
permit persons to whom the Software is furnished to do so, subject to
|
99
|
+
the following conditions:
|
100
|
+
|
101
|
+
The above copyright notice and this permission notice shall be
|
102
|
+
included in all copies or substantial portions of the Software.
|
103
|
+
|
104
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
105
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
106
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
107
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
108
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
109
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
110
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -8,6 +8,7 @@ require 'rake/rdoctask'
|
|
8
8
|
require 'rake/contrib/rubyforgepublisher'
|
9
9
|
require 'fileutils'
|
10
10
|
require 'hoe'
|
11
|
+
|
11
12
|
include FileUtils
|
12
13
|
require File.join(File.dirname(__FILE__), 'lib', 'handshake', 'version')
|
13
14
|
|
@@ -23,7 +24,7 @@ NAME = "handshake"
|
|
23
24
|
REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
24
25
|
VERS = ENV['VERSION'] || (Handshake::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
25
26
|
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
26
|
-
RDOC_OPTS = ['--quiet', '--title', "
|
27
|
+
RDOC_OPTS = ['--quiet', '--title', "Handshake documentation",
|
27
28
|
"--opname", "index.html",
|
28
29
|
"--line-numbers",
|
29
30
|
"--main", "README",
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Proc
|
2
|
+
attr_reader :contract
|
3
|
+
|
4
|
+
# Define a contract on this Proc. When the contract is set, redefine this
|
5
|
+
# proc's call method to ensure that values that pass through the proc are
|
6
|
+
# checked according to the contract.
|
7
|
+
def contract=(proc_contract)
|
8
|
+
@contract = proc_contract
|
9
|
+
end
|
10
|
+
|
11
|
+
def checked?
|
12
|
+
not @contract.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def checked_call(*args)
|
16
|
+
Handshake.catch_contract("Contract violated in call to proc #{self}") do
|
17
|
+
@contract.check_accepts! *args
|
18
|
+
end if checked?
|
19
|
+
|
20
|
+
return_value = orig_call(*args)
|
21
|
+
|
22
|
+
Handshake.catch_contract("Contract violated by proc #{self}") do
|
23
|
+
@contract.check_returns! return_value
|
24
|
+
end if checked?
|
25
|
+
|
26
|
+
return_value
|
27
|
+
end
|
28
|
+
alias :orig_call :call
|
29
|
+
alias :call :checked_call
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module Handshake
|
34
|
+
# For block-checking, we need a class which is_a? Proc for instance checking
|
35
|
+
# purposes but isn't the same so as not to prevent the user from passing in
|
36
|
+
# explicitly defined procs as arguments.
|
37
|
+
# Retained for backwards compatibility; all blocks should be block contracts.
|
38
|
+
class Block
|
39
|
+
def Block.===(o); Proc === o; end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClauseMethods
|
43
|
+
|
44
|
+
# Block signature definition. Returns a ProcContract with the given
|
45
|
+
# attributes
|
46
|
+
def Block(contract_hash)
|
47
|
+
pc = Handshake::ProcContract.new
|
48
|
+
pc.signature = contract_hash
|
49
|
+
pc
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Handshake
|
2
|
+
# Transforms the given block into a contract clause. Clause fails if
|
3
|
+
# the given block returns false or nil, passes otherwise. See
|
4
|
+
# Handshake::ClauseMethods for more examples of its use. This object may
|
5
|
+
# be instantiated directly but calling Handshake::ClauseMethods#clause is
|
6
|
+
# generally preferable.
|
7
|
+
class Clause
|
8
|
+
# Defines a new Clause object with a block and a message.
|
9
|
+
# The block should return a boolean value. The message is optional but
|
10
|
+
# strongly recommended for human-readable contract violation errors.
|
11
|
+
def initialize(mesg=nil, &block) # :yields: argument
|
12
|
+
@mesg, @block = mesg, block
|
13
|
+
end
|
14
|
+
# Returns true if the block passed to the constructor returns true when
|
15
|
+
# called with the given argument.
|
16
|
+
def ===(o)
|
17
|
+
@block.call(o)
|
18
|
+
end
|
19
|
+
# Returns the message defined for this Clause, or "undocumented clause"
|
20
|
+
# if none is defined.
|
21
|
+
def inspect; @mesg || "undocumented clause"; end
|
22
|
+
def ==(other)
|
23
|
+
other.class == self.class && other.mesg == @mesg && other.block == @block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# A collection of methods for defining constraints on method arguments.
|
28
|
+
# Use them inline with method signatures:
|
29
|
+
# contract any?(String, nil) => all?(Fixnum, nonzero?)
|
30
|
+
module ClauseMethods
|
31
|
+
ANYTHING = Clause.new("anything") { true }
|
32
|
+
|
33
|
+
# Passes if the given block returns true when passed the argument.
|
34
|
+
def clause(mesg=nil, &block) # :yields: argument
|
35
|
+
Clause.new(mesg, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Passes if the subclause does not pass on the argument.
|
39
|
+
def not?(clause)
|
40
|
+
clause("not #{clause.inspect}") { |o| not ( clause === o ) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Always passes.
|
44
|
+
def anything; ANYTHING; end
|
45
|
+
|
46
|
+
# Distinct from nil, and only for accepts: passes if zero arguments.
|
47
|
+
# Since Ruby will throw an arity exception anyway, this is essentially
|
48
|
+
# aesthetics.
|
49
|
+
def nothing; []; end
|
50
|
+
|
51
|
+
# Passes if argument is true or false.
|
52
|
+
# contract self => boolean?
|
53
|
+
# def ==(other)
|
54
|
+
# ...
|
55
|
+
# end
|
56
|
+
def boolean?
|
57
|
+
any?(TrueClass, FalseClass)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Passes if any of the subclauses pass on the argument.
|
61
|
+
# contract any?(String, Symbol) => anything
|
62
|
+
def any?(*clauses)
|
63
|
+
clause("any of #{clauses.inspect}") { |o| clauses.any? {|c| c === o} }
|
64
|
+
end
|
65
|
+
alias :or? :any?
|
66
|
+
|
67
|
+
# Passes only if all of the subclauses pass on the argument.
|
68
|
+
# contract all?(Integer, nonzero?)
|
69
|
+
def all?(*clauses)
|
70
|
+
clause("all of #{clauses.inspect}") { |o| clauses.all? {|c| c === o} }
|
71
|
+
end
|
72
|
+
alias :and? :all?
|
73
|
+
|
74
|
+
# Passes if argument is numeric and nonzero.
|
75
|
+
def nonzero?
|
76
|
+
all? Numeric, clause("nonzero") {|o| o != 0}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Passes if argument is Enumerable and the subclause passes on all of
|
80
|
+
# its objects.
|
81
|
+
#
|
82
|
+
# class StringArray < Array
|
83
|
+
# include Handshake
|
84
|
+
# contract :+, many?(String) => self
|
85
|
+
# end
|
86
|
+
def many?(clause)
|
87
|
+
many_with_map?(clause) { |o| o }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Passes if argument is Enumerable and the subclause passes on all of
|
91
|
+
# its objects, mapped over the given block.
|
92
|
+
# contract many_with_map?(nonzero?, "person age") { |person| person.age } => anything
|
93
|
+
def many_with_map?(clause, mesg=nil, &block) # :yields: argument
|
94
|
+
map_mesg = ( mesg.nil? ? "" : " after map #{mesg}" )
|
95
|
+
many_with_map = clause("many of #{clause.inspect}#{map_mesg}") do |o|
|
96
|
+
o.map(&block).all? { |p| clause === p }
|
97
|
+
end
|
98
|
+
all? Enumerable, many_with_map
|
99
|
+
end
|
100
|
+
|
101
|
+
# Passes if argument is a Hash and if the key and value clauses pass all
|
102
|
+
# of its keys and values, respectively.
|
103
|
+
# E.g. <tt>hash_of?(Symbol, String)</tt>:
|
104
|
+
#
|
105
|
+
# :foo => "bar", :baz => "qux" # passes
|
106
|
+
# :foo => "bar", "baz" => 3 # fails
|
107
|
+
def hash_of?(key_clause, value_clause)
|
108
|
+
all_keys = many_with_map?(key_clause, "all keys") { |kv| kv[0] }
|
109
|
+
all_values = many_with_map?(value_clause, "all values") { |kv| kv[1] }
|
110
|
+
all? Hash, all_keys, all_values
|
111
|
+
end
|
112
|
+
|
113
|
+
# Passes only if argument is a hash and does not contain any keys except
|
114
|
+
# those given.
|
115
|
+
# E.g. <tt>hash_with_keys(:foo, :bar, :baz)</tt>:
|
116
|
+
#
|
117
|
+
# :foo => 3 # passes
|
118
|
+
# :foo => 10, :bar => "foo" # passes
|
119
|
+
# :foo => "eight", :chunky_bacon => "delicious" # fails
|
120
|
+
def hash_with_keys(*keys)
|
121
|
+
key_assertion = clause("contains keys #{keys.inspect}") do |o|
|
122
|
+
( o.keys - keys ).empty?
|
123
|
+
end
|
124
|
+
all? Hash, key_assertion
|
125
|
+
end
|
126
|
+
alias :hash_with_options :hash_with_keys
|
127
|
+
|
128
|
+
# Passes if:
|
129
|
+
# * argument is a hash, and
|
130
|
+
# * argument contains only the keys explicitly specified in the given
|
131
|
+
# hash, and
|
132
|
+
# * every value contract in the given hash passes every applicable value
|
133
|
+
# in the argument hash
|
134
|
+
# E.g. <tt>hash_contract(:foo => String, :bar => Integer)</tt>
|
135
|
+
#
|
136
|
+
# :foo => "foo" # passes
|
137
|
+
# :bar => 3 # passes
|
138
|
+
# :foo => "bar", :bar => 42 # passes
|
139
|
+
# :foo => 88, :bar => "none" # fails
|
140
|
+
def hash_contract(hash)
|
141
|
+
value_assertions = hash.keys.map do |k|
|
142
|
+
clause("key #{k} requires #{hash[k].inspect}") do |o|
|
143
|
+
o.has_key?(k) ? hash[k] === o[k] : true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
all? hash_with_keys(*hash.keys), *value_assertions
|
147
|
+
end
|
148
|
+
|
149
|
+
# Passes if argument responds to all of the given methods.
|
150
|
+
def responds_to?(*methods)
|
151
|
+
respond_assertions = methods.map do |m|
|
152
|
+
clause("responds to #{m}") { |o| o.respond_to? m }
|
153
|
+
end
|
154
|
+
all? *respond_assertions
|
155
|
+
end
|
156
|
+
|
157
|
+
# Allows you to check whether the argument is_a? of the given symbol.
|
158
|
+
# For example, is?(:String). Useful for situations where you want
|
159
|
+
# to check for a class type that hasn't been defined yet when Ruby
|
160
|
+
# evaluates the contract but will have been by the time the code runs.
|
161
|
+
# Note that <tt>String => anything</tt> is equivalent to
|
162
|
+
# <tt>is?(:String) => anything</tt>.
|
163
|
+
def is?(class_symbol)
|
164
|
+
clause(class_symbol.to_s) { |o|
|
165
|
+
Object.const_defined?(class_symbol) && o.is_a?(Object.const_get(class_symbol))
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Redefines each of the given methods as a call to self#send. This assumes
|
2
|
+
# that self#send knows what do with them. In this case is to make sure each
|
3
|
+
# method call is checked by contracts.
|
4
|
+
class Class # :nodoc:
|
5
|
+
def proxy_self(*meths)
|
6
|
+
meths.each do |meth|
|
7
|
+
class_eval <<-EOS
|
8
|
+
def #{meth}(*args, &block)
|
9
|
+
self.send(:#{meth}, *args, &block)
|
10
|
+
end
|
11
|
+
EOS
|
12
|
+
end
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# def ===(other)
|
17
|
+
# other.is_a? self
|
18
|
+
# end
|
19
|
+
end
|