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 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/handshake.rb
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', "handshake documentation",
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
@@ -1,7 +1,7 @@
1
1
  module Handshake # :nodoc:
2
2
  module VERSION # :nodoc:
3
3
  MAJOR = 0
4
- MINOR = 1
4
+ MINOR = 2
5
5
  TINY = 0
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end