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