protocol 0.8.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/Rakefile +75 -0
- data/VERSION +1 -0
- data/examples/comparing.rb +37 -0
- data/examples/enumerating.rb +47 -0
- data/examples/game.rb +81 -0
- data/examples/hello_world_patternitis.rb +77 -0
- data/examples/indexing.rb +27 -0
- data/examples/locking.rb +111 -0
- data/examples/queue.rb +154 -0
- data/examples/stack.rb +75 -0
- data/install.rb +18 -0
- data/lib/protocol.rb +1070 -0
- data/lib/protocol/core.rb +22 -0
- data/make_doc.rb +9 -0
- data/tests/test_protocol.rb +620 -0
- metadata +74 -0
data/examples/queue.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'protocol'
|
2
|
+
|
3
|
+
# Queue + Observer example.
|
4
|
+
ObserverProtocol = Protocol do
|
5
|
+
def after_enq(q)
|
6
|
+
s = q.size
|
7
|
+
postcondition { q.size == s }
|
8
|
+
end
|
9
|
+
|
10
|
+
def after_deq(q)
|
11
|
+
s = q.size
|
12
|
+
postcondition { q.size == s }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class O
|
17
|
+
def after_enq(q)
|
18
|
+
puts "Enqueued."
|
19
|
+
end
|
20
|
+
|
21
|
+
def after_deq(q)
|
22
|
+
puts "Dequeued."
|
23
|
+
end
|
24
|
+
|
25
|
+
conform_to ObserverProtocol
|
26
|
+
end
|
27
|
+
|
28
|
+
class SneakyO
|
29
|
+
def after_enq(q)
|
30
|
+
puts "Enqueued."
|
31
|
+
q.deq
|
32
|
+
end
|
33
|
+
|
34
|
+
def after_deq(q)
|
35
|
+
puts "Dequeued."
|
36
|
+
end
|
37
|
+
|
38
|
+
conform_to ObserverProtocol
|
39
|
+
end
|
40
|
+
|
41
|
+
QueueProtocol = Protocol do
|
42
|
+
def observer=(o)
|
43
|
+
ObserverProtocol =~ o
|
44
|
+
end
|
45
|
+
|
46
|
+
def enq(x)
|
47
|
+
postcondition { not empty? }
|
48
|
+
postcondition { result == myself }
|
49
|
+
end
|
50
|
+
|
51
|
+
def size() end
|
52
|
+
|
53
|
+
def first()
|
54
|
+
postcondition { (size == 0) == (result == nil) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def deq()
|
58
|
+
precondition { size > 0 }
|
59
|
+
end
|
60
|
+
|
61
|
+
def empty?()
|
62
|
+
postcondition { (size == 0) == result }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Q
|
67
|
+
def initialize
|
68
|
+
@ary = []
|
69
|
+
@o = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def observer=(o)
|
73
|
+
@o = o
|
74
|
+
end
|
75
|
+
|
76
|
+
def enq(x)
|
77
|
+
@ary.push x
|
78
|
+
@o and @o.after_enq(self)
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def size()
|
83
|
+
@ary.size
|
84
|
+
end
|
85
|
+
|
86
|
+
def first()
|
87
|
+
@ary.first
|
88
|
+
end
|
89
|
+
|
90
|
+
def empty?
|
91
|
+
@ary.empty?
|
92
|
+
end
|
93
|
+
|
94
|
+
def deq()
|
95
|
+
r = @ary.shift
|
96
|
+
@o and @o.after_deq(self)
|
97
|
+
r
|
98
|
+
end
|
99
|
+
|
100
|
+
conform_to QueueProtocol
|
101
|
+
end
|
102
|
+
|
103
|
+
if $0 == __FILE__
|
104
|
+
q = Q.new
|
105
|
+
q.observer = O.new
|
106
|
+
q.empty? # => true
|
107
|
+
begin
|
108
|
+
q.deq
|
109
|
+
rescue Protocol::CheckError => e
|
110
|
+
e # => #<Protocol::PreconditionCheckError: QueueProtocol#deq(0): precondition failed for Q>
|
111
|
+
end
|
112
|
+
q.empty? # => true
|
113
|
+
q.size # => 0
|
114
|
+
q.first # => nil
|
115
|
+
q.enq 2
|
116
|
+
q.empty? # => false
|
117
|
+
q.size # => 1
|
118
|
+
q.first # => 2
|
119
|
+
q.enq 2
|
120
|
+
q.size # => 2
|
121
|
+
q.deq # => 2
|
122
|
+
q.first # => 2
|
123
|
+
q.size # => 1
|
124
|
+
|
125
|
+
q = Q.new
|
126
|
+
q.observer = O.new
|
127
|
+
q.empty? # => true
|
128
|
+
begin
|
129
|
+
q.deq
|
130
|
+
rescue Protocol::CheckError => e
|
131
|
+
e # => #<Protocol::PreconditionCheckError: QueueProtocol#deq(0): precondition failed for Q>
|
132
|
+
end
|
133
|
+
q.empty? # => true
|
134
|
+
q.size # => 0
|
135
|
+
q.first # => nil
|
136
|
+
q.enq 2
|
137
|
+
q.empty? # => false
|
138
|
+
q.size # => 1
|
139
|
+
q.first # => 2
|
140
|
+
q.enq 2
|
141
|
+
q.size # => 2
|
142
|
+
q.deq # => 2
|
143
|
+
q.first # => 2
|
144
|
+
q.size # => 1
|
145
|
+
q.observer = SneakyO.new
|
146
|
+
q.deq # => 2
|
147
|
+
q.empty? # => true
|
148
|
+
begin
|
149
|
+
q.enq 7
|
150
|
+
rescue Protocol::CheckError => e
|
151
|
+
e # => #<Protocol::PostconditionCheckError: ObserverProtocol#after_enq(1): postcondition failed for SneakyO, result = 7>
|
152
|
+
end
|
153
|
+
end
|
154
|
+
# >> "Enqueued.\nEnqueued.\nDequeued.\nEnqueued.\nEnqueued.\nDequeued.\nDequeued.\nEnqueued.\nDequeued.\n"
|
data/examples/stack.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'protocol'
|
2
|
+
|
3
|
+
# Classical Stack example.
|
4
|
+
StackProtocol = Protocol do
|
5
|
+
def push(x)
|
6
|
+
postcondition { top == x }
|
7
|
+
postcondition { result == myself }
|
8
|
+
end
|
9
|
+
|
10
|
+
def top() end
|
11
|
+
|
12
|
+
def size() end
|
13
|
+
|
14
|
+
def empty?()
|
15
|
+
postcondition { size == 0 ? result : !result }
|
16
|
+
end
|
17
|
+
|
18
|
+
def pop()
|
19
|
+
s = size
|
20
|
+
precondition { not empty? }
|
21
|
+
postcondition { size == s - 1 }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class S
|
26
|
+
def initialize
|
27
|
+
@ary = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def push(x)
|
31
|
+
@ary.push x
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def top
|
36
|
+
@ary.last
|
37
|
+
end
|
38
|
+
|
39
|
+
def size()
|
40
|
+
@ary.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty?
|
44
|
+
@ary.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def pop()
|
48
|
+
@ary.pop
|
49
|
+
end
|
50
|
+
|
51
|
+
conform_to StackProtocol
|
52
|
+
end
|
53
|
+
|
54
|
+
if $0 == __FILE__
|
55
|
+
s = S.new
|
56
|
+
s.top # => nil
|
57
|
+
s.empty? # => true
|
58
|
+
s.size # => 0
|
59
|
+
begin
|
60
|
+
s.pop
|
61
|
+
rescue Protocol::CheckError => e
|
62
|
+
e # => #<Protocol::PreconditionCheckError: StackProtocol#empty?(0): precondition failed for S>
|
63
|
+
end
|
64
|
+
s.empty? # => true
|
65
|
+
s.push 2
|
66
|
+
s.empty? # => false
|
67
|
+
s.size # => 1
|
68
|
+
s.top # => 2
|
69
|
+
s.push 4
|
70
|
+
s.top # => 4
|
71
|
+
s.size # => 2
|
72
|
+
s.pop # => 4
|
73
|
+
s.top # => 2
|
74
|
+
s.size # => 1
|
75
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set et sw=2 ts=2:
|
3
|
+
|
4
|
+
require 'rbconfig'
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils::Verbose
|
7
|
+
|
8
|
+
include Config
|
9
|
+
|
10
|
+
file = 'lib/protocol.rb'
|
11
|
+
dest = CONFIG["sitelibdir"]
|
12
|
+
install(file, dest)
|
13
|
+
|
14
|
+
dest = File.join(CONFIG["sitelibdir"], 'protocol')
|
15
|
+
mkdir_p dest
|
16
|
+
for file in Dir['lib/protocol/*.rb']
|
17
|
+
install(file, dest)
|
18
|
+
end
|
data/lib/protocol.rb
ADDED
@@ -0,0 +1,1070 @@
|
|
1
|
+
# :stopdoc:
|
2
|
+
# vim: set et sw=2 ts=2:
|
3
|
+
# :startdoc:
|
4
|
+
require 'parse_tree'
|
5
|
+
require 'sexp_processor'
|
6
|
+
#
|
7
|
+
# = protocol.rb - Method Protocol Specifications in Ruby
|
8
|
+
#
|
9
|
+
# == Author
|
10
|
+
#
|
11
|
+
# Florian Frank mailto:flori@ping.de
|
12
|
+
#
|
13
|
+
# == License
|
14
|
+
#
|
15
|
+
# This is free software; you can redistribute it and/or modify it under the
|
16
|
+
# terms of the GNU General Public License Version 2 as published by the Free
|
17
|
+
# Software Foundation: www.gnu.org/copyleft/gpl.html
|
18
|
+
#
|
19
|
+
# == Download
|
20
|
+
#
|
21
|
+
# The latest version of <b>protocol</b> can be found at
|
22
|
+
#
|
23
|
+
# * http://rubyforge.org/frs/?group_id=4778
|
24
|
+
#
|
25
|
+
# The homepage of this library is located at
|
26
|
+
#
|
27
|
+
# * http://protocol.rubyforge.org
|
28
|
+
#
|
29
|
+
# == Description
|
30
|
+
#
|
31
|
+
# This library offers an implementation of protocols against which you can
|
32
|
+
# check the conformity of your classes or instances of your classes. They are a
|
33
|
+
# bit like Java Interfaces, but as mixin modules they can also contain already
|
34
|
+
# implemented methods. Additionally you can define preconditions/postconditions
|
35
|
+
# for methods specified in a protocol.
|
36
|
+
#
|
37
|
+
# == Usage
|
38
|
+
#
|
39
|
+
# This defines a protocol named +Enumerating+:
|
40
|
+
#
|
41
|
+
# Enumerating = Protocol do
|
42
|
+
# # Iterate over each element of this Enumerating class and pass it to the
|
43
|
+
# # +block+.
|
44
|
+
# def each(&block) end
|
45
|
+
#
|
46
|
+
# include Enumerable
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Every class, that conforms to this protocol, has to implement the understood
|
50
|
+
# messages (+each+ in this example - with no ordinary arguments and a block
|
51
|
+
# argument). The following would be an equivalent protocol definition:
|
52
|
+
#
|
53
|
+
# Enumerating = Protocol do
|
54
|
+
# # Iterate over each element of this Enumerating class and pass it to the
|
55
|
+
# # +block+.
|
56
|
+
# understand :each, 0, true
|
57
|
+
#
|
58
|
+
# include Enumerable
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# An example of a conforming class is the class +Ary+:
|
62
|
+
# class Ary
|
63
|
+
# def initialize
|
64
|
+
# @ary = [1, 2, 3]
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# def each(&block)
|
68
|
+
# @ary.each(&block)
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# conform_to Enumerating
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# The last line (this command being the last line of the class definition is
|
75
|
+
# important!) of class +Ary+ <tt>conform_to Enumerating</tt> checks the
|
76
|
+
# conformance of +Ary+ to the +Enumerating+ protocol. If the +each+ method were
|
77
|
+
# not implemented in +Ary+ a CheckFailed exception would have been thrown,
|
78
|
+
# containing all the offending CheckError instances.
|
79
|
+
#
|
80
|
+
# It also mixes in all the methods that were included in protocol +Enumerating+
|
81
|
+
# (+Enumerable+'s instance methods). More examples of this can be seen in the
|
82
|
+
# examples sub directory of the source distribution of this library in file
|
83
|
+
# examples/enumerating.rb.
|
84
|
+
#
|
85
|
+
# === Template Method Pattern
|
86
|
+
#
|
87
|
+
# It's also possible to mix protocol specification and behaviour implementation
|
88
|
+
# like this:
|
89
|
+
#
|
90
|
+
# Locking = Protocol do
|
91
|
+
# specification # not necessary, because Protocol defaults to specification
|
92
|
+
# # mode already
|
93
|
+
#
|
94
|
+
# def lock() end
|
95
|
+
#
|
96
|
+
# def unlock() end
|
97
|
+
#
|
98
|
+
# implementation
|
99
|
+
#
|
100
|
+
# def synchronize
|
101
|
+
# lock
|
102
|
+
# begin
|
103
|
+
# yield
|
104
|
+
# ensure
|
105
|
+
# unlock
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# This specifies a Locking protocol against which several class implementations
|
111
|
+
# can be checked against for conformance. Here's a FileMutex implementation:
|
112
|
+
#
|
113
|
+
# class FileMutex
|
114
|
+
# def initialize
|
115
|
+
# @tempfile = Tempfile.new 'file-mutex'
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# def path
|
119
|
+
# @tempfile.path
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# def lock
|
123
|
+
# puts "Locking '#{path}'."
|
124
|
+
# @tempfile.flock File::LOCK_EX
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# def unlock
|
128
|
+
# puts "Unlocking '#{path}'."
|
129
|
+
# @tempfile.flock File::LOCK_UN
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# conform_to Locking
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# The Locking#synchronize method is a template method (see
|
136
|
+
# http://en.wikipedia.org/wiki/Template_method_pattern), that uses the
|
137
|
+
# implemtented methods, to make block based locking possbile:
|
138
|
+
#
|
139
|
+
# mutex = FileMutex.new
|
140
|
+
# mutex.synchronize do
|
141
|
+
# puts "Synchronized with '#{file.path}'."
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# Now it's easy to swap the implementation to a memory based mutex
|
145
|
+
# implementation instead:
|
146
|
+
#
|
147
|
+
# class MemoryMutex
|
148
|
+
# def initialize
|
149
|
+
# @mutex = Mutex.new
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# def lock
|
153
|
+
# @mutex.lock
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# def unlock
|
157
|
+
# @mutex.unlock
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# conform_to Locking # actually Mutex itself would conform as well ;)
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# To check an +object+ for conformity to the Locking protocol call
|
164
|
+
# Locking.check +object+ and rescue a CheckFailed. Here's an example class
|
165
|
+
#
|
166
|
+
# class MyClass
|
167
|
+
# def initialize
|
168
|
+
# @mutex = FileMutex.new
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# attr_reader :mutex
|
172
|
+
#
|
173
|
+
# def mutex=(mutex)
|
174
|
+
# Locking.check mutex
|
175
|
+
# @mutex = mutex
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# This causes a CheckFailed exception to be thrown:
|
180
|
+
# obj.mutex = Object.new
|
181
|
+
#
|
182
|
+
# This would not raise an exception:
|
183
|
+
# obj.mutex = MemoryMutex.new
|
184
|
+
#
|
185
|
+
# And neither would this
|
186
|
+
#
|
187
|
+
# obj.mutex = Mutex.new # => #<Mutex:0xb799a4f0 @locked=false, @waiting=[]>
|
188
|
+
#
|
189
|
+
# because coincidentally this is true
|
190
|
+
#
|
191
|
+
# Mutex.conform_to? Locking # => true
|
192
|
+
#
|
193
|
+
# and thus Locking.check doesn't throw an exception. See the
|
194
|
+
# examples/locking.rb file for code.
|
195
|
+
#
|
196
|
+
# === Preconditions and Postconditions
|
197
|
+
#
|
198
|
+
# You can add additional runtime checks for method arguments and results by
|
199
|
+
# specifying pre- and postconditions. Here is the classical stack example, that
|
200
|
+
# shows how:
|
201
|
+
#
|
202
|
+
# StackProtocol = Protocol do
|
203
|
+
# def push(x)
|
204
|
+
# postcondition { top == x }
|
205
|
+
# postcondition { result == myself }
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# def top() end
|
209
|
+
#
|
210
|
+
# def size() end
|
211
|
+
#
|
212
|
+
# def empty?()
|
213
|
+
# postcondition { size == 0 ? result : !result }
|
214
|
+
# end
|
215
|
+
#
|
216
|
+
# def pop()
|
217
|
+
# s = size
|
218
|
+
# precondition { not empty? }
|
219
|
+
# postcondition { size == s - 1 }
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# Defining protocols and checking against conformance doesn't get in the way of
|
224
|
+
# Ruby's duck typing, but you can still use protocols to define, document, and
|
225
|
+
# check implementations that you expect from client code.
|
226
|
+
#
|
227
|
+
# === Error modes in Protocols
|
228
|
+
#
|
229
|
+
# You can set different error modes for your protocols. By default the mode is
|
230
|
+
# set to :error, and a failed protocol conformance check raises a CheckError (a
|
231
|
+
# marker module) exception. Alternatively you can set the error mode to
|
232
|
+
# :warning with:
|
233
|
+
#
|
234
|
+
# Foo = Protocol do
|
235
|
+
# check_failure :warning
|
236
|
+
# end
|
237
|
+
#
|
238
|
+
# during Protocol definition or later
|
239
|
+
#
|
240
|
+
# Foo.check_failure :warning
|
241
|
+
#
|
242
|
+
# In :warning mode no execptions are raised, only a warning is printed to
|
243
|
+
# STDERR. If you set the error mode via Protocol::ProtocolModule#check_failure
|
244
|
+
# to :none, nothing will happen on conformance check failures.
|
245
|
+
#
|
246
|
+
module Protocol
|
247
|
+
class ::Object
|
248
|
+
# Returns true if this object conforms to +protocol+, otherwise false.
|
249
|
+
#
|
250
|
+
# This is especially useful, if check_failure in the protocol is set to
|
251
|
+
# :none or :warning, and conformance of a class to a protocol should be
|
252
|
+
# checked later in runtime.
|
253
|
+
def conform_to?(protocol)
|
254
|
+
protocol.check(self, :none)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Define a protocol configured by +block+. Look at the methods of
|
258
|
+
# ProtocolModule to get an idea on how to do that.
|
259
|
+
def Protocol(&block)
|
260
|
+
ProtocolModule.new(&block)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class ::Class
|
265
|
+
# This method should be called at the end of a class definition, that is,
|
266
|
+
# after all methods have been added to the class. The conformance to the
|
267
|
+
# protocol of the class given as the argument is checked. See
|
268
|
+
# Protocol::CHECK_MODES for the consequences of failure of this check.
|
269
|
+
alias conform_to include
|
270
|
+
|
271
|
+
# Returns true if this class conforms to +protocol+, otherwise false.
|
272
|
+
#
|
273
|
+
# This is especially useful, if check_failure in the protocol is set to
|
274
|
+
# :none or :warning, and conformance of a class to a protocol should be
|
275
|
+
# checked later in runtime.
|
276
|
+
def conform_to?(protocol)
|
277
|
+
protocol.check(self, :none)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# The base class for protocol errors.
|
282
|
+
class ProtocolError < StandardError; end
|
283
|
+
|
284
|
+
# This exception is raise if an error was encountered while defining a
|
285
|
+
# protocol specification.
|
286
|
+
class SpecificationError < ProtocolError; end
|
287
|
+
|
288
|
+
# Marker module for all exceptions related to check errors.
|
289
|
+
module CheckError; end
|
290
|
+
|
291
|
+
# If a protocol check failes this exception is raised.
|
292
|
+
class BaseCheckError < ProtocolError
|
293
|
+
include CheckError
|
294
|
+
|
295
|
+
def initialize(protocol_message, message)
|
296
|
+
super(message)
|
297
|
+
@protocol_message = protocol_message
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns the Protocol::Message object, that caused this CheckError to be
|
301
|
+
# raised.
|
302
|
+
attr_reader :protocol_message
|
303
|
+
|
304
|
+
def to_s
|
305
|
+
"#{protocol_message}: #{super}"
|
306
|
+
end
|
307
|
+
|
308
|
+
def inspect
|
309
|
+
"#<#{self.class.name}: #{to_s}>"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# This exception is raised if a method was not implented in a class, that was
|
314
|
+
# required to conform to the checked protocol.
|
315
|
+
class NotImplementedErrorCheckError < BaseCheckError; end
|
316
|
+
|
317
|
+
# This exception is raised if a method implented in a class didn't have the
|
318
|
+
# required arity, that was required to conform to the checked protocol.
|
319
|
+
class ArgumentErrorCheckError < BaseCheckError; end
|
320
|
+
|
321
|
+
# This exception is raised if a method implented in a class didn't have the
|
322
|
+
# expected block argument, that was required to conform to the checked
|
323
|
+
# protocol.
|
324
|
+
class BlockCheckError < BaseCheckError; end
|
325
|
+
|
326
|
+
# This exception is raised if a precondition check failed (the yielded
|
327
|
+
# block returned a non-true value) in a protocol description.
|
328
|
+
class PreconditionCheckError < BaseCheckError; end
|
329
|
+
|
330
|
+
# This exception is raised if a postcondition check failed (the yielded block
|
331
|
+
# returned a non-true value) in a protocol description.
|
332
|
+
class PostconditionCheckError < BaseCheckError; end
|
333
|
+
|
334
|
+
# This exception collects CheckError exceptions and mixes in Enumerable for
|
335
|
+
# further processing of them.
|
336
|
+
class CheckFailed < ProtocolError
|
337
|
+
include CheckError
|
338
|
+
|
339
|
+
def initialize(*errors)
|
340
|
+
@errors = errors
|
341
|
+
end
|
342
|
+
|
343
|
+
attr_reader :errors
|
344
|
+
|
345
|
+
# Return true, if this CheckFailed doesn't contain any errors (yet).
|
346
|
+
# Otherwise false is returned.
|
347
|
+
def empty?
|
348
|
+
errors.empty?
|
349
|
+
end
|
350
|
+
|
351
|
+
# Add +check_error+ to this CheckFailed instance.
|
352
|
+
def <<(check_error)
|
353
|
+
@errors << check_error
|
354
|
+
self
|
355
|
+
end
|
356
|
+
|
357
|
+
# Iterate over all errors of this CheckFailed instance and pass each one to
|
358
|
+
# +block+.
|
359
|
+
def each_error(&block)
|
360
|
+
errors.each(&block)
|
361
|
+
end
|
362
|
+
|
363
|
+
alias each each_error
|
364
|
+
include Enumerable
|
365
|
+
|
366
|
+
def to_s
|
367
|
+
errors * "|"
|
368
|
+
end
|
369
|
+
|
370
|
+
def inspect
|
371
|
+
"#<#{self.class.name}: #{errors.map { |e| e.inspect} * '|'}"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# The legal check modes, that influence the behaviour of the conform_to
|
376
|
+
# method in a class definition:
|
377
|
+
#
|
378
|
+
# - :error -> raises a CheckFailed exception, containing other
|
379
|
+
# CheckError exceptions.
|
380
|
+
# - :warning -> prints a warning to STDERR.
|
381
|
+
# - :none -> does nothing.
|
382
|
+
CHECK_MODES = [ :error, :warning, :none ]
|
383
|
+
|
384
|
+
# This class is a proxy that stores postcondition blocks, which are called
|
385
|
+
# after the result of the wrapped method was determined.
|
386
|
+
class Postcondition
|
387
|
+
instance_methods.each do |m|
|
388
|
+
m =~ /\A(__|instance_eval\Z|inspect\Z)/ or undef_method m
|
389
|
+
end
|
390
|
+
|
391
|
+
def initialize(object)
|
392
|
+
@object = object
|
393
|
+
@blocks = []
|
394
|
+
end
|
395
|
+
|
396
|
+
# This is the alternative result "keyword".
|
397
|
+
def __result__
|
398
|
+
@result
|
399
|
+
end
|
400
|
+
|
401
|
+
# This is the result "keyword" which can be used to query the result of
|
402
|
+
# wrapped method in a postcondition clause.
|
403
|
+
def result
|
404
|
+
if @object.respond_to? :result
|
405
|
+
warn "#{@object.class} already defines a result method, "\
|
406
|
+
"try __result__ instead"
|
407
|
+
@object.__send__(:result)
|
408
|
+
else
|
409
|
+
@result
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
# This is the "keyword" to be used instead of +self+ to refer to current
|
414
|
+
# object.
|
415
|
+
def myself
|
416
|
+
@object
|
417
|
+
end
|
418
|
+
|
419
|
+
# :stopdoc:
|
420
|
+
def __result__=(result)
|
421
|
+
@result = result
|
422
|
+
end
|
423
|
+
|
424
|
+
def __check__
|
425
|
+
@blocks.all? { |block| instance_eval(&block) }
|
426
|
+
end
|
427
|
+
|
428
|
+
def __add__(block)
|
429
|
+
@blocks << block
|
430
|
+
self
|
431
|
+
end
|
432
|
+
# :startdoc:
|
433
|
+
|
434
|
+
# Send all remaining messages to the object.
|
435
|
+
def method_missing(*a, &b)
|
436
|
+
@object.__send__(*a, &b)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# A Message consists of the name of the message (=method name), and the
|
441
|
+
# method argument's arity.
|
442
|
+
class Message
|
443
|
+
include Comparable
|
444
|
+
|
445
|
+
# Creates a Message instance named +name+, with the arity +arity+.
|
446
|
+
# If +arity+ is nil, the arity isn't checked during conformity tests.
|
447
|
+
def initialize(protocol, name, arity = nil, block_expected = false)
|
448
|
+
@protocol, @name, @arity, @block_expected =
|
449
|
+
protocol, name.to_s, arity, !!block_expected
|
450
|
+
end
|
451
|
+
|
452
|
+
# The protocol this message was defined in.
|
453
|
+
attr_reader :protocol
|
454
|
+
|
455
|
+
# Name of this message.
|
456
|
+
attr_reader :name
|
457
|
+
|
458
|
+
# Arity of this message = the number of arguments.
|
459
|
+
attr_accessor :arity
|
460
|
+
|
461
|
+
# Set to true if this message should expect a block.
|
462
|
+
def block_expected=(block_expected)
|
463
|
+
@block_expected = !!block_expected
|
464
|
+
end
|
465
|
+
|
466
|
+
# Returns true if this message is expected to include a block argument.
|
467
|
+
def block_expected?
|
468
|
+
@block_expected
|
469
|
+
end
|
470
|
+
|
471
|
+
# Message order is alphabetic name order.
|
472
|
+
def <=>(other)
|
473
|
+
name <=> other.name
|
474
|
+
end
|
475
|
+
|
476
|
+
# Returns true if this message equals the message +other+.
|
477
|
+
def ==(other)
|
478
|
+
name == other.name && arity == other.arity
|
479
|
+
end
|
480
|
+
|
481
|
+
# Returns the shortcut for this message of the form "methodname(arity)".
|
482
|
+
def shortcut
|
483
|
+
"#{name}(#{arity}#{block_expected? ? '&' : ''})"
|
484
|
+
end
|
485
|
+
|
486
|
+
# Return a string representation of this message, in the form
|
487
|
+
# Protocol#name(arity).
|
488
|
+
def to_s
|
489
|
+
"#{protocol.name}##{shortcut}"
|
490
|
+
end
|
491
|
+
|
492
|
+
# Concatenates a method signature as ruby code to the +result+ string and
|
493
|
+
# returns it.
|
494
|
+
def to_ruby(result = '')
|
495
|
+
if arity
|
496
|
+
result << " def #{name}("
|
497
|
+
args = if arity >= 0
|
498
|
+
(1..arity).map { |i| "x#{i}" }
|
499
|
+
else
|
500
|
+
(1..~arity).map { |i| "x#{i}" } << '*rest'
|
501
|
+
end
|
502
|
+
if block_expected?
|
503
|
+
args << '&block'
|
504
|
+
end
|
505
|
+
result << args * ', '
|
506
|
+
result << ") end\n"
|
507
|
+
else
|
508
|
+
result << " understand :#{name}\n"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# The class +klass+ is checked against this Message instance. A CheckError
|
513
|
+
# exception will called, if either a required method isn't found in the
|
514
|
+
# +klass+, or it doesn't have the required arity (if a fixed arity was
|
515
|
+
# demanded).
|
516
|
+
def check(object, checked)
|
517
|
+
check_message = object.is_a?(Class) ? :check_class : :check_object
|
518
|
+
if checked.key?(name)
|
519
|
+
true
|
520
|
+
else
|
521
|
+
checked[name] = __send__(check_message, object)
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
private
|
526
|
+
|
527
|
+
# Check class +klass+ against this Message instance, and raise a CheckError
|
528
|
+
# exception if necessary.
|
529
|
+
def check_class(klass)
|
530
|
+
unless klass.method_defined?(name)
|
531
|
+
raise NotImplementedErrorCheckError.new(self,
|
532
|
+
"method '#{name}' not implemented in #{klass}")
|
533
|
+
end
|
534
|
+
check_method = klass.instance_method(name)
|
535
|
+
if arity and (check_arity = check_method.arity) != arity
|
536
|
+
raise ArgumentErrorCheckError.new(self,
|
537
|
+
"wrong number of arguments for protocol"\
|
538
|
+
" in method '#{name}' (#{check_arity} for #{arity}) of #{klass}")
|
539
|
+
end
|
540
|
+
if block_expected?
|
541
|
+
modul = Utilities.find_method_module(name, klass.ancestors)
|
542
|
+
parser = MethodParser.new(modul, name)
|
543
|
+
parser.block_arg? or raise BlockCheckError.new(self,
|
544
|
+
"expected a block argument for #{klass}")
|
545
|
+
end
|
546
|
+
arity and wrap_method(klass)
|
547
|
+
true
|
548
|
+
end
|
549
|
+
|
550
|
+
# :stopdoc:
|
551
|
+
MyArray = Array.dup # Hack to make checking against Array possible.
|
552
|
+
# :startdoc:
|
553
|
+
|
554
|
+
def wrap_method(klass)
|
555
|
+
check_name = "__protocol_check_#{name}"
|
556
|
+
if klass.method_defined?(check_name)
|
557
|
+
inner_name = "__protocol_inner_#{name}"
|
558
|
+
unless klass.method_defined?(inner_name)
|
559
|
+
args =
|
560
|
+
if arity >= 0
|
561
|
+
(1..arity).map { |i| "x#{i}," }
|
562
|
+
else
|
563
|
+
(1..~arity).map { |i| "x#{i}," } << '*rest,'
|
564
|
+
end
|
565
|
+
wrapped_call = %{
|
566
|
+
alias_method :'#{inner_name}', :'#{name}'
|
567
|
+
|
568
|
+
def precondition
|
569
|
+
yield or
|
570
|
+
raise Protocol::PreconditionCheckError.new(
|
571
|
+
ObjectSpace._id2ref(#{__id__}),
|
572
|
+
"precondition failed for \#{self.class}")
|
573
|
+
end unless method_defined?(:precondition)
|
574
|
+
|
575
|
+
def postcondition(&block)
|
576
|
+
post_name = "__protocol_#{klass.__id__.abs}_postcondition__"
|
577
|
+
(Thread.current[post_name][-1] ||= Protocol::Postcondition.new(
|
578
|
+
self)).__add__ block
|
579
|
+
end unless method_defined?(:postcondition)
|
580
|
+
|
581
|
+
def #{name}(#{args} &block)
|
582
|
+
result = nil
|
583
|
+
post_name = "__protocol_#{klass.__id__.abs}_postcondition__"
|
584
|
+
(Thread.current[post_name] ||= MyArray.new) << nil
|
585
|
+
__send__('#{check_name}', #{args} &block)
|
586
|
+
if postcondition = Thread.current[post_name].last
|
587
|
+
begin
|
588
|
+
reraised = false
|
589
|
+
result = __send__('#{inner_name}', #{args} &block)
|
590
|
+
postcondition.__result__= result
|
591
|
+
rescue Protocol::PostconditionCheckError => e
|
592
|
+
reraised = true
|
593
|
+
raise e
|
594
|
+
ensure
|
595
|
+
unless reraised
|
596
|
+
postcondition.__check__ or
|
597
|
+
raise Protocol::PostconditionCheckError.new(
|
598
|
+
ObjectSpace._id2ref(#{__id__}),
|
599
|
+
"postcondition failed for \#{self.class}, result = " +
|
600
|
+
result.inspect)
|
601
|
+
end
|
602
|
+
end
|
603
|
+
else
|
604
|
+
result = __send__('#{inner_name}', #{args} &block)
|
605
|
+
end
|
606
|
+
result
|
607
|
+
rescue Protocol::CheckError => e
|
608
|
+
case ObjectSpace._id2ref(#{__id__}).protocol.mode
|
609
|
+
when :error
|
610
|
+
raise e
|
611
|
+
when :warning
|
612
|
+
warn e
|
613
|
+
end
|
614
|
+
ensure
|
615
|
+
Thread.current[post_name].pop
|
616
|
+
Thread.current[post_name].empty? and
|
617
|
+
Thread.current[post_name] = nil
|
618
|
+
end
|
619
|
+
}
|
620
|
+
klass.class_eval wrapped_call
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
# Check object +object+ against this Message instance, and raise a
|
626
|
+
# CheckError exception if necessary.
|
627
|
+
def check_object(object)
|
628
|
+
if !object.respond_to?(name)
|
629
|
+
raise NotImplementedErrorCheckError.new(self,
|
630
|
+
"method '#{name}' not responding in #{object}")
|
631
|
+
end
|
632
|
+
check_method = object.method(name)
|
633
|
+
if arity and (check_arity = check_method.arity) != arity
|
634
|
+
raise ArgumentErrorCheckError.new(self,
|
635
|
+
"wrong number of arguments for protocol"\
|
636
|
+
" in method '#{name}' (#{check_arity} for #{arity}) of #{object}")
|
637
|
+
end
|
638
|
+
if block_expected?
|
639
|
+
if object.singleton_methods(false).include?(name)
|
640
|
+
parser = MethodParser.new(object, name, true)
|
641
|
+
else
|
642
|
+
ancestors = object.class.ancestors
|
643
|
+
modul = Utilities.find_method_module(name, ancestors)
|
644
|
+
parser = MethodParser.new(modul, name)
|
645
|
+
end
|
646
|
+
parser.block_arg? or raise BlockCheckError.new(self,
|
647
|
+
"expected a block argument for #{object}:#{object.class}")
|
648
|
+
end
|
649
|
+
if arity and not protocol === object
|
650
|
+
object.extend protocol
|
651
|
+
wrap_method(class << object ; self ; end)
|
652
|
+
end
|
653
|
+
true
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
# This class encapsulates the protocol description, to check the classes
|
658
|
+
# against, if the Class#conform_to method is called with the protocol constant
|
659
|
+
# as an argument.
|
660
|
+
class Descriptor
|
661
|
+
# Creates a new Protocol::Descriptor object.
|
662
|
+
def initialize(protocol)
|
663
|
+
@protocol = protocol
|
664
|
+
@messages = {}
|
665
|
+
end
|
666
|
+
|
667
|
+
# Addes a new Protocol::Message instance to this Protocol::Descriptor
|
668
|
+
# object.
|
669
|
+
def add_message(message)
|
670
|
+
@messages.key?(message.name) and raise SpecificationError,
|
671
|
+
"A message named #{message.name} was already defined in #@protocol"
|
672
|
+
@messages[message.name] = message
|
673
|
+
end
|
674
|
+
|
675
|
+
# Return all the messages stored in this Descriptor instance.
|
676
|
+
def messages
|
677
|
+
@messages.values
|
678
|
+
end
|
679
|
+
|
680
|
+
# Returns a string representation of this Protocol::Descriptor object.
|
681
|
+
def inspect
|
682
|
+
"#<#{self.class}(#@protocol)>"
|
683
|
+
end
|
684
|
+
|
685
|
+
def to_s
|
686
|
+
messages * ', '
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
# Parse protocol method definition to derive a Message specification.
|
691
|
+
class MethodParser < SexpProcessor
|
692
|
+
# Create a new MethodParser instance for method +methodname+ of module
|
693
|
+
# +modul+. For eigenmethods set +eigenclass+ to true, otherwise bad things
|
694
|
+
# will happen.
|
695
|
+
def initialize(modul, methodname, eigenclass = false)
|
696
|
+
super()
|
697
|
+
self.strict = false
|
698
|
+
self.auto_shift_type = true
|
699
|
+
@complex = false
|
700
|
+
@block_arg = false
|
701
|
+
@first_defn = true
|
702
|
+
@first_block = true
|
703
|
+
@args = []
|
704
|
+
parsed = ParseTree.new.parse_tree_for_method(modul, methodname, eigenclass)
|
705
|
+
process parsed
|
706
|
+
end
|
707
|
+
|
708
|
+
# Process +exp+, but catch UnsupportedNodeError exceptions and ignore them.
|
709
|
+
def process(exp)
|
710
|
+
super
|
711
|
+
rescue UnsupportedNodeError => ignore
|
712
|
+
end
|
713
|
+
|
714
|
+
# Returns the names of the arguments of the parsed method.
|
715
|
+
attr_reader :args
|
716
|
+
|
717
|
+
# Returns the arity of the parsed method.
|
718
|
+
def arity
|
719
|
+
@args.size
|
720
|
+
end
|
721
|
+
|
722
|
+
# Return true if this protocol method is a complex method, which ought to
|
723
|
+
# be called for checking conformance to the protocol.
|
724
|
+
def complex?
|
725
|
+
@complex
|
726
|
+
end
|
727
|
+
|
728
|
+
# Return true if a block argument was detected.
|
729
|
+
def block_arg?
|
730
|
+
@block_arg
|
731
|
+
end
|
732
|
+
|
733
|
+
# Only consider first the first defn, skip inner method definitions.
|
734
|
+
def process_defn(exp)
|
735
|
+
if @first_defn
|
736
|
+
@first_defn = false
|
737
|
+
_name, scope = exp
|
738
|
+
process scope
|
739
|
+
end
|
740
|
+
exp.clear
|
741
|
+
s :dummy
|
742
|
+
end
|
743
|
+
|
744
|
+
# Remember the argument names in +exp+ in the args attribute.
|
745
|
+
def process_args(exp)
|
746
|
+
@args.replace exp
|
747
|
+
exp.clear
|
748
|
+
s :dummy
|
749
|
+
end
|
750
|
+
|
751
|
+
# Remember if we encounter a block argument or a yield keyword.
|
752
|
+
def process_block_arg(exp)
|
753
|
+
@block_arg = true
|
754
|
+
exp.clear
|
755
|
+
s :dummy
|
756
|
+
end
|
757
|
+
|
758
|
+
alias process_yield process_block_arg
|
759
|
+
|
760
|
+
# We only consider the first block in +exp+ (can there be more than one?),
|
761
|
+
# and then try to figure out, if this is a complex method or not. Continue
|
762
|
+
# processing the +exp+ tree after that.
|
763
|
+
def process_block(exp)
|
764
|
+
if @first_block
|
765
|
+
@first_block = false
|
766
|
+
@complex = exp[-1][0] != :nil rescue false
|
767
|
+
exp.each { |e| process e }
|
768
|
+
end
|
769
|
+
exp.clear
|
770
|
+
s :dummy
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
# A ProtocolModule object
|
775
|
+
class ProtocolModule < Module
|
776
|
+
# Creates an new ProtocolModule instance.
|
777
|
+
def initialize(&block)
|
778
|
+
@descriptor = Descriptor.new(self)
|
779
|
+
@mode = :error
|
780
|
+
module_eval(&block)
|
781
|
+
end
|
782
|
+
|
783
|
+
# The current check mode :none, :warning, or :error (the default).
|
784
|
+
attr_reader :mode
|
785
|
+
|
786
|
+
# Returns all the protocol descriptions to check against as an Array.
|
787
|
+
def descriptors
|
788
|
+
descriptors = []
|
789
|
+
protocols.each do |a|
|
790
|
+
descriptors << a.instance_variable_get('@descriptor')
|
791
|
+
end
|
792
|
+
descriptors
|
793
|
+
end
|
794
|
+
|
795
|
+
# Return self and all protocols included into self.
|
796
|
+
def protocols
|
797
|
+
ancestors.select { |modul| modul.is_a? ProtocolModule }
|
798
|
+
end
|
799
|
+
|
800
|
+
# Concatenates the protocol as Ruby code to the +result+ string and return
|
801
|
+
# it. At the moment this method only supports method signatures with
|
802
|
+
# generic argument names.
|
803
|
+
def to_ruby(result = '')
|
804
|
+
result << "#{name} = Protocol do"
|
805
|
+
first = true
|
806
|
+
if messages.empty?
|
807
|
+
result << "\n"
|
808
|
+
else
|
809
|
+
messages.each do |m|
|
810
|
+
result << "\n"
|
811
|
+
m.to_ruby(result)
|
812
|
+
end
|
813
|
+
end
|
814
|
+
result << "end\n"
|
815
|
+
end
|
816
|
+
|
817
|
+
# Returns all messages this protocol (plus the included protocols) consists
|
818
|
+
# of in alphabetic order. This method caches the computed result array. You
|
819
|
+
# have to call #reset_messages, if you want to recompute the array in the
|
820
|
+
# next call to #messages.
|
821
|
+
def messages
|
822
|
+
@messages and return @messages
|
823
|
+
@messages = []
|
824
|
+
seen = {}
|
825
|
+
descriptors.each do |d|
|
826
|
+
dm = d.messages
|
827
|
+
dm.delete_if do |m|
|
828
|
+
delete = seen[m.name]
|
829
|
+
seen[m.name] = true
|
830
|
+
delete
|
831
|
+
end
|
832
|
+
@messages.concat dm
|
833
|
+
end
|
834
|
+
@messages.sort!
|
835
|
+
@messages
|
836
|
+
end
|
837
|
+
|
838
|
+
alias to_a messages
|
839
|
+
|
840
|
+
# Reset the cached message array. Call this if you want to change the
|
841
|
+
# protocol dynamically after it was already used (= the #messages method
|
842
|
+
# was called).
|
843
|
+
def reset_messages
|
844
|
+
@messages = nil
|
845
|
+
self
|
846
|
+
end
|
847
|
+
|
848
|
+
# Returns true if it is required to understand the
|
849
|
+
def understand?(name, arity = nil)
|
850
|
+
name = name.to_s
|
851
|
+
!!find { |m| m.name == name && (!arity || m.arity == arity) }
|
852
|
+
end
|
853
|
+
|
854
|
+
# Return the Message object named +name+ or nil, if it doesn't exist.
|
855
|
+
def [](name)
|
856
|
+
name = name.to_s
|
857
|
+
find { |m| m.name == name }
|
858
|
+
end
|
859
|
+
|
860
|
+
# Return all message whose names matches pattern.
|
861
|
+
def grep(pattern)
|
862
|
+
select { |m| pattern === m.name }
|
863
|
+
end
|
864
|
+
|
865
|
+
# Iterate over all messages and yield to all of them.
|
866
|
+
def each_message(&block) # :yields: message
|
867
|
+
messages.each(&block)
|
868
|
+
self
|
869
|
+
end
|
870
|
+
alias each each_message
|
871
|
+
|
872
|
+
include Enumerable
|
873
|
+
|
874
|
+
# Returns a string representation of this protocol, that consists of the
|
875
|
+
# understood messages. This protocol
|
876
|
+
#
|
877
|
+
# FooProtocol = Protocol do
|
878
|
+
# def bar(x, y, &b) end
|
879
|
+
# def baz(x, y, z) end
|
880
|
+
# def foo(*rest) end
|
881
|
+
# end
|
882
|
+
#
|
883
|
+
# returns this string:
|
884
|
+
#
|
885
|
+
# FooProtocol#bar(2&), FooProtocol#baz(3), FooProtocol#foo(-1)
|
886
|
+
def to_s
|
887
|
+
messages * ', '
|
888
|
+
end
|
889
|
+
|
890
|
+
# Returns a short string representation of this protocol, that consists of
|
891
|
+
# the understood messages. This protocol
|
892
|
+
#
|
893
|
+
# FooProtocol = Protocol do
|
894
|
+
# def bar(x, y, &b) end
|
895
|
+
# def baz(x, y, z) end
|
896
|
+
# def foo(*rest) end
|
897
|
+
# end
|
898
|
+
#
|
899
|
+
# returns this string:
|
900
|
+
#
|
901
|
+
# #<FooProtocol: bar(2&), baz(3), foo(-1)>
|
902
|
+
def inspect
|
903
|
+
"#<#{name}: #{messages.map { |m| m.shortcut } * ', '}>"
|
904
|
+
end
|
905
|
+
|
906
|
+
# Check the conformity of +object+ recursively. This method returns either
|
907
|
+
# false OR true, if +mode+ is :none or :warning, or raises an
|
908
|
+
# CheckFailed, if +mode+ was :error.
|
909
|
+
def check(object, mode = @mode)
|
910
|
+
checked = {}
|
911
|
+
result = true
|
912
|
+
errors = CheckFailed.new
|
913
|
+
each do |message|
|
914
|
+
begin
|
915
|
+
message.check(object, checked)
|
916
|
+
rescue CheckError => e
|
917
|
+
case mode
|
918
|
+
when :error
|
919
|
+
errors << e
|
920
|
+
when :warning
|
921
|
+
warn e.to_s
|
922
|
+
result = false
|
923
|
+
when :none
|
924
|
+
result = false
|
925
|
+
end
|
926
|
+
end
|
927
|
+
end
|
928
|
+
raise errors unless errors.empty?
|
929
|
+
result
|
930
|
+
end
|
931
|
+
|
932
|
+
alias =~ check
|
933
|
+
|
934
|
+
# Return all messages for whick a check failed.
|
935
|
+
def check_failures(object)
|
936
|
+
check object
|
937
|
+
rescue CheckFailed => e
|
938
|
+
return e.errors.map { |e| e.protocol_message }
|
939
|
+
end
|
940
|
+
|
941
|
+
# This callback is called, when a module, that was extended with Protocol,
|
942
|
+
# is included (via Modul#include/via Class#conform_to) into some other
|
943
|
+
# module/class.
|
944
|
+
# If +modul+ is a Class, all protocol descriptions of the inheritance tree
|
945
|
+
# are collected and the given class is checked for conformance to the
|
946
|
+
# protocol. +modul+ isn't a Class and only a Module, it is extended with
|
947
|
+
# the Protocol
|
948
|
+
# module.
|
949
|
+
def included(modul)
|
950
|
+
super
|
951
|
+
if modul.is_a? Class and @mode == :error or @mode == :warning
|
952
|
+
$DEBUG and warn "#{name} is checking class #{modul}"
|
953
|
+
check modul
|
954
|
+
end
|
955
|
+
end
|
956
|
+
|
957
|
+
# Sets the check mode to +id+. +id+ should be one of :none, :warning, or
|
958
|
+
# :error. The mode to use while doing a conformity check is always the root
|
959
|
+
# module, that is, the modes of the included modules aren't important for
|
960
|
+
# the check.
|
961
|
+
def check_failure(mode)
|
962
|
+
CHECK_MODES.include?(mode) or
|
963
|
+
raise ArgumentError, "illegal check mode #{mode}"
|
964
|
+
@mode = mode
|
965
|
+
end
|
966
|
+
|
967
|
+
# This method defines one of the messages, the protocol in question
|
968
|
+
# consists of: The messages which the class, that conforms to this
|
969
|
+
# protocol, should understand and respond to. An example shows best
|
970
|
+
# which +message+descriptions_ are allowed:
|
971
|
+
#
|
972
|
+
# MyProtocol = Protocol do
|
973
|
+
# understand :bar # conforming class must respond to :bar
|
974
|
+
# understand :baz, 3 # c. c. must respond to :baz with 3 args.
|
975
|
+
# understand :foo, -1 # c. c. must respond to :foo, any number of args.
|
976
|
+
# understand :quux, 0, true # c. c. must respond to :quux, no args + block.
|
977
|
+
# understand :quux1, 1, true # c. c. must respond to :quux, 1 arg + block.
|
978
|
+
# end
|
979
|
+
def understand(methodname, arity = nil, block_expected = false)
|
980
|
+
m = Message.new(self, methodname.to_s, arity, block_expected)
|
981
|
+
@descriptor.add_message(m)
|
982
|
+
self
|
983
|
+
end
|
984
|
+
|
985
|
+
def parse_instance_method_signature(modul, methodname)
|
986
|
+
methodname = methodname.to_s
|
987
|
+
method = modul.instance_method(methodname)
|
988
|
+
real_module = Utilities.find_method_module(methodname, modul.ancestors)
|
989
|
+
parser = MethodParser.new(real_module, methodname)
|
990
|
+
Message.new(self, methodname, method.arity, parser.block_arg?)
|
991
|
+
end
|
992
|
+
private :parse_instance_method_signature
|
993
|
+
|
994
|
+
# Inherit a method signature from an instance method named +methodname+ of
|
995
|
+
# +modul+. This means that this protocol should understand these instance
|
996
|
+
# methods with their arity and block expectation. Note that automatic
|
997
|
+
# detection of blocks does not work for Ruby methods defined in C. You can
|
998
|
+
# set the +block_expected+ argument if you want to do this manually.
|
999
|
+
def inherit(modul, methodname, block_expected = nil)
|
1000
|
+
Module === modul or
|
1001
|
+
raise TypeError, "expected Module not #{modul.class} as modul argument"
|
1002
|
+
methodnames = methodname.respond_to?(:to_ary) ?
|
1003
|
+
methodname.to_ary :
|
1004
|
+
[ methodname ]
|
1005
|
+
methodnames.each do |methodname|
|
1006
|
+
m = parse_instance_method_signature(modul, methodname)
|
1007
|
+
block_expected and m.block_expected = block_expected
|
1008
|
+
@descriptor.add_message m
|
1009
|
+
end
|
1010
|
+
self
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
# Switch to implementation mode. Defined methods are added to the
|
1014
|
+
# ProtocolModule as instance methods.
|
1015
|
+
def implementation
|
1016
|
+
@implementation = true
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Return true, if the ProtocolModule is currently in implementation mode.
|
1020
|
+
# Otherwise return false.
|
1021
|
+
def implementation?
|
1022
|
+
!!@implementation
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
# Switch to specification mode. Defined methods are added to the protocol
|
1026
|
+
# description in order to be checked against in later conformance tests.
|
1027
|
+
def specification
|
1028
|
+
@implementation = false
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
# Return true, if the ProtocolModule is currently in specification mode.
|
1032
|
+
# Otherwise return false.
|
1033
|
+
def specification?
|
1034
|
+
!@implementation
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
# Capture all added methods and either leave the implementation in place or
|
1038
|
+
# add them to the protocol description.
|
1039
|
+
def method_added(methodname)
|
1040
|
+
methodname = methodname.to_s
|
1041
|
+
if specification? and methodname !~ /^__protocol_check_/
|
1042
|
+
protocol_check = instance_method(methodname)
|
1043
|
+
parser = MethodParser.new(self, methodname)
|
1044
|
+
if parser.complex?
|
1045
|
+
define_method("__protocol_check_#{methodname}", protocol_check)
|
1046
|
+
understand methodname, protocol_check.arity, parser.block_arg?
|
1047
|
+
else
|
1048
|
+
understand methodname, protocol_check.arity, parser.block_arg?
|
1049
|
+
end
|
1050
|
+
remove_method methodname
|
1051
|
+
else
|
1052
|
+
super
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
# A module for some Utility methods.
|
1058
|
+
module Utilities
|
1059
|
+
module_function
|
1060
|
+
|
1061
|
+
# This Method tries to find the first module that implements the method
|
1062
|
+
# named +methodname+ in the array of +ancestors+. If this fails nil is
|
1063
|
+
# returned.
|
1064
|
+
def find_method_module(methodname, ancestors) ancestors.each do |a|
|
1065
|
+
return a if a.instance_methods(false).include? methodname
|
1066
|
+
end
|
1067
|
+
nil
|
1068
|
+
end
|
1069
|
+
end
|
1070
|
+
end
|