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