protocol 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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