face-faye 0.8.9
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/History.txt +304 -0
- data/README.rdoc +83 -0
- data/lib/faye-browser-min.js +2 -0
- data/lib/faye-browser-min.js.map +8 -0
- data/lib/faye-browser.js +2194 -0
- data/lib/faye.rb +122 -0
- data/lib/faye/adapters/rack_adapter.rb +216 -0
- data/lib/faye/adapters/static_server.rb +56 -0
- data/lib/faye/engines/connection.rb +60 -0
- data/lib/faye/engines/memory.rb +112 -0
- data/lib/faye/engines/proxy.rb +121 -0
- data/lib/faye/error.rb +49 -0
- data/lib/faye/mixins/logging.rb +47 -0
- data/lib/faye/mixins/publisher.rb +30 -0
- data/lib/faye/mixins/timeouts.rb +22 -0
- data/lib/faye/protocol/channel.rb +124 -0
- data/lib/faye/protocol/client.rb +376 -0
- data/lib/faye/protocol/extensible.rb +43 -0
- data/lib/faye/protocol/grammar.rb +58 -0
- data/lib/faye/protocol/publication.rb +5 -0
- data/lib/faye/protocol/server.rb +293 -0
- data/lib/faye/protocol/socket.rb +23 -0
- data/lib/faye/protocol/subscription.rb +24 -0
- data/lib/faye/transport/http.rb +76 -0
- data/lib/faye/transport/local.rb +22 -0
- data/lib/faye/transport/transport.rb +116 -0
- data/lib/faye/transport/web_socket.rb +92 -0
- data/lib/faye/util/namespace.rb +20 -0
- data/spec/browser.html +45 -0
- data/spec/encoding_helper.rb +7 -0
- data/spec/install.sh +78 -0
- data/spec/javascript/channel_spec.js +15 -0
- data/spec/javascript/client_spec.js +729 -0
- data/spec/javascript/engine/memory_spec.js +7 -0
- data/spec/javascript/engine_spec.js +417 -0
- data/spec/javascript/faye_spec.js +34 -0
- data/spec/javascript/grammar_spec.js +66 -0
- data/spec/javascript/node_adapter_spec.js +307 -0
- data/spec/javascript/publisher_spec.js +27 -0
- data/spec/javascript/server/connect_spec.js +168 -0
- data/spec/javascript/server/disconnect_spec.js +121 -0
- data/spec/javascript/server/extensions_spec.js +60 -0
- data/spec/javascript/server/handshake_spec.js +145 -0
- data/spec/javascript/server/integration_spec.js +131 -0
- data/spec/javascript/server/publish_spec.js +85 -0
- data/spec/javascript/server/subscribe_spec.js +247 -0
- data/spec/javascript/server/unsubscribe_spec.js +245 -0
- data/spec/javascript/server_spec.js +121 -0
- data/spec/javascript/transport_spec.js +135 -0
- data/spec/node.js +55 -0
- data/spec/phantom.js +17 -0
- data/spec/ruby/channel_spec.rb +17 -0
- data/spec/ruby/client_spec.rb +741 -0
- data/spec/ruby/engine/memory_spec.rb +7 -0
- data/spec/ruby/engine_examples.rb +427 -0
- data/spec/ruby/faye_spec.rb +30 -0
- data/spec/ruby/grammar_spec.rb +68 -0
- data/spec/ruby/publisher_spec.rb +27 -0
- data/spec/ruby/rack_adapter_spec.rb +236 -0
- data/spec/ruby/server/connect_spec.rb +170 -0
- data/spec/ruby/server/disconnect_spec.rb +120 -0
- data/spec/ruby/server/extensions_spec.rb +68 -0
- data/spec/ruby/server/handshake_spec.rb +143 -0
- data/spec/ruby/server/integration_spec.rb +133 -0
- data/spec/ruby/server/publish_spec.rb +81 -0
- data/spec/ruby/server/subscribe_spec.rb +247 -0
- data/spec/ruby/server/unsubscribe_spec.rb +247 -0
- data/spec/ruby/server_spec.rb +121 -0
- data/spec/ruby/transport_spec.rb +136 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/testswarm +42 -0
- data/spec/thin_proxy.rb +37 -0
- metadata +441 -0
@@ -0,0 +1,376 @@
|
|
1
|
+
module Faye
|
2
|
+
class Client
|
3
|
+
|
4
|
+
include EventMachine::Deferrable
|
5
|
+
include Publisher
|
6
|
+
include Logging
|
7
|
+
include Extensible
|
8
|
+
|
9
|
+
UNCONNECTED = 1
|
10
|
+
CONNECTING = 2
|
11
|
+
CONNECTED = 3
|
12
|
+
DISCONNECTED = 4
|
13
|
+
|
14
|
+
HANDSHAKE = 'handshake'
|
15
|
+
RETRY = 'retry'
|
16
|
+
NONE = 'none'
|
17
|
+
|
18
|
+
CONNECTION_TIMEOUT = 60.0
|
19
|
+
DEFAULT_RETRY = 5.0
|
20
|
+
|
21
|
+
attr_reader :client_id, :endpoint, :endpoints, :retry, :transports
|
22
|
+
|
23
|
+
def initialize(endpoint = nil, options = {})
|
24
|
+
info('New client created for ?', endpoint)
|
25
|
+
|
26
|
+
@options = options
|
27
|
+
@endpoint = endpoint || RackAdapter::DEFAULT_ENDPOINT
|
28
|
+
@endpoints = @options[:endpoints] || {}
|
29
|
+
@transports = {}
|
30
|
+
@cookies = CookieJar::Jar.new
|
31
|
+
@headers = {}
|
32
|
+
@disabled = []
|
33
|
+
@retry = @options[:retry] || DEFAULT_RETRY
|
34
|
+
|
35
|
+
@state = UNCONNECTED
|
36
|
+
@channels = Channel::Set.new
|
37
|
+
@message_id = 0
|
38
|
+
|
39
|
+
@response_callbacks = {}
|
40
|
+
|
41
|
+
@advice = {
|
42
|
+
'reconnect' => RETRY,
|
43
|
+
'interval' => 1000.0 * (@options[:interval] || Engine::INTERVAL),
|
44
|
+
'timeout' => 1000.0 * (@options[:timeout] || CONNECTION_TIMEOUT)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def disable(feature)
|
49
|
+
@disabled << feature
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_header(name, value)
|
53
|
+
@headers[name.to_s] = value.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def state
|
57
|
+
case @state
|
58
|
+
when UNCONNECTED then :UNCONNECTED
|
59
|
+
when CONNECTING then :CONNECTING
|
60
|
+
when CONNECTED then :CONNECTED
|
61
|
+
when DISCONNECTED then :DISCONNECTED
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Request
|
66
|
+
# MUST include: * channel
|
67
|
+
# * version
|
68
|
+
# * supportedConnectionTypes
|
69
|
+
# MAY include: * minimumVersion
|
70
|
+
# * ext
|
71
|
+
# * id
|
72
|
+
#
|
73
|
+
# Success Response Failed Response
|
74
|
+
# MUST include: * channel MUST include: * channel
|
75
|
+
# * version * successful
|
76
|
+
# * supportedConnectionTypes * error
|
77
|
+
# * clientId MAY include: * supportedConnectionTypes
|
78
|
+
# * successful * advice
|
79
|
+
# MAY include: * minimumVersion * version
|
80
|
+
# * advice * minimumVersion
|
81
|
+
# * ext * ext
|
82
|
+
# * id * id
|
83
|
+
# * authSuccessful
|
84
|
+
def handshake(&block)
|
85
|
+
return if @advice['reconnect'] == NONE
|
86
|
+
return if @state != UNCONNECTED
|
87
|
+
|
88
|
+
@state = CONNECTING
|
89
|
+
|
90
|
+
info('Initiating handshake with ?', @endpoint)
|
91
|
+
select_transport(MANDATORY_CONNECTION_TYPES)
|
92
|
+
|
93
|
+
send({
|
94
|
+
'channel' => Channel::HANDSHAKE,
|
95
|
+
'version' => BAYEUX_VERSION,
|
96
|
+
'supportedConnectionTypes' => [@transport.connection_type]
|
97
|
+
|
98
|
+
}) do |response|
|
99
|
+
|
100
|
+
if response['successful']
|
101
|
+
@state = CONNECTED
|
102
|
+
@client_id = response['clientId']
|
103
|
+
|
104
|
+
select_transport(response['supportedConnectionTypes'])
|
105
|
+
|
106
|
+
info('Handshake successful: ?', @client_id)
|
107
|
+
|
108
|
+
subscribe(@channels.keys, true)
|
109
|
+
block.call if block_given?
|
110
|
+
|
111
|
+
else
|
112
|
+
info('Handshake unsuccessful')
|
113
|
+
EventMachine.add_timer(@advice['interval'] / 1000.0) { handshake(&block) }
|
114
|
+
@state = UNCONNECTED
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Request Response
|
120
|
+
# MUST include: * channel MUST include: * channel
|
121
|
+
# * clientId * successful
|
122
|
+
# * connectionType * clientId
|
123
|
+
# MAY include: * ext MAY include: * error
|
124
|
+
# * id * advice
|
125
|
+
# * ext
|
126
|
+
# * id
|
127
|
+
# * timestamp
|
128
|
+
def connect(&block)
|
129
|
+
return if @advice['reconnect'] == NONE or
|
130
|
+
@state == DISCONNECTED
|
131
|
+
|
132
|
+
return handshake { connect(&block) } if @state == UNCONNECTED
|
133
|
+
|
134
|
+
callback(&block)
|
135
|
+
return unless @state == CONNECTED
|
136
|
+
|
137
|
+
info('Calling deferred actions for ?', @client_id)
|
138
|
+
set_deferred_status(:succeeded)
|
139
|
+
set_deferred_status(:deferred)
|
140
|
+
|
141
|
+
return unless @connect_request.nil?
|
142
|
+
@connect_request = true
|
143
|
+
|
144
|
+
info('Initiating connection for ?', @client_id)
|
145
|
+
|
146
|
+
send({
|
147
|
+
'channel' => Channel::CONNECT,
|
148
|
+
'clientId' => @client_id,
|
149
|
+
'connectionType' => @transport.connection_type
|
150
|
+
|
151
|
+
}) do
|
152
|
+
cycle_connection
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Request Response
|
157
|
+
# MUST include: * channel MUST include: * channel
|
158
|
+
# * clientId * successful
|
159
|
+
# MAY include: * ext * clientId
|
160
|
+
# * id MAY include: * error
|
161
|
+
# * ext
|
162
|
+
# * id
|
163
|
+
def disconnect
|
164
|
+
return unless @state == CONNECTED
|
165
|
+
@state = DISCONNECTED
|
166
|
+
|
167
|
+
info('Disconnecting ?', @client_id)
|
168
|
+
|
169
|
+
send({
|
170
|
+
'channel' => Channel::DISCONNECT,
|
171
|
+
'clientId' => @client_id
|
172
|
+
|
173
|
+
}) do |response|
|
174
|
+
@transport.close if response['successful']
|
175
|
+
end
|
176
|
+
|
177
|
+
info('Clearing channel listeners for ?', @client_id)
|
178
|
+
@channels = Channel::Set.new
|
179
|
+
end
|
180
|
+
|
181
|
+
# Request Response
|
182
|
+
# MUST include: * channel MUST include: * channel
|
183
|
+
# * clientId * successful
|
184
|
+
# * subscription * clientId
|
185
|
+
# MAY include: * ext * subscription
|
186
|
+
# * id MAY include: * error
|
187
|
+
# * advice
|
188
|
+
# * ext
|
189
|
+
# * id
|
190
|
+
# * timestamp
|
191
|
+
def subscribe(channel, force = false, &block)
|
192
|
+
if Array === channel
|
193
|
+
return channel.map { |c| subscribe(c, force, &block) }
|
194
|
+
end
|
195
|
+
|
196
|
+
subscription = Subscription.new(self, channel, block)
|
197
|
+
has_subscribe = @channels.has_subscription?(channel)
|
198
|
+
|
199
|
+
if has_subscribe and not force
|
200
|
+
@channels.subscribe([channel], block)
|
201
|
+
subscription.set_deferred_status(:succeeded)
|
202
|
+
return subscription
|
203
|
+
end
|
204
|
+
|
205
|
+
connect {
|
206
|
+
info('Client ? attempting to subscribe to ?', @client_id, channel)
|
207
|
+
@channels.subscribe([channel], block) unless force
|
208
|
+
|
209
|
+
send({
|
210
|
+
'channel' => Channel::SUBSCRIBE,
|
211
|
+
'clientId' => @client_id,
|
212
|
+
'subscription' => channel
|
213
|
+
|
214
|
+
}) do |response|
|
215
|
+
unless response['successful']
|
216
|
+
subscription.set_deferred_status(:failed, Error.parse(response['error']))
|
217
|
+
next @channels.unsubscribe(channel, block)
|
218
|
+
end
|
219
|
+
|
220
|
+
channels = [response['subscription']].flatten
|
221
|
+
info('Subscription acknowledged for ? to ?', @client_id, channels)
|
222
|
+
subscription.set_deferred_status(:succeeded)
|
223
|
+
end
|
224
|
+
}
|
225
|
+
subscription
|
226
|
+
end
|
227
|
+
|
228
|
+
# Request Response
|
229
|
+
# MUST include: * channel MUST include: * channel
|
230
|
+
# * clientId * successful
|
231
|
+
# * subscription * clientId
|
232
|
+
# MAY include: * ext * subscription
|
233
|
+
# * id MAY include: * error
|
234
|
+
# * advice
|
235
|
+
# * ext
|
236
|
+
# * id
|
237
|
+
# * timestamp
|
238
|
+
def unsubscribe(channel, &block)
|
239
|
+
if Array === channel
|
240
|
+
return channel.map { |c| unsubscribe(c, &block) }
|
241
|
+
end
|
242
|
+
|
243
|
+
dead = @channels.unsubscribe(channel, block)
|
244
|
+
return unless dead
|
245
|
+
|
246
|
+
connect {
|
247
|
+
info('Client ? attempting to unsubscribe from ?', @client_id, channel)
|
248
|
+
|
249
|
+
send({
|
250
|
+
'channel' => Channel::UNSUBSCRIBE,
|
251
|
+
'clientId' => @client_id,
|
252
|
+
'subscription' => channel
|
253
|
+
|
254
|
+
}) do |response|
|
255
|
+
next unless response['successful']
|
256
|
+
|
257
|
+
channels = [response['subscription']].flatten
|
258
|
+
info('Unsubscription acknowledged for ? from ?', @client_id, channels)
|
259
|
+
end
|
260
|
+
}
|
261
|
+
end
|
262
|
+
|
263
|
+
# Request Response
|
264
|
+
# MUST include: * channel MUST include: * channel
|
265
|
+
# * data * successful
|
266
|
+
# MAY include: * clientId MAY include: * id
|
267
|
+
# * id * error
|
268
|
+
# * ext * ext
|
269
|
+
def publish(channel, data)
|
270
|
+
publication = Publication.new
|
271
|
+
connect {
|
272
|
+
info('Client ? queueing published message to ?: ?', @client_id, channel, data)
|
273
|
+
|
274
|
+
send({
|
275
|
+
'channel' => channel,
|
276
|
+
'data' => data,
|
277
|
+
'clientId' => @client_id
|
278
|
+
}) do |response|
|
279
|
+
if response['successful']
|
280
|
+
publication.set_deferred_status(:succeeded)
|
281
|
+
else
|
282
|
+
publication.set_deferred_status(:failed, Error.parse(response['error']))
|
283
|
+
end
|
284
|
+
end
|
285
|
+
}
|
286
|
+
publication
|
287
|
+
end
|
288
|
+
|
289
|
+
def receive_message(message)
|
290
|
+
pipe_through_extensions(:incoming, message) do |message|
|
291
|
+
next unless message
|
292
|
+
|
293
|
+
handle_advice(message['advice']) if message['advice']
|
294
|
+
deliver_message(message)
|
295
|
+
|
296
|
+
next unless message.has_key?('successful')
|
297
|
+
|
298
|
+
callback = @response_callbacks[message['id']]
|
299
|
+
next unless callback
|
300
|
+
|
301
|
+
@response_callbacks.delete(message['id'])
|
302
|
+
callback.call(message)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
private
|
307
|
+
|
308
|
+
def select_transport(transport_types)
|
309
|
+
Transport.get(self, transport_types, @disabled) do |transport|
|
310
|
+
debug('Selected ? transport for ?', transport.connection_type, transport.endpoint)
|
311
|
+
|
312
|
+
@transport = transport
|
313
|
+
@transport.cookies = @cookies
|
314
|
+
@transport.headers = @headers
|
315
|
+
|
316
|
+
transport.bind :down do
|
317
|
+
if @transport_up.nil? or @transport_up
|
318
|
+
@transport_up = false
|
319
|
+
trigger('transport:down')
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
transport.bind :up do
|
324
|
+
if @transport_up.nil? or not @transport_up
|
325
|
+
@transport_up = true
|
326
|
+
trigger('transport:up')
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def send(message, &callback)
|
333
|
+
message['id'] = generate_message_id
|
334
|
+
@response_callbacks[message['id']] = callback if callback
|
335
|
+
|
336
|
+
pipe_through_extensions(:outgoing, message) do |message|
|
337
|
+
@transport.send(message, @advice['timeout'] / 1000.0) if message
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def generate_message_id
|
342
|
+
@message_id += 1
|
343
|
+
@message_id = 0 if @message_id >= 2**32
|
344
|
+
@message_id.to_s(36)
|
345
|
+
end
|
346
|
+
|
347
|
+
def handle_advice(advice)
|
348
|
+
@advice.update(advice)
|
349
|
+
|
350
|
+
if @advice['reconnect'] == HANDSHAKE and @state != DISCONNECTED
|
351
|
+
@state = UNCONNECTED
|
352
|
+
@client_id = nil
|
353
|
+
cycle_connection
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def deliver_message(message)
|
358
|
+
return unless message.has_key?('channel') and message.has_key?('data')
|
359
|
+
info('Client ? calling listeners for ? with ?', @client_id, message['channel'], message['data'])
|
360
|
+
@channels.distribute_message(message)
|
361
|
+
end
|
362
|
+
|
363
|
+
def teardown_connection
|
364
|
+
return unless @connect_request
|
365
|
+
@connect_request = nil
|
366
|
+
info('Closed connection for ?', @client_id)
|
367
|
+
end
|
368
|
+
|
369
|
+
def cycle_connection
|
370
|
+
teardown_connection
|
371
|
+
EventMachine.add_timer(@advice['interval'] / 1000.0) { connect }
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Faye
|
2
|
+
module Extensible
|
3
|
+
include Logging
|
4
|
+
|
5
|
+
def add_extension(extension)
|
6
|
+
@extensions ||= []
|
7
|
+
@extensions << extension
|
8
|
+
extension.added(self) if extension.respond_to?(:added)
|
9
|
+
end
|
10
|
+
|
11
|
+
def remove_extension(extension)
|
12
|
+
return unless @extensions
|
13
|
+
@extensions.delete_if do |ext|
|
14
|
+
next false unless ext == extension
|
15
|
+
extension.removed(self) if extension.respond_to?(:removed)
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pipe_through_extensions(stage, message, &callback)
|
21
|
+
debug 'Passing through ? extensions: ?', stage, message
|
22
|
+
|
23
|
+
return callback.call(message) unless @extensions
|
24
|
+
extensions = @extensions.dup
|
25
|
+
|
26
|
+
pipe = lambda do |message|
|
27
|
+
next callback.call(message) unless message
|
28
|
+
|
29
|
+
extension = extensions.shift
|
30
|
+
next callback.call(message) unless extension
|
31
|
+
|
32
|
+
if extension.respond_to?(stage)
|
33
|
+
extension.__send__(stage, message, pipe)
|
34
|
+
else
|
35
|
+
pipe.call(message)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
pipe.call(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Faye
|
2
|
+
module Grammar
|
3
|
+
|
4
|
+
def self.rule(&block)
|
5
|
+
source = instance_eval(&block)
|
6
|
+
%r{^#{string(source)}$}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.choice(*list)
|
10
|
+
'(' + list.map(&method(:string)) * '|' + ')'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.repeat(*pattern)
|
14
|
+
'(' + string(pattern) + ')*'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.oneormore(*pattern)
|
18
|
+
'(' + string(pattern) + ')+'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.string(item)
|
22
|
+
return item.map(&method(:string)) * '' if Array === item
|
23
|
+
String === item ? item : item.source.gsub(/^\^/, '').gsub(/\$$/, '')
|
24
|
+
end
|
25
|
+
|
26
|
+
LOWALPHA = rule {[ '[a-z]' ]}
|
27
|
+
UPALPHA = rule {[ '[A-Z]' ]}
|
28
|
+
ALPHA = rule {[ choice(LOWALPHA, UPALPHA) ]}
|
29
|
+
DIGIT = rule {[ '[0-9]' ]}
|
30
|
+
ALPHANUM = rule {[ choice(ALPHA, DIGIT) ]}
|
31
|
+
MARK = rule {[ choice(*%w[\\- \\_ \\! \\~ \\( \\) \\$ \\@]) ]}
|
32
|
+
STRING = rule {[ repeat(choice(ALPHANUM, MARK, ' ', '\\/', '\\*', '\\.')) ]}
|
33
|
+
TOKEN = rule {[ oneormore(choice(ALPHANUM, MARK)) ]}
|
34
|
+
INTEGER = rule {[ oneormore(DIGIT) ]}
|
35
|
+
|
36
|
+
CHANNEL_SEGMENT = rule {[ TOKEN ]}
|
37
|
+
CHANNEL_SEGMENTS = rule {[ CHANNEL_SEGMENT, repeat('\\/', CHANNEL_SEGMENT) ]}
|
38
|
+
CHANNEL_NAME = rule {[ '\\/', CHANNEL_SEGMENTS ]}
|
39
|
+
|
40
|
+
WILD_CARD = rule {[ '\\*{1,2}' ]}
|
41
|
+
CHANNEL_PATTERN = rule {[ repeat('\\/', CHANNEL_SEGMENT), '\\/', WILD_CARD ]}
|
42
|
+
|
43
|
+
VERSION_ELEMENT = rule {[ ALPHANUM, repeat(choice(ALPHANUM, '\\-', '\\_')) ]}
|
44
|
+
VERSION = rule {[ INTEGER, repeat('\\.', VERSION_ELEMENT) ]}
|
45
|
+
|
46
|
+
CLIENT_ID = rule {[ oneormore(ALPHANUM) ]}
|
47
|
+
|
48
|
+
ID = rule {[ oneormore(ALPHANUM) ]}
|
49
|
+
|
50
|
+
ERROR_MESSAGE = rule {[ STRING ]}
|
51
|
+
ERROR_ARGS = rule {[ STRING, repeat(',', STRING) ]}
|
52
|
+
ERROR_CODE = rule {[ DIGIT, DIGIT, DIGIT ]}
|
53
|
+
ERROR = rule {[ choice(string([ERROR_CODE, ':', ERROR_ARGS, ':', ERROR_MESSAGE]),
|
54
|
+
string([ERROR_CODE, ':', ':', ERROR_MESSAGE])) ]}
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|