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 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