protocol 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/protocol.rb CHANGED
@@ -1,139 +1,14 @@
1
1
  require 'protocol/version'
2
2
 
3
3
  module Protocol
4
- if RUBY_VERSION[/\A1\.8\./]
5
- require 'protocol/method_parser/parse_tree'
6
- else
7
- require 'protocol/method_parser/ruby_parser'
8
- end
9
-
10
- class ::Object
11
- # Returns true if this object conforms to +protocol+, otherwise false.
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