handshake 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|