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