protocol 0.9.0 → 1.0.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.
- 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
|