protocol 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +11 -0
- data/Gemfile +7 -0
- data/{doc-main.txt → README.rdoc} +12 -12
- data/Rakefile +29 -89
- data/VERSION +1 -1
- data/benchmarks/data/.keep +0 -0
- data/benchmarks/method_parser.rb +3 -0
- data/examples/assignments.rb +59 -0
- data/examples/game.rb +1 -1
- data/examples/hello_world_patternitis.rb +1 -1
- data/examples/locking.rb +1 -1
- data/lib/protocol/descriptor.rb +34 -0
- data/lib/protocol/errors.rb +95 -0
- data/lib/protocol/message.rb +219 -0
- data/lib/protocol/method_parser/ruby_parser.rb +5 -2
- data/lib/protocol/post_condition.rb +57 -0
- data/lib/protocol/protocol_module.rb +291 -0
- data/lib/protocol/utilities.rb +21 -0
- data/lib/protocol/version.rb +1 -1
- data/lib/protocol/xt.rb +41 -0
- data/lib/protocol.rb +8 -741
- data/protocol.gemspec +32 -22
- data/tests/{test_protocol_method_parser.rb → protocol_method_parser_test.rb} +2 -3
- data/tests/{test_protocol.rb → protocol_test.rb} +106 -107
- data/tests/test_helper.rb +8 -0
- metadata +105 -65
- data/lib/protocol/method_parser/parse_tree.rb +0 -124
- data/make_doc.rb +0 -5
data/lib/protocol.rb
CHANGED
@@ -1,139 +1,14 @@
|
|
1
1
|
require 'protocol/version'
|
2
2
|
|
3
3
|
module Protocol
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
# This is especially useful, if check_failure in the protocol is set to
|
14
|
-
# :none or :warning, and conformance of a class to a protocol should be
|
15
|
-
# checked later in runtime.
|
16
|
-
def conform_to?(protocol)
|
17
|
-
protocol.check(self, :none)
|
18
|
-
end
|
19
|
-
|
20
|
-
# Define a protocol configured by +block+. Look at the methods of
|
21
|
-
# ProtocolModule to get an idea on how to do that.
|
22
|
-
def Protocol(&block)
|
23
|
-
ProtocolModule.new(&block)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class ::Class
|
28
|
-
# This method should be called at the end of a class definition, that is,
|
29
|
-
# after all methods have been added to the class. The conformance to the
|
30
|
-
# protocol of the class given as the argument is checked. See
|
31
|
-
# Protocol::CHECK_MODES for the consequences of failure of this check.
|
32
|
-
alias conform_to include
|
33
|
-
|
34
|
-
# Returns true if this class conforms to +protocol+, otherwise false.
|
35
|
-
#
|
36
|
-
# This is especially useful, if check_failure in the protocol is set to
|
37
|
-
# :none or :warning, and conformance of a class to a protocol should be
|
38
|
-
# checked later in runtime.
|
39
|
-
def conform_to?(protocol)
|
40
|
-
protocol.check(self, :none)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# The base class for protocol errors.
|
45
|
-
class ProtocolError < StandardError; end
|
46
|
-
|
47
|
-
# This exception is raise if an error was encountered while defining a
|
48
|
-
# protocol specification.
|
49
|
-
class SpecificationError < ProtocolError; end
|
50
|
-
|
51
|
-
# Marker module for all exceptions related to check errors.
|
52
|
-
module CheckError; end
|
53
|
-
|
54
|
-
# If a protocol check failes this exception is raised.
|
55
|
-
class BaseCheckError < ProtocolError
|
56
|
-
include CheckError
|
57
|
-
|
58
|
-
def initialize(protocol_message, message)
|
59
|
-
super(message)
|
60
|
-
@protocol_message = protocol_message
|
61
|
-
end
|
62
|
-
|
63
|
-
# Returns the Protocol::Message object, that caused this CheckError to be
|
64
|
-
# raised.
|
65
|
-
attr_reader :protocol_message
|
66
|
-
|
67
|
-
def to_s
|
68
|
-
"#{protocol_message}: #{super}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def inspect
|
72
|
-
"#<#{self.class.name}: #{to_s}>"
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# This exception is raised if a method was not implented in a class, that was
|
77
|
-
# required to conform to the checked protocol.
|
78
|
-
class NotImplementedErrorCheckError < BaseCheckError; end
|
79
|
-
|
80
|
-
# This exception is raised if a method implented in a class didn't have the
|
81
|
-
# required arity, that was required to conform to the checked protocol.
|
82
|
-
class ArgumentErrorCheckError < BaseCheckError; end
|
83
|
-
|
84
|
-
# This exception is raised if a method implented in a class didn't have the
|
85
|
-
# expected block argument, that was required to conform to the checked
|
86
|
-
# protocol.
|
87
|
-
class BlockCheckError < BaseCheckError; end
|
88
|
-
|
89
|
-
# This exception is raised if a precondition check failed (the yielded
|
90
|
-
# block returned a non-true value) in a protocol description.
|
91
|
-
class PreconditionCheckError < BaseCheckError; end
|
92
|
-
|
93
|
-
# This exception is raised if a postcondition check failed (the yielded block
|
94
|
-
# returned a non-true value) in a protocol description.
|
95
|
-
class PostconditionCheckError < BaseCheckError; end
|
96
|
-
|
97
|
-
# This exception collects CheckError exceptions and mixes in Enumerable for
|
98
|
-
# further processing of them.
|
99
|
-
class CheckFailed < ProtocolError
|
100
|
-
include CheckError
|
101
|
-
|
102
|
-
def initialize(*errors)
|
103
|
-
@errors = errors
|
104
|
-
end
|
105
|
-
|
106
|
-
attr_reader :errors
|
107
|
-
|
108
|
-
# Return true, if this CheckFailed doesn't contain any errors (yet).
|
109
|
-
# Otherwise false is returned.
|
110
|
-
def empty?
|
111
|
-
errors.empty?
|
112
|
-
end
|
113
|
-
|
114
|
-
# Add +check_error+ to this CheckFailed instance.
|
115
|
-
def <<(check_error)
|
116
|
-
@errors << check_error
|
117
|
-
self
|
118
|
-
end
|
119
|
-
|
120
|
-
# Iterate over all errors of this CheckFailed instance and pass each one to
|
121
|
-
# +block+.
|
122
|
-
def each_error(&block)
|
123
|
-
errors.each(&block)
|
124
|
-
end
|
125
|
-
|
126
|
-
alias each each_error
|
127
|
-
include Enumerable
|
128
|
-
|
129
|
-
def to_s
|
130
|
-
errors * "|"
|
131
|
-
end
|
132
|
-
|
133
|
-
def inspect
|
134
|
-
"#<#{self.class.name}: #{errors.map { |e| e.inspect} * '|'}"
|
135
|
-
end
|
136
|
-
end
|
4
|
+
require 'protocol/method_parser/ruby_parser'
|
5
|
+
require 'protocol/utilities'
|
6
|
+
require 'protocol/protocol_module'
|
7
|
+
require 'protocol/post_condition'
|
8
|
+
require 'protocol/descriptor'
|
9
|
+
require 'protocol/message'
|
10
|
+
require 'protocol/errors'
|
11
|
+
require 'protocol/xt'
|
137
12
|
|
138
13
|
# The legal check modes, that influence the behaviour of the conform_to
|
139
14
|
# method in a class definition:
|
@@ -143,612 +18,4 @@ module Protocol
|
|
143
18
|
# - :warning -> prints a warning to STDERR.
|
144
19
|
# - :none -> does nothing.
|
145
20
|
CHECK_MODES = [ :error, :warning, :none ]
|
146
|
-
|
147
|
-
# This class is a proxy that stores postcondition blocks, which are called
|
148
|
-
# after the result of the wrapped method was determined.
|
149
|
-
class Postcondition
|
150
|
-
instance_methods.each do |m|
|
151
|
-
m.to_s =~ /\A(__|object_id|instance_eval\Z|inspect\Z)/ or undef_method m
|
152
|
-
end
|
153
|
-
|
154
|
-
def initialize(object)
|
155
|
-
@object = object
|
156
|
-
@blocks = []
|
157
|
-
end
|
158
|
-
|
159
|
-
# This is the alternative result "keyword".
|
160
|
-
def __result__
|
161
|
-
@result
|
162
|
-
end
|
163
|
-
|
164
|
-
# This is the result "keyword" which can be used to query the result of
|
165
|
-
# wrapped method in a postcondition clause.
|
166
|
-
def result
|
167
|
-
if @object.respond_to? :result
|
168
|
-
warn "#{@object.class} already defines a result method, "\
|
169
|
-
"try __result__ instead"
|
170
|
-
@object.__send__(:result)
|
171
|
-
else
|
172
|
-
@result
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
# This is the "keyword" to be used instead of +self+ to refer to current
|
177
|
-
# object.
|
178
|
-
def myself
|
179
|
-
@object
|
180
|
-
end
|
181
|
-
|
182
|
-
# :stopdoc:
|
183
|
-
def __result__=(result)
|
184
|
-
@result = result
|
185
|
-
end
|
186
|
-
|
187
|
-
def __check__
|
188
|
-
@blocks.all? { |block| instance_eval(&block) }
|
189
|
-
end
|
190
|
-
|
191
|
-
def __add__(block)
|
192
|
-
@blocks << block
|
193
|
-
self
|
194
|
-
end
|
195
|
-
# :startdoc:
|
196
|
-
|
197
|
-
# Send all remaining messages to the object.
|
198
|
-
def method_missing(*a, &b)
|
199
|
-
@object.__send__(*a, &b)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# A Message consists of the name of the message (=method name), and the
|
204
|
-
# method argument's arity.
|
205
|
-
class Message
|
206
|
-
include Comparable
|
207
|
-
|
208
|
-
# Creates a Message instance named +name+, with the arity +arity+.
|
209
|
-
# If +arity+ is nil, the arity isn't checked during conformity tests.
|
210
|
-
def initialize(protocol, name, arity = nil, block_expected = false)
|
211
|
-
name = name.to_s
|
212
|
-
@protocol, @name, @arity, @block_expected =
|
213
|
-
protocol, name, arity, !!block_expected
|
214
|
-
end
|
215
|
-
|
216
|
-
# The protocol this message was defined in.
|
217
|
-
attr_reader :protocol
|
218
|
-
|
219
|
-
# Name of this message.
|
220
|
-
attr_reader :name
|
221
|
-
|
222
|
-
# Arity of this message = the number of arguments.
|
223
|
-
attr_accessor :arity
|
224
|
-
|
225
|
-
# Set to true if this message should expect a block.
|
226
|
-
def block_expected=(block_expected)
|
227
|
-
@block_expected = !!block_expected
|
228
|
-
end
|
229
|
-
|
230
|
-
# Returns true if this message is expected to include a block argument.
|
231
|
-
def block_expected?
|
232
|
-
@block_expected
|
233
|
-
end
|
234
|
-
|
235
|
-
# Message order is alphabetic name order.
|
236
|
-
def <=>(other)
|
237
|
-
name <=> other.name
|
238
|
-
end
|
239
|
-
|
240
|
-
# Returns true if this message equals the message +other+.
|
241
|
-
def ==(other)
|
242
|
-
name == other.name && arity == other.arity
|
243
|
-
end
|
244
|
-
|
245
|
-
# Returns the shortcut for this message of the form "methodname(arity)".
|
246
|
-
def shortcut
|
247
|
-
"#{name}(#{arity}#{block_expected? ? '&' : ''})"
|
248
|
-
end
|
249
|
-
|
250
|
-
# Return a string representation of this message, in the form
|
251
|
-
# Protocol#name(arity).
|
252
|
-
def to_s
|
253
|
-
"#{protocol.name}##{shortcut}"
|
254
|
-
end
|
255
|
-
|
256
|
-
# Concatenates a method signature as ruby code to the +result+ string and
|
257
|
-
# returns it.
|
258
|
-
def to_ruby(result = '')
|
259
|
-
if arity
|
260
|
-
result << " def #{name}("
|
261
|
-
args = if arity >= 0
|
262
|
-
(1..arity).map { |i| "x#{i}" }
|
263
|
-
else
|
264
|
-
(1..~arity).map { |i| "x#{i}" } << '*rest'
|
265
|
-
end
|
266
|
-
if block_expected?
|
267
|
-
args << '&block'
|
268
|
-
end
|
269
|
-
result << args * ', '
|
270
|
-
result << ") end\n"
|
271
|
-
else
|
272
|
-
result << " understand :#{name}\n"
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
# The class +klass+ is checked against this Message instance. A CheckError
|
277
|
-
# exception will called, if either a required method isn't found in the
|
278
|
-
# +klass+, or it doesn't have the required arity (if a fixed arity was
|
279
|
-
# demanded).
|
280
|
-
def check(object, checked)
|
281
|
-
check_message = object.is_a?(Class) ? :check_class : :check_object
|
282
|
-
if checked.key?(name)
|
283
|
-
true
|
284
|
-
else
|
285
|
-
checked[name] = __send__(check_message, object)
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
private
|
290
|
-
|
291
|
-
# Check class +klass+ against this Message instance, and raise a CheckError
|
292
|
-
# exception if necessary.
|
293
|
-
def check_class(klass)
|
294
|
-
unless klass.method_defined?(name)
|
295
|
-
raise NotImplementedErrorCheckError.new(self,
|
296
|
-
"method '#{name}' not implemented in #{klass}")
|
297
|
-
end
|
298
|
-
check_method = klass.instance_method(name)
|
299
|
-
if arity and (check_arity = check_method.arity) != arity
|
300
|
-
raise ArgumentErrorCheckError.new(self,
|
301
|
-
"wrong number of arguments for protocol"\
|
302
|
-
" in method '#{name}' (#{check_arity} for #{arity}) of #{klass}")
|
303
|
-
end
|
304
|
-
if block_expected?
|
305
|
-
modul = Utilities.find_method_module(name, klass.ancestors)
|
306
|
-
parser = MethodParser.new(modul, name)
|
307
|
-
parser.block_arg? or raise BlockCheckError.new(self,
|
308
|
-
"expected a block argument for #{klass}")
|
309
|
-
end
|
310
|
-
arity and wrap_method(klass)
|
311
|
-
true
|
312
|
-
end
|
313
|
-
|
314
|
-
# :stopdoc:
|
315
|
-
MyArray = Array.dup # Hack to make checking against Array possible.
|
316
|
-
# :startdoc:
|
317
|
-
|
318
|
-
def wrap_method(klass)
|
319
|
-
check_name = "__protocol_check_#{name}"
|
320
|
-
if klass.method_defined?(check_name)
|
321
|
-
inner_name = "__protocol_inner_#{name}"
|
322
|
-
unless klass.method_defined?(inner_name)
|
323
|
-
args =
|
324
|
-
if arity >= 0
|
325
|
-
(1..arity).map { |i| "x#{i}," }
|
326
|
-
else
|
327
|
-
(1..~arity).map { |i| "x#{i}," } << '*rest,'
|
328
|
-
end.join
|
329
|
-
wrapped_call = %{
|
330
|
-
alias_method :'#{inner_name}', :'#{name}'
|
331
|
-
|
332
|
-
def precondition
|
333
|
-
yield or
|
334
|
-
raise Protocol::PreconditionCheckError.new(
|
335
|
-
ObjectSpace._id2ref(#{__id__}),
|
336
|
-
"precondition failed for \#{self.class}")
|
337
|
-
end unless method_defined?(:precondition)
|
338
|
-
|
339
|
-
def postcondition(&block)
|
340
|
-
post_name = "__protocol_#{klass.__id__.abs}_postcondition__"
|
341
|
-
(Thread.current[post_name][-1] ||= Protocol::Postcondition.new(
|
342
|
-
self)).__add__ block
|
343
|
-
end unless method_defined?(:postcondition)
|
344
|
-
|
345
|
-
def #{name}(#{args} &block)
|
346
|
-
result = nil
|
347
|
-
post_name = "__protocol_#{klass.__id__.abs}_postcondition__"
|
348
|
-
(Thread.current[post_name] ||= MyArray.new) << nil
|
349
|
-
__send__('#{check_name}', #{args} &block)
|
350
|
-
if postcondition = Thread.current[post_name].last
|
351
|
-
begin
|
352
|
-
reraised = false
|
353
|
-
result = __send__('#{inner_name}', #{args} &block)
|
354
|
-
postcondition.__result__= result
|
355
|
-
rescue Protocol::PostconditionCheckError => e
|
356
|
-
reraised = true
|
357
|
-
raise e
|
358
|
-
ensure
|
359
|
-
unless reraised
|
360
|
-
postcondition.__check__ or
|
361
|
-
raise Protocol::PostconditionCheckError.new(
|
362
|
-
ObjectSpace._id2ref(#{__id__}),
|
363
|
-
"postcondition failed for \#{self.class}, result = " +
|
364
|
-
result.inspect)
|
365
|
-
end
|
366
|
-
end
|
367
|
-
else
|
368
|
-
result = __send__('#{inner_name}', #{args} &block)
|
369
|
-
end
|
370
|
-
result
|
371
|
-
rescue Protocol::CheckError => e
|
372
|
-
case ObjectSpace._id2ref(#{__id__}).protocol.mode
|
373
|
-
when :error
|
374
|
-
raise e
|
375
|
-
when :warning
|
376
|
-
warn e
|
377
|
-
end
|
378
|
-
ensure
|
379
|
-
Thread.current[post_name].pop
|
380
|
-
Thread.current[post_name].empty? and
|
381
|
-
Thread.current[post_name] = nil
|
382
|
-
end
|
383
|
-
}
|
384
|
-
klass.class_eval wrapped_call
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
# Check object +object+ against this Message instance, and raise a
|
390
|
-
# CheckError exception if necessary.
|
391
|
-
def check_object(object)
|
392
|
-
if !object.respond_to?(name)
|
393
|
-
raise NotImplementedErrorCheckError.new(self,
|
394
|
-
"method '#{name}' not responding in #{object}")
|
395
|
-
end
|
396
|
-
check_method = object.method(name)
|
397
|
-
if arity and (check_arity = check_method.arity) != arity
|
398
|
-
raise ArgumentErrorCheckError.new(self,
|
399
|
-
"wrong number of arguments for protocol"\
|
400
|
-
" in method '#{name}' (#{check_arity} for #{arity}) of #{object}")
|
401
|
-
end
|
402
|
-
if block_expected?
|
403
|
-
if object.singleton_methods(false).map { |m| m.to_s } .include?(name)
|
404
|
-
parser = MethodParser.new(object, name, true)
|
405
|
-
else
|
406
|
-
ancestors = object.class.ancestors
|
407
|
-
modul = Utilities.find_method_module(name, ancestors)
|
408
|
-
parser = MethodParser.new(modul, name)
|
409
|
-
end
|
410
|
-
parser.block_arg? or raise BlockCheckError.new(self,
|
411
|
-
"expected a block argument for #{object}:#{object.class}")
|
412
|
-
end
|
413
|
-
if arity and not protocol === object
|
414
|
-
object.extend protocol
|
415
|
-
wrap_method(class << object ; self ; end)
|
416
|
-
end
|
417
|
-
true
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
# This class encapsulates the protocol description, to check the classes
|
422
|
-
# against, if the Class#conform_to method is called with the protocol constant
|
423
|
-
# as an argument.
|
424
|
-
class Descriptor
|
425
|
-
# Creates a new Protocol::Descriptor object.
|
426
|
-
def initialize(protocol)
|
427
|
-
@protocol = protocol
|
428
|
-
@messages = {}
|
429
|
-
end
|
430
|
-
|
431
|
-
# Addes a new Protocol::Message instance to this Protocol::Descriptor
|
432
|
-
# object.
|
433
|
-
def add_message(message)
|
434
|
-
@messages.key?(message.name) and raise SpecificationError,
|
435
|
-
"A message named #{message.name} was already defined in #@protocol"
|
436
|
-
@messages[message.name] = message
|
437
|
-
end
|
438
|
-
|
439
|
-
# Return all the messages stored in this Descriptor instance.
|
440
|
-
def messages
|
441
|
-
@messages.values
|
442
|
-
end
|
443
|
-
|
444
|
-
# Returns a string representation of this Protocol::Descriptor object.
|
445
|
-
def inspect
|
446
|
-
"#<#{self.class}(#@protocol)>"
|
447
|
-
end
|
448
|
-
|
449
|
-
def to_s
|
450
|
-
messages * ', '
|
451
|
-
end
|
452
|
-
end
|
453
|
-
|
454
|
-
# A ProtocolModule object
|
455
|
-
class ProtocolModule < Module
|
456
|
-
# Creates an new ProtocolModule instance.
|
457
|
-
def initialize(&block)
|
458
|
-
@descriptor = Descriptor.new(self)
|
459
|
-
@mode = :error
|
460
|
-
module_eval(&block)
|
461
|
-
end
|
462
|
-
|
463
|
-
# The current check mode :none, :warning, or :error (the default).
|
464
|
-
attr_reader :mode
|
465
|
-
|
466
|
-
# Returns all the protocol descriptions to check against as an Array.
|
467
|
-
def descriptors
|
468
|
-
descriptors = []
|
469
|
-
protocols.each do |a|
|
470
|
-
descriptors << a.instance_variable_get(:@descriptor)
|
471
|
-
end
|
472
|
-
descriptors
|
473
|
-
end
|
474
|
-
|
475
|
-
# Return self and all protocols included into self.
|
476
|
-
def protocols
|
477
|
-
ancestors.select { |modul| modul.is_a? ProtocolModule }
|
478
|
-
end
|
479
|
-
|
480
|
-
# Concatenates the protocol as Ruby code to the +result+ string and return
|
481
|
-
# it. At the moment this method only supports method signatures with
|
482
|
-
# generic argument names.
|
483
|
-
def to_ruby(result = '')
|
484
|
-
result << "#{name} = Protocol do"
|
485
|
-
first = true
|
486
|
-
if messages.empty?
|
487
|
-
result << "\n"
|
488
|
-
else
|
489
|
-
messages.each do |m|
|
490
|
-
result << "\n"
|
491
|
-
m.to_ruby(result)
|
492
|
-
end
|
493
|
-
end
|
494
|
-
result << "end\n"
|
495
|
-
end
|
496
|
-
|
497
|
-
# Returns all messages this protocol (plus the included protocols) consists
|
498
|
-
# of in alphabetic order. This method caches the computed result array. You
|
499
|
-
# have to call #reset_messages, if you want to recompute the array in the
|
500
|
-
# next call to #messages.
|
501
|
-
def messages
|
502
|
-
result = []
|
503
|
-
seen = {}
|
504
|
-
descriptors.each do |d|
|
505
|
-
dm = d.messages
|
506
|
-
dm.delete_if do |m|
|
507
|
-
delete = seen[m.name]
|
508
|
-
seen[m.name] = true
|
509
|
-
delete
|
510
|
-
end
|
511
|
-
result.concat dm
|
512
|
-
end
|
513
|
-
result.sort!
|
514
|
-
end
|
515
|
-
|
516
|
-
alias to_a messages
|
517
|
-
|
518
|
-
# Reset the cached message array. Call this if you want to change the
|
519
|
-
# protocol dynamically after it was already used (= the #messages method
|
520
|
-
# was called).
|
521
|
-
def reset_messages
|
522
|
-
@messages = nil
|
523
|
-
self
|
524
|
-
end
|
525
|
-
|
526
|
-
# Returns true if it is required to understand the
|
527
|
-
def understand?(name, arity = nil)
|
528
|
-
name = name.to_s
|
529
|
-
!!find { |m| m.name == name && (!arity || m.arity == arity) }
|
530
|
-
end
|
531
|
-
|
532
|
-
# Return the Message object named +name+ or nil, if it doesn't exist.
|
533
|
-
def [](name)
|
534
|
-
name = name.to_s
|
535
|
-
find { |m| m.name == name }
|
536
|
-
end
|
537
|
-
|
538
|
-
# Return all message whose names matches pattern.
|
539
|
-
def grep(pattern)
|
540
|
-
select { |m| pattern === m.name }
|
541
|
-
end
|
542
|
-
|
543
|
-
# Iterate over all messages and yield to all of them.
|
544
|
-
def each_message(&block) # :yields: message
|
545
|
-
messages.each(&block)
|
546
|
-
self
|
547
|
-
end
|
548
|
-
alias each each_message
|
549
|
-
|
550
|
-
include Enumerable
|
551
|
-
|
552
|
-
# Returns a string representation of this protocol, that consists of the
|
553
|
-
# understood messages. This protocol
|
554
|
-
#
|
555
|
-
# FooProtocol = Protocol do
|
556
|
-
# def bar(x, y, &b) end
|
557
|
-
# def baz(x, y, z) end
|
558
|
-
# def foo(*rest) end
|
559
|
-
# end
|
560
|
-
#
|
561
|
-
# returns this string:
|
562
|
-
#
|
563
|
-
# FooProtocol#bar(2&), FooProtocol#baz(3), FooProtocol#foo(-1)
|
564
|
-
def to_s
|
565
|
-
messages * ', '
|
566
|
-
end
|
567
|
-
|
568
|
-
# Returns a short string representation of this protocol, that consists of
|
569
|
-
# the understood messages. This protocol
|
570
|
-
#
|
571
|
-
# FooProtocol = Protocol do
|
572
|
-
# def bar(x, y, &b) end
|
573
|
-
# def baz(x, y, z) end
|
574
|
-
# def foo(*rest) end
|
575
|
-
# end
|
576
|
-
#
|
577
|
-
# returns this string:
|
578
|
-
#
|
579
|
-
# #<FooProtocol: bar(2&), baz(3), foo(-1)>
|
580
|
-
def inspect
|
581
|
-
"#<#{name}: #{messages.map { |m| m.shortcut } * ', '}>"
|
582
|
-
end
|
583
|
-
|
584
|
-
# Check the conformity of +object+ recursively. This method returns either
|
585
|
-
# false OR true, if +mode+ is :none or :warning, or raises an
|
586
|
-
# CheckFailed, if +mode+ was :error.
|
587
|
-
def check(object, mode = @mode)
|
588
|
-
checked = {}
|
589
|
-
result = true
|
590
|
-
errors = CheckFailed.new
|
591
|
-
each do |message|
|
592
|
-
begin
|
593
|
-
message.check(object, checked)
|
594
|
-
rescue CheckError => e
|
595
|
-
case mode
|
596
|
-
when :error
|
597
|
-
errors << e
|
598
|
-
when :warning
|
599
|
-
warn e.to_s
|
600
|
-
result = false
|
601
|
-
when :none
|
602
|
-
result = false
|
603
|
-
end
|
604
|
-
end
|
605
|
-
end
|
606
|
-
raise errors unless errors.empty?
|
607
|
-
result
|
608
|
-
end
|
609
|
-
|
610
|
-
alias =~ check
|
611
|
-
|
612
|
-
# Return all messages for whick a check failed.
|
613
|
-
def check_failures(object)
|
614
|
-
check object
|
615
|
-
rescue CheckFailed => e
|
616
|
-
return e.errors.map { |e| e.protocol_message }
|
617
|
-
end
|
618
|
-
|
619
|
-
# This callback is called, when a module, that was extended with Protocol,
|
620
|
-
# is included (via Modul#include/via Class#conform_to) into some other
|
621
|
-
# module/class.
|
622
|
-
# If +modul+ is a Class, all protocol descriptions of the inheritance tree
|
623
|
-
# are collected and the given class is checked for conformance to the
|
624
|
-
# protocol. +modul+ isn't a Class and only a Module, it is extended with
|
625
|
-
# the Protocol
|
626
|
-
# module.
|
627
|
-
def included(modul)
|
628
|
-
super
|
629
|
-
if modul.is_a? Class and @mode == :error or @mode == :warning
|
630
|
-
$DEBUG and warn "#{name} is checking class #{modul}"
|
631
|
-
check modul
|
632
|
-
end
|
633
|
-
end
|
634
|
-
|
635
|
-
# Sets the check mode to +id+. +id+ should be one of :none, :warning, or
|
636
|
-
# :error. The mode to use while doing a conformity check is always the root
|
637
|
-
# module, that is, the modes of the included modules aren't important for
|
638
|
-
# the check.
|
639
|
-
def check_failure(mode)
|
640
|
-
CHECK_MODES.include?(mode) or
|
641
|
-
raise ArgumentError, "illegal check mode #{mode}"
|
642
|
-
@mode = mode
|
643
|
-
end
|
644
|
-
|
645
|
-
# This method defines one of the messages, the protocol in question
|
646
|
-
# consists of: The messages which the class, that conforms to this
|
647
|
-
# protocol, should understand and respond to. An example shows best
|
648
|
-
# which +message+descriptions_ are allowed:
|
649
|
-
#
|
650
|
-
# MyProtocol = Protocol do
|
651
|
-
# understand :bar # conforming class must respond to :bar
|
652
|
-
# understand :baz, 3 # c. c. must respond to :baz with 3 args.
|
653
|
-
# understand :foo, -1 # c. c. must respond to :foo, any number of args.
|
654
|
-
# understand :quux, 0, true # c. c. must respond to :quux, no args + block.
|
655
|
-
# understand :quux1, 1, true # c. c. must respond to :quux, 1 arg + block.
|
656
|
-
# end
|
657
|
-
def understand(methodname, arity = nil, block_expected = false)
|
658
|
-
m = Message.new(self, methodname.to_s, arity, block_expected)
|
659
|
-
@descriptor.add_message(m)
|
660
|
-
self
|
661
|
-
end
|
662
|
-
|
663
|
-
def parse_instance_method_signature(modul, methodname)
|
664
|
-
methodname = methodname.to_s
|
665
|
-
method = modul.instance_method(methodname)
|
666
|
-
real_module = Utilities.find_method_module(methodname, modul.ancestors)
|
667
|
-
parser = MethodParser.new(real_module, methodname)
|
668
|
-
Message.new(self, methodname, method.arity, parser.block_arg?)
|
669
|
-
end
|
670
|
-
private :parse_instance_method_signature
|
671
|
-
|
672
|
-
# Inherit a method signature from an instance method named +methodname+ of
|
673
|
-
# +modul+. This means that this protocol should understand these instance
|
674
|
-
# methods with their arity and block expectation. Note that automatic
|
675
|
-
# detection of blocks does not work for Ruby methods defined in C. You can
|
676
|
-
# set the +block_expected+ argument if you want to do this manually.
|
677
|
-
def inherit(modul, methodname, block_expected = nil)
|
678
|
-
Module === modul or
|
679
|
-
raise TypeError, "expected Module not #{modul.class} as modul argument"
|
680
|
-
methodnames = methodname.respond_to?(:to_ary) ?
|
681
|
-
methodname.to_ary :
|
682
|
-
[ methodname ]
|
683
|
-
methodnames.each do |methodname|
|
684
|
-
m = parse_instance_method_signature(modul, methodname)
|
685
|
-
block_expected and m.block_expected = block_expected
|
686
|
-
@descriptor.add_message m
|
687
|
-
end
|
688
|
-
self
|
689
|
-
end
|
690
|
-
|
691
|
-
# Switch to implementation mode. Defined methods are added to the
|
692
|
-
# ProtocolModule as instance methods.
|
693
|
-
def implementation
|
694
|
-
@implementation = true
|
695
|
-
end
|
696
|
-
|
697
|
-
# Return true, if the ProtocolModule is currently in implementation mode.
|
698
|
-
# Otherwise return false.
|
699
|
-
def implementation?
|
700
|
-
!!@implementation
|
701
|
-
end
|
702
|
-
|
703
|
-
# Switch to specification mode. Defined methods are added to the protocol
|
704
|
-
# description in order to be checked against in later conformance tests.
|
705
|
-
def specification
|
706
|
-
@implementation = false
|
707
|
-
end
|
708
|
-
|
709
|
-
# Return true, if the ProtocolModule is currently in specification mode.
|
710
|
-
# Otherwise return false.
|
711
|
-
def specification?
|
712
|
-
!@implementation
|
713
|
-
end
|
714
|
-
|
715
|
-
# Capture all added methods and either leave the implementation in place or
|
716
|
-
# add them to the protocol description.
|
717
|
-
def method_added(methodname)
|
718
|
-
methodname = methodname.to_s
|
719
|
-
if specification? and methodname !~ /^__protocol_check_/
|
720
|
-
protocol_check = instance_method(methodname)
|
721
|
-
parser = MethodParser.new(self, methodname)
|
722
|
-
if parser.complex?
|
723
|
-
define_method("__protocol_check_#{methodname}", protocol_check)
|
724
|
-
understand methodname, protocol_check.arity, parser.block_arg?
|
725
|
-
else
|
726
|
-
understand methodname, protocol_check.arity, parser.block_arg?
|
727
|
-
end
|
728
|
-
remove_method methodname
|
729
|
-
else
|
730
|
-
super
|
731
|
-
end
|
732
|
-
end
|
733
|
-
end
|
734
|
-
|
735
|
-
# A module for some Utility methods.
|
736
|
-
module Utilities
|
737
|
-
module_function
|
738
|
-
|
739
|
-
# This Method tries to find the first module that implements the method
|
740
|
-
# named +methodname+ in the array of +ancestors+. If this fails nil is
|
741
|
-
# returned.
|
742
|
-
def find_method_module(methodname, ancestors)
|
743
|
-
methodname = methodname.to_s
|
744
|
-
ancestors.each do |a|
|
745
|
-
begin
|
746
|
-
a.instance_method(methodname)
|
747
|
-
return a
|
748
|
-
rescue NameError
|
749
|
-
end
|
750
|
-
end
|
751
|
-
nil
|
752
|
-
end
|
753
|
-
end
|
754
21
|
end
|