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
@@ -87,6 +87,7 @@ module Protocol
|
|
87
87
|
begin
|
88
88
|
source = IO.readlines(filename)
|
89
89
|
cache[filename] = source
|
90
|
+
$DEBUG and warn "#{self.class} just cached #{filename.inspect}."
|
90
91
|
rescue SystemCallError => e
|
91
92
|
$DEBUG and warn "Caught #{e.class}: #{e}"
|
92
93
|
nil
|
@@ -103,16 +104,18 @@ module Protocol
|
|
103
104
|
source = source[(lineno - 1)..-1].join
|
104
105
|
current = 0
|
105
106
|
tree = nil
|
107
|
+
parser = RubyParser.new
|
106
108
|
while current = source.index('end', current)
|
107
109
|
current += 3
|
108
110
|
begin
|
109
|
-
tree =
|
111
|
+
tree = parser.parse(source[0, current], filename)
|
110
112
|
break
|
111
113
|
rescue SyntaxError, Racc::ParseError
|
112
114
|
end
|
115
|
+
parser.reset
|
113
116
|
end
|
114
117
|
ary = tree.to_a.flatten
|
115
|
-
@complex = ary.
|
118
|
+
@complex = ary.any? { |node| [ :call, :fcall, :vcall ].include?(node) }
|
116
119
|
if ary.index(:yield) and @arg_kinds.last != :block
|
117
120
|
@args.push :'&block'
|
118
121
|
@arg_kinds.push :block
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Protocol
|
2
|
+
# This class is a proxy that stores postcondition blocks, which are called
|
3
|
+
# after the result of the wrapped method was determined.
|
4
|
+
class Postcondition
|
5
|
+
instance_methods.each do |m|
|
6
|
+
m.to_s =~ /\A(__|object_id|instance_eval\z|inspect\z)/ or undef_method m
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(object)
|
10
|
+
@object = object
|
11
|
+
@blocks = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# This is the alternative result "keyword".
|
15
|
+
def __result__
|
16
|
+
@result
|
17
|
+
end
|
18
|
+
|
19
|
+
# This is the result "keyword" which can be used to query the result of
|
20
|
+
# wrapped method in a postcondition clause.
|
21
|
+
def result
|
22
|
+
if @object.respond_to? :result
|
23
|
+
warn "#{@object.class} already defines a result method, "\
|
24
|
+
"try __result__ instead"
|
25
|
+
@object.__send__(:result)
|
26
|
+
else
|
27
|
+
@result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# This is the "keyword" to be used instead of +self+ to refer to current
|
32
|
+
# object.
|
33
|
+
def myself
|
34
|
+
@object
|
35
|
+
end
|
36
|
+
|
37
|
+
# :stopdoc:
|
38
|
+
def __result__=(result)
|
39
|
+
@result = result
|
40
|
+
end
|
41
|
+
|
42
|
+
def __check__
|
43
|
+
@blocks.all? { |block| instance_eval(&block) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def __add__(block)
|
47
|
+
@blocks << block
|
48
|
+
self
|
49
|
+
end
|
50
|
+
# :startdoc:
|
51
|
+
|
52
|
+
# Send all remaining messages to the object.
|
53
|
+
def method_missing(*a, &b)
|
54
|
+
@object.__send__(*a, &b)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
module Protocol
|
2
|
+
# A ProtocolModule object
|
3
|
+
class ProtocolModule < Module
|
4
|
+
# Creates an new ProtocolModule instance.
|
5
|
+
def initialize(&block)
|
6
|
+
@descriptor = Descriptor.new(self)
|
7
|
+
@mode = :error
|
8
|
+
module_eval(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
# The current check mode :none, :warning, or :error (the default).
|
12
|
+
attr_reader :mode
|
13
|
+
|
14
|
+
# Returns all the protocol descriptions to check against as an Array.
|
15
|
+
def descriptors
|
16
|
+
descriptors = []
|
17
|
+
protocols.each do |a|
|
18
|
+
descriptors << a.instance_variable_get(:@descriptor)
|
19
|
+
end
|
20
|
+
descriptors
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return self and all protocols included into self.
|
24
|
+
def protocols
|
25
|
+
ancestors.select { |modul| modul.is_a? ProtocolModule }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Concatenates the protocol as Ruby code to the +result+ string and return
|
29
|
+
# it. At the moment this method only supports method signatures with
|
30
|
+
# generic argument names.
|
31
|
+
def to_ruby(result = '')
|
32
|
+
result << "#{name} = Protocol do"
|
33
|
+
first = true
|
34
|
+
if messages.empty?
|
35
|
+
result << "\n"
|
36
|
+
else
|
37
|
+
messages.each do |m|
|
38
|
+
result << "\n"
|
39
|
+
m.to_ruby(result)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
result << "end\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns all messages this protocol (plus the included protocols) consists
|
46
|
+
# of in alphabetic order. This method caches the computed result array. You
|
47
|
+
# have to call #reset_messages, if you want to recompute the array in the
|
48
|
+
# next call to #messages.
|
49
|
+
def messages
|
50
|
+
result = []
|
51
|
+
seen = {}
|
52
|
+
descriptors.each do |d|
|
53
|
+
dm = d.messages
|
54
|
+
dm.delete_if do |m|
|
55
|
+
delete = seen[m.name]
|
56
|
+
seen[m.name] = true
|
57
|
+
delete
|
58
|
+
end
|
59
|
+
result.concat dm
|
60
|
+
end
|
61
|
+
result.sort!
|
62
|
+
end
|
63
|
+
|
64
|
+
alias to_a messages
|
65
|
+
|
66
|
+
# Reset the cached message array. Call this if you want to change the
|
67
|
+
# protocol dynamically after it was already used (= the #messages method
|
68
|
+
# was called).
|
69
|
+
def reset_messages
|
70
|
+
@messages = nil
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true if it is required to understand the
|
75
|
+
def understand?(name, arity = nil)
|
76
|
+
name = name.to_s
|
77
|
+
!!find { |m| m.name == name && (!arity || m.arity == arity) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the Message object named +name+ or nil, if it doesn't exist.
|
81
|
+
def [](name)
|
82
|
+
name = name.to_s
|
83
|
+
find { |m| m.name == name }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return all message whose names matches pattern.
|
87
|
+
def grep(pattern)
|
88
|
+
select { |m| pattern === m.name }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Iterate over all messages and yield to all of them.
|
92
|
+
def each_message(&block) # :yields: message
|
93
|
+
messages.each(&block)
|
94
|
+
self
|
95
|
+
end
|
96
|
+
alias each each_message
|
97
|
+
|
98
|
+
include Enumerable
|
99
|
+
|
100
|
+
# Returns a string representation of this protocol, that consists of the
|
101
|
+
# understood messages. This protocol
|
102
|
+
#
|
103
|
+
# FooProtocol = Protocol do
|
104
|
+
# def bar(x, y, &b) end
|
105
|
+
# def baz(x, y, z) end
|
106
|
+
# def foo(*rest) end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# returns this string:
|
110
|
+
#
|
111
|
+
# FooProtocol#bar(2&), FooProtocol#baz(3), FooProtocol#foo(-1)
|
112
|
+
def to_s
|
113
|
+
messages * ', '
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a short string representation of this protocol, that consists of
|
117
|
+
# the understood messages. This protocol
|
118
|
+
#
|
119
|
+
# FooProtocol = Protocol do
|
120
|
+
# def bar(x, y, &b) end
|
121
|
+
# def baz(x, y, z) end
|
122
|
+
# def foo(*rest) end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# returns this string:
|
126
|
+
#
|
127
|
+
# #<FooProtocol: bar(2&), baz(3), foo(-1)>
|
128
|
+
def inspect
|
129
|
+
"#<#{name}: #{messages.map { |m| m.shortcut } * ', '}>"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check the conformity of +object+ recursively. This method returns either
|
133
|
+
# false OR true, if +mode+ is :none or :warning, or raises an
|
134
|
+
# CheckFailed, if +mode+ was :error.
|
135
|
+
def check(object, mode = @mode)
|
136
|
+
checked = {}
|
137
|
+
result = true
|
138
|
+
errors = CheckFailed.new
|
139
|
+
each do |message|
|
140
|
+
begin
|
141
|
+
message.check(object, checked)
|
142
|
+
rescue CheckError => e
|
143
|
+
case mode
|
144
|
+
when :error
|
145
|
+
errors << e
|
146
|
+
when :warning
|
147
|
+
warn e.to_s
|
148
|
+
result = false
|
149
|
+
when :none
|
150
|
+
result = false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
raise errors unless errors.empty?
|
155
|
+
result
|
156
|
+
end
|
157
|
+
|
158
|
+
alias =~ check
|
159
|
+
|
160
|
+
# Return all messages for whick a check failed.
|
161
|
+
def check_failures(object)
|
162
|
+
check object
|
163
|
+
rescue CheckFailed => e
|
164
|
+
return e.errors.map { |e| e.protocol_message }
|
165
|
+
end
|
166
|
+
|
167
|
+
# This callback is called, when a module, that was extended with Protocol,
|
168
|
+
# is included (via Modul#include/via Class#conform_to) into some other
|
169
|
+
# module/class.
|
170
|
+
# If +modul+ is a Class, all protocol descriptions of the inheritance tree
|
171
|
+
# are collected and the given class is checked for conformance to the
|
172
|
+
# protocol. +modul+ isn't a Class and only a Module, it is extended with
|
173
|
+
# the Protocol
|
174
|
+
# module.
|
175
|
+
def included(modul)
|
176
|
+
super
|
177
|
+
if modul.is_a? Class and @mode == :error or @mode == :warning
|
178
|
+
$DEBUG and warn "#{name} is checking class #{modul}"
|
179
|
+
check modul
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def extend_object(object)
|
184
|
+
result = super
|
185
|
+
if @mode == :error or @mode == :warning
|
186
|
+
$DEBUG and warn "#{name} is checking class #{object}"
|
187
|
+
check object
|
188
|
+
end
|
189
|
+
result
|
190
|
+
end
|
191
|
+
|
192
|
+
# Sets the check mode to +id+. +id+ should be one of :none, :warning, or
|
193
|
+
# :error. The mode to use while doing a conformity check is always the root
|
194
|
+
# module, that is, the modes of the included modules aren't important for
|
195
|
+
# the check.
|
196
|
+
def check_failure(mode)
|
197
|
+
CHECK_MODES.include?(mode) or
|
198
|
+
raise ArgumentError, "illegal check mode #{mode}"
|
199
|
+
@mode = mode
|
200
|
+
end
|
201
|
+
|
202
|
+
# This method defines one of the messages, the protocol in question
|
203
|
+
# consists of: The messages which the class, that conforms to this
|
204
|
+
# protocol, should understand and respond to. An example shows best
|
205
|
+
# which +message+descriptions_ are allowed:
|
206
|
+
#
|
207
|
+
# MyProtocol = Protocol do
|
208
|
+
# understand :bar # conforming class must respond to :bar
|
209
|
+
# understand :baz, 3 # c. c. must respond to :baz with 3 args.
|
210
|
+
# understand :foo, -1 # c. c. must respond to :foo, any number of args.
|
211
|
+
# understand :quux, 0, true # c. c. must respond to :quux, no args + block.
|
212
|
+
# understand :quux1, 1, true # c. c. must respond to :quux, 1 arg + block.
|
213
|
+
# end
|
214
|
+
def understand(methodname, arity = nil, block_expected = false)
|
215
|
+
m = Message.new(self, methodname.to_s, arity, block_expected)
|
216
|
+
@descriptor.add_message(m)
|
217
|
+
self
|
218
|
+
end
|
219
|
+
|
220
|
+
def parse_instance_method_signature(modul, methodname)
|
221
|
+
methodname = methodname.to_s
|
222
|
+
method = modul.instance_method(methodname)
|
223
|
+
real_module = Utilities.find_method_module(methodname, modul.ancestors)
|
224
|
+
parser = MethodParser.new(real_module, methodname)
|
225
|
+
Message.new(self, methodname, method.arity, parser.block_arg?)
|
226
|
+
end
|
227
|
+
private :parse_instance_method_signature
|
228
|
+
|
229
|
+
# Inherit a method signature from an instance method named +methodname+ of
|
230
|
+
# +modul+. This means that this protocol should understand these instance
|
231
|
+
# methods with their arity and block expectation. Note that automatic
|
232
|
+
# detection of blocks does not work for Ruby methods defined in C. You can
|
233
|
+
# set the +block_expected+ argument if you want to do this manually.
|
234
|
+
def inherit(modul, methodname, block_expected = nil)
|
235
|
+
Module === modul or
|
236
|
+
raise TypeError, "expected Module not #{modul.class} as modul argument"
|
237
|
+
methodnames = methodname.respond_to?(:to_ary) ?
|
238
|
+
methodname.to_ary :
|
239
|
+
[ methodname ]
|
240
|
+
methodnames.each do |methodname|
|
241
|
+
m = parse_instance_method_signature(modul, methodname)
|
242
|
+
block_expected and m.block_expected = block_expected
|
243
|
+
@descriptor.add_message m
|
244
|
+
end
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
# Switch to implementation mode. Defined methods are added to the
|
249
|
+
# ProtocolModule as instance methods.
|
250
|
+
def implementation
|
251
|
+
@implementation = true
|
252
|
+
end
|
253
|
+
|
254
|
+
# Return true, if the ProtocolModule is currently in implementation mode.
|
255
|
+
# Otherwise return false.
|
256
|
+
def implementation?
|
257
|
+
!!@implementation
|
258
|
+
end
|
259
|
+
|
260
|
+
# Switch to specification mode. Defined methods are added to the protocol
|
261
|
+
# description in order to be checked against in later conformance tests.
|
262
|
+
def specification
|
263
|
+
@implementation = false
|
264
|
+
end
|
265
|
+
|
266
|
+
# Return true, if the ProtocolModule is currently in specification mode.
|
267
|
+
# Otherwise return false.
|
268
|
+
def specification?
|
269
|
+
!@implementation
|
270
|
+
end
|
271
|
+
|
272
|
+
# Capture all added methods and either leave the implementation in place or
|
273
|
+
# add them to the protocol description.
|
274
|
+
def method_added(methodname)
|
275
|
+
methodname = methodname.to_s
|
276
|
+
if specification? and methodname !~ /^__protocol_check_/
|
277
|
+
protocol_check = instance_method(methodname)
|
278
|
+
parser = MethodParser.new(self, methodname)
|
279
|
+
if parser.complex?
|
280
|
+
define_method("__protocol_check_#{methodname}", protocol_check)
|
281
|
+
understand methodname, protocol_check.arity, parser.block_arg?
|
282
|
+
else
|
283
|
+
understand methodname, protocol_check.arity, parser.block_arg?
|
284
|
+
end
|
285
|
+
remove_method methodname
|
286
|
+
else
|
287
|
+
super
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Protocol
|
2
|
+
# A module for some Utility methods.
|
3
|
+
module Utilities
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# This Method tries to find the first module that implements the method
|
7
|
+
# named +methodname+ in the array of +ancestors+. If this fails nil is
|
8
|
+
# returned.
|
9
|
+
def find_method_module(methodname, ancestors)
|
10
|
+
methodname = methodname.to_s
|
11
|
+
ancestors.each do |a|
|
12
|
+
begin
|
13
|
+
a.instance_method(methodname)
|
14
|
+
return a
|
15
|
+
rescue NameError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/protocol/version.rb
CHANGED
data/lib/protocol/xt.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Protocol
|
2
|
+
class ::Object
|
3
|
+
# Returns true if this object conforms to +protocol+, otherwise false.
|
4
|
+
#
|
5
|
+
# This is especially useful, if check_failure in the protocol is set to
|
6
|
+
# :none or :warning, and conformance of a class to a protocol should be
|
7
|
+
# checked later in runtime.
|
8
|
+
def conform_to?(protocol)
|
9
|
+
protocol.check(self, :none)
|
10
|
+
end
|
11
|
+
|
12
|
+
def conform_to!(protocol)
|
13
|
+
extend(protocol)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Define a protocol configured by +block+. Look at the methods of
|
17
|
+
# ProtocolModule to get an idea on how to do that.
|
18
|
+
def Protocol(&block)
|
19
|
+
ProtocolModule.new(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
alias protocol Protocol
|
23
|
+
end
|
24
|
+
|
25
|
+
class ::Class
|
26
|
+
# This method should be called at the end of a class definition, that is,
|
27
|
+
# after all methods have been added to the class. The conformance to the
|
28
|
+
# protocol of the class given as the argument is checked. See
|
29
|
+
# Protocol::CHECK_MODES for the consequences of failure of this check.
|
30
|
+
alias conform_to include
|
31
|
+
|
32
|
+
# Returns true if this class conforms to +protocol+, otherwise false.
|
33
|
+
#
|
34
|
+
# This is especially useful, if check_failure in the protocol is set to
|
35
|
+
# :none or :warning, and conformance of a class to a protocol should be
|
36
|
+
# checked later in runtime.
|
37
|
+
def conform_to?(protocol)
|
38
|
+
protocol.check(self, :none)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|