mt-uv-rays 2.4.7
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/LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +22 -0
- data/lib/faraday/adapter/mt-libuv.rb +89 -0
- data/lib/handsoap/http/drivers/mt-libuv_driver.rb +43 -0
- data/lib/httpi/adapter/mt-libuv.rb +69 -0
- data/lib/mt-uv-rays/abstract_tokenizer.rb +121 -0
- data/lib/mt-uv-rays/buffered_tokenizer.rb +176 -0
- data/lib/mt-uv-rays/connection.rb +190 -0
- data/lib/mt-uv-rays/http/encoding.rb +131 -0
- data/lib/mt-uv-rays/http/parser.rb +175 -0
- data/lib/mt-uv-rays/http/request.rb +262 -0
- data/lib/mt-uv-rays/http_endpoint.rb +336 -0
- data/lib/mt-uv-rays/ping.rb +189 -0
- data/lib/mt-uv-rays/scheduler/time.rb +307 -0
- data/lib/mt-uv-rays/scheduler.rb +386 -0
- data/lib/mt-uv-rays/tcp_server.rb +46 -0
- data/lib/mt-uv-rays/version.rb +5 -0
- data/lib/mt-uv-rays.rb +94 -0
- data/mt-uv-rays.gemspec +38 -0
- data/spec/abstract_tokenizer_spec.rb +129 -0
- data/spec/buffered_tokenizer_spec.rb +277 -0
- data/spec/connection_spec.rb +124 -0
- data/spec/http_endpoint_spec.rb +636 -0
- data/spec/ping_spec.rb +73 -0
- data/spec/scheduler_spec.rb +118 -0
- data/spec/scheduler_time_spec.rb +132 -0
- metadata +300 -0
@@ -0,0 +1,386 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set' # ruby std lib
|
4
|
+
require 'bisect' # insert into a sorted array
|
5
|
+
require 'tzinfo' # timezone information
|
6
|
+
require 'mt-uv-rays/scheduler/time'
|
7
|
+
|
8
|
+
module MTUV
|
9
|
+
|
10
|
+
class ScheduledEvent < ::MTLibuv::Q::DeferredPromise
|
11
|
+
# Note:: Comparable should not effect Hashes
|
12
|
+
# it will however effect arrays
|
13
|
+
include Comparable
|
14
|
+
|
15
|
+
attr_reader :created
|
16
|
+
attr_reader :last_scheduled
|
17
|
+
attr_reader :next_scheduled
|
18
|
+
attr_reader :trigger_count
|
19
|
+
|
20
|
+
def initialize(scheduler)
|
21
|
+
# Create a dummy deferrable
|
22
|
+
reactor = scheduler.reactor
|
23
|
+
defer = reactor.defer
|
24
|
+
|
25
|
+
# Record a backtrace of where the schedule was created
|
26
|
+
@trace = caller
|
27
|
+
|
28
|
+
# Setup common event variables
|
29
|
+
@scheduler = scheduler
|
30
|
+
@created = reactor.now
|
31
|
+
@last_scheduled = @created
|
32
|
+
@trigger_count = 0
|
33
|
+
|
34
|
+
# init the promise
|
35
|
+
super(reactor, defer)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provide relevant inspect information
|
39
|
+
def inspect
|
40
|
+
insp = String.new("#<#{self.class}:#{"0x00%x" % (self.__id__ << 1)} ")
|
41
|
+
insp << "trigger_count=#{@trigger_count} "
|
42
|
+
insp << "config=#{info} " if self.respond_to?(:info, true)
|
43
|
+
insp << "next_scheduled=#{to_time(@next_scheduled)} "
|
44
|
+
insp << "last_scheduled=#{to_time(@last_scheduled)} created=#{to_time(@created)}>"
|
45
|
+
insp
|
46
|
+
end
|
47
|
+
alias_method :to_s, :inspect
|
48
|
+
|
49
|
+
def to_time(internal_time)
|
50
|
+
if internal_time
|
51
|
+
((internal_time + @scheduler.time_diff) / 1000).to_i
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# required for comparable
|
57
|
+
def <=>(anOther)
|
58
|
+
@next_scheduled <=> anOther.next_scheduled
|
59
|
+
end
|
60
|
+
|
61
|
+
# reject the promise
|
62
|
+
def cancel
|
63
|
+
@defer.reject(:cancelled)
|
64
|
+
end
|
65
|
+
|
66
|
+
# notify listeners of the event
|
67
|
+
def trigger
|
68
|
+
@trigger_count += 1
|
69
|
+
@defer.notify(@reactor.now, self)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class OneShot < ScheduledEvent
|
74
|
+
def initialize(scheduler, at)
|
75
|
+
super(scheduler)
|
76
|
+
|
77
|
+
@next_scheduled = at
|
78
|
+
end
|
79
|
+
|
80
|
+
# Updates the scheduled time
|
81
|
+
def update(time)
|
82
|
+
@last_scheduled = @reactor.now
|
83
|
+
|
84
|
+
parsed_time = Scheduler.parse_in(time, :quiet)
|
85
|
+
if parsed_time.nil?
|
86
|
+
# Parse at will throw an error if time is invalid
|
87
|
+
parsed_time = Scheduler.parse_at(time) - @scheduler.time_diff
|
88
|
+
else
|
89
|
+
parsed_time += @last_scheduled
|
90
|
+
end
|
91
|
+
|
92
|
+
@next_scheduled = parsed_time
|
93
|
+
@scheduler.reschedule(self)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Runs the event and cancels the schedule
|
97
|
+
def trigger
|
98
|
+
super()
|
99
|
+
@defer.resolve(:triggered)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Repeat < ScheduledEvent
|
104
|
+
def initialize(scheduler, every)
|
105
|
+
super(scheduler)
|
106
|
+
|
107
|
+
@every = every
|
108
|
+
next_time
|
109
|
+
end
|
110
|
+
|
111
|
+
# Update the time period of the repeating event
|
112
|
+
#
|
113
|
+
# @param schedule [String] a standard CRON job line or a human readable string representing a time period.
|
114
|
+
def update(every, timezone: nil)
|
115
|
+
time = Scheduler.parse_in(every, :quiet) || Scheduler.parse_cron(every, :quiet, timezone: timezone)
|
116
|
+
raise ArgumentError.new("couldn't parse \"#{o}\"") if time.nil?
|
117
|
+
|
118
|
+
@every = time
|
119
|
+
reschedule
|
120
|
+
end
|
121
|
+
|
122
|
+
# removes the event from the schedule
|
123
|
+
def pause
|
124
|
+
@paused = true
|
125
|
+
@scheduler.unschedule(self)
|
126
|
+
end
|
127
|
+
|
128
|
+
# reschedules the event to the next time period
|
129
|
+
# can be used to reset a repeating timer
|
130
|
+
def resume
|
131
|
+
@paused = false
|
132
|
+
@last_scheduled = @reactor.now
|
133
|
+
reschedule
|
134
|
+
end
|
135
|
+
|
136
|
+
# Runs the event and reschedules
|
137
|
+
def trigger
|
138
|
+
super()
|
139
|
+
@reactor.next_tick do
|
140
|
+
# Do this next tick to avoid needless scheduling
|
141
|
+
# if the event is stopped in the callback
|
142
|
+
reschedule
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
|
150
|
+
def next_time
|
151
|
+
@last_scheduled = @reactor.now
|
152
|
+
if @every.is_a? Integer
|
153
|
+
@next_scheduled = @last_scheduled + @every
|
154
|
+
else
|
155
|
+
# must be a cron
|
156
|
+
@next_scheduled = (@every.next.to_f * 1000).to_i - @scheduler.time_diff
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def reschedule
|
161
|
+
unless @paused
|
162
|
+
next_time
|
163
|
+
@scheduler.reschedule(self)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def info
|
168
|
+
"repeat:#{@every.inspect}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
class Scheduler
|
174
|
+
attr_reader :reactor
|
175
|
+
attr_reader :time_diff
|
176
|
+
attr_reader :next
|
177
|
+
|
178
|
+
|
179
|
+
def initialize(reactor)
|
180
|
+
@reactor = reactor
|
181
|
+
@schedules = Set.new
|
182
|
+
@scheduled = []
|
183
|
+
@next = nil # Next schedule time
|
184
|
+
@timer = nil # Reference to the timer
|
185
|
+
|
186
|
+
# Not really required when used correctly
|
187
|
+
@critical = Mutex.new
|
188
|
+
|
189
|
+
# Every hour we should re-calibrate this (just in case)
|
190
|
+
calibrate_time
|
191
|
+
|
192
|
+
@calibrate = @reactor.timer do
|
193
|
+
calibrate_time
|
194
|
+
@calibrate.start(3600000)
|
195
|
+
end
|
196
|
+
@calibrate.start(3600000)
|
197
|
+
@calibrate.unref
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# As the libuv time is taken from an arbitrary point in time we
|
202
|
+
# need to roughly synchronize between it and ruby's Time.now
|
203
|
+
def calibrate_time
|
204
|
+
@reactor.update_time
|
205
|
+
@time_diff = (Time.now.to_f * 1000).to_i - @reactor.now
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create a repeating event that occurs each time period
|
209
|
+
#
|
210
|
+
# @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
|
211
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
212
|
+
# @return [::MTUV::Repeat]
|
213
|
+
def every(time)
|
214
|
+
ms = Scheduler.parse_in(time)
|
215
|
+
event = Repeat.new(self, ms)
|
216
|
+
event.progress &Proc.new if block_given?
|
217
|
+
schedule(event)
|
218
|
+
event
|
219
|
+
end
|
220
|
+
|
221
|
+
# Create a one off event that occurs after the time period
|
222
|
+
#
|
223
|
+
# @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
|
224
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
225
|
+
# @return [::MTUV::OneShot]
|
226
|
+
def in(time)
|
227
|
+
ms = @reactor.now + Scheduler.parse_in(time)
|
228
|
+
event = OneShot.new(self, ms)
|
229
|
+
event.progress &Proc.new if block_given?
|
230
|
+
schedule(event)
|
231
|
+
event
|
232
|
+
end
|
233
|
+
|
234
|
+
# Create a one off event that occurs at a particular date and time
|
235
|
+
#
|
236
|
+
# @param time [String, Time] a representation of a date and time that can be parsed
|
237
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
238
|
+
# @return [::MTUV::OneShot]
|
239
|
+
def at(time)
|
240
|
+
ms = Scheduler.parse_at(time) - @time_diff
|
241
|
+
event = OneShot.new(self, ms)
|
242
|
+
event.progress &Proc.new if block_given?
|
243
|
+
schedule(event)
|
244
|
+
event
|
245
|
+
end
|
246
|
+
|
247
|
+
# Create a repeating event that uses a CRON line to determine the trigger time
|
248
|
+
#
|
249
|
+
# @param schedule [String] a standard CRON job line.
|
250
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
251
|
+
# @return [::MTUV::Repeat]
|
252
|
+
def cron(schedule, timezone: nil)
|
253
|
+
ms = Scheduler.parse_cron(schedule, timezone: timezone)
|
254
|
+
event = Repeat.new(self, ms)
|
255
|
+
event.progress &Proc.new if block_given?
|
256
|
+
schedule(event)
|
257
|
+
event
|
258
|
+
end
|
259
|
+
|
260
|
+
# Schedules an event for execution
|
261
|
+
#
|
262
|
+
# @param event [ScheduledEvent]
|
263
|
+
def reschedule(event)
|
264
|
+
# Check promise is not resolved
|
265
|
+
return if event.resolved?
|
266
|
+
|
267
|
+
@critical.synchronize {
|
268
|
+
# Remove the event from the scheduled list and ensure it is in the schedules set
|
269
|
+
if @schedules.include?(event)
|
270
|
+
remove(event)
|
271
|
+
else
|
272
|
+
@schedules << event
|
273
|
+
end
|
274
|
+
|
275
|
+
# optimal algorithm for inserting into an already sorted list
|
276
|
+
Bisect.insort(@scheduled, event)
|
277
|
+
|
278
|
+
# Update the timer
|
279
|
+
check_timer
|
280
|
+
}
|
281
|
+
end
|
282
|
+
|
283
|
+
# Removes an event from the schedule
|
284
|
+
#
|
285
|
+
# @param event [ScheduledEvent]
|
286
|
+
def unschedule(event)
|
287
|
+
@critical.synchronize {
|
288
|
+
# Only call delete and update the timer when required
|
289
|
+
if @schedules.include?(event)
|
290
|
+
@schedules.delete(event)
|
291
|
+
remove(event)
|
292
|
+
check_timer
|
293
|
+
end
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
|
301
|
+
# Remove an element from the array
|
302
|
+
def remove(obj)
|
303
|
+
position = nil
|
304
|
+
|
305
|
+
@scheduled.each_index do |i|
|
306
|
+
# object level comparison
|
307
|
+
if obj.equal? @scheduled[i]
|
308
|
+
position = i
|
309
|
+
break
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
@scheduled.slice!(position) unless position.nil?
|
314
|
+
end
|
315
|
+
|
316
|
+
# First time schedule we want to bind to the promise
|
317
|
+
def schedule(event)
|
318
|
+
reschedule(event)
|
319
|
+
|
320
|
+
event.finally do
|
321
|
+
unschedule event
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Ensures the current timer, if any, is still
|
326
|
+
# accurate by checking the head of the schedule
|
327
|
+
def check_timer
|
328
|
+
@reactor.update_time
|
329
|
+
|
330
|
+
existing = @next
|
331
|
+
schedule = @scheduled.first
|
332
|
+
@next = schedule.nil? ? nil : schedule.next_scheduled
|
333
|
+
|
334
|
+
if existing != @next
|
335
|
+
# lazy load the timer
|
336
|
+
if @timer.nil?
|
337
|
+
new_timer
|
338
|
+
else
|
339
|
+
@timer.stop
|
340
|
+
end
|
341
|
+
|
342
|
+
if not @next.nil?
|
343
|
+
in_time = @next - @reactor.now
|
344
|
+
|
345
|
+
# Ensure there are never negative start times
|
346
|
+
if in_time > 3
|
347
|
+
@timer.start(in_time)
|
348
|
+
else
|
349
|
+
# Effectively next tick
|
350
|
+
@timer.start(0)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Is called when the libuv timer fires
|
357
|
+
def on_timer
|
358
|
+
@critical.synchronize {
|
359
|
+
schedule = @scheduled.shift
|
360
|
+
@schedules.delete(schedule)
|
361
|
+
schedule.trigger
|
362
|
+
|
363
|
+
# execute schedules that are within 3ms of this event
|
364
|
+
# Basic timer coalescing..
|
365
|
+
now = @reactor.now + 3
|
366
|
+
while @scheduled.first && @scheduled.first.next_scheduled <= now
|
367
|
+
schedule = @scheduled.shift
|
368
|
+
@schedules.delete(schedule)
|
369
|
+
schedule.trigger
|
370
|
+
end
|
371
|
+
check_timer
|
372
|
+
}
|
373
|
+
end
|
374
|
+
|
375
|
+
# Provide some assurances on timer failure
|
376
|
+
def new_timer
|
377
|
+
@timer = @reactor.timer { on_timer }
|
378
|
+
@timer.finally do
|
379
|
+
new_timer
|
380
|
+
unless @next.nil?
|
381
|
+
@timer.start(@next)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddress' # IP Address parser
|
4
|
+
|
5
|
+
module MTUV
|
6
|
+
class TcpServer < ::MTLibuv::TCP
|
7
|
+
def initialize(reactor, server, port, klass, *args)
|
8
|
+
super(reactor)
|
9
|
+
|
10
|
+
@klass = klass
|
11
|
+
@args = args
|
12
|
+
|
13
|
+
if server == port && port.is_a?(Integer)
|
14
|
+
# We are opening a socket descriptor
|
15
|
+
open(server)
|
16
|
+
else
|
17
|
+
# Perform basic checks before attempting to bind address
|
18
|
+
server = '127.0.0.1' if server == 'localhost'
|
19
|
+
if IPAddress.valid? server
|
20
|
+
@server = server
|
21
|
+
bind(server, port) { |client| new_connection(client) }
|
22
|
+
listen(1024)
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Invalid server address #{server}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
|
33
|
+
def new_connection(client)
|
34
|
+
# prevent buffering
|
35
|
+
client.enable_nodelay
|
36
|
+
|
37
|
+
# create the connection class
|
38
|
+
c = @klass.new(client)
|
39
|
+
c.post_init *@args
|
40
|
+
|
41
|
+
# start read after post init and call connected
|
42
|
+
client.start_read
|
43
|
+
c.on_connect(client)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/mt-uv-rays.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mt-libuv'
|
4
|
+
|
5
|
+
module MTUV
|
6
|
+
autoload :Ping, 'mt-uv-rays/ping'
|
7
|
+
|
8
|
+
# In-memory event scheduling
|
9
|
+
autoload :Scheduler, 'mt-uv-rays/scheduler'
|
10
|
+
|
11
|
+
# Intelligent stream buffering
|
12
|
+
autoload :BufferedTokenizer, 'mt-uv-rays/buffered_tokenizer'
|
13
|
+
autoload :AbstractTokenizer, 'mt-uv-rays/abstract_tokenizer'
|
14
|
+
|
15
|
+
# TCP Connections
|
16
|
+
autoload :TcpServer, 'mt-uv-rays/tcp_server'
|
17
|
+
autoload :Connection, 'mt-uv-rays/connection'
|
18
|
+
autoload :TcpConnection, 'mt-uv-rays/connection'
|
19
|
+
autoload :InboundConnection, 'mt-uv-rays/connection'
|
20
|
+
autoload :OutboundConnection, 'mt-uv-rays/connection'
|
21
|
+
autoload :DatagramConnection, 'mt-uv-rays/connection'
|
22
|
+
|
23
|
+
# HTTP related methods
|
24
|
+
autoload :HttpEndpoint, 'mt-uv-rays/http_endpoint'
|
25
|
+
|
26
|
+
|
27
|
+
# @private
|
28
|
+
def self.klass_from_handler(klass, handler = nil, *args)
|
29
|
+
klass = if handler and handler.is_a?(Class)
|
30
|
+
raise ArgumentError, "must provide module or subclass of #{klass.name}" unless klass >= handler
|
31
|
+
handler
|
32
|
+
elsif handler
|
33
|
+
begin
|
34
|
+
handler::UR_CONNECTION_CLASS
|
35
|
+
rescue NameError
|
36
|
+
handler::const_set(:UR_CONNECTION_CLASS, Class.new(klass) {include handler})
|
37
|
+
end
|
38
|
+
else
|
39
|
+
klass
|
40
|
+
end
|
41
|
+
|
42
|
+
arity = klass.instance_method(:post_init).arity
|
43
|
+
expected = arity >= 0 ? arity : -(arity + 1)
|
44
|
+
if (arity >= 0 and args.size != expected) or (arity < 0 and args.size < expected)
|
45
|
+
raise ArgumentError, "wrong number of arguments for #{klass}#post_init (#{args.size} for #{expected})"
|
46
|
+
end
|
47
|
+
|
48
|
+
klass
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def self.connect(server, port, handler, *args)
|
53
|
+
klass = klass_from_handler(OutboundConnection, handler, *args)
|
54
|
+
|
55
|
+
c = klass.new server, port
|
56
|
+
c.post_init *args
|
57
|
+
c
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.start_server(server, port, handler, *args)
|
61
|
+
thread = reactor # Get the reactor running on this thread
|
62
|
+
raise ThreadError, "There is no Libuv reactor running on the current thread" if thread.nil?
|
63
|
+
|
64
|
+
klass = klass_from_handler(InboundConnection, handler, *args)
|
65
|
+
MTUV::TcpServer.new thread, server, port, klass, *args
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.attach_server(sock, handler, *args)
|
69
|
+
thread = reactor # Get the reactor running on this thread
|
70
|
+
raise ThreadError, "There is no Libuv reactor running on the current thread" if thread.nil?
|
71
|
+
|
72
|
+
klass = klass_from_handler(InboundConnection, handler, *args)
|
73
|
+
sd = sock.respond_to?(:fileno) ? sock.fileno : sock
|
74
|
+
|
75
|
+
MTUV::TcpServer.new thread, sd, sd, klass, *args
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.open_datagram_socket(handler, server = nil, port = nil, *args)
|
79
|
+
klass = klass_from_handler(DatagramConnection, handler, *args)
|
80
|
+
|
81
|
+
c = klass.new server, port
|
82
|
+
c.post_init *args
|
83
|
+
c
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module MTLibuv
|
88
|
+
class Reactor
|
89
|
+
def scheduler
|
90
|
+
@scheduler ||= ::MTUV::Scheduler.new(@reactor)
|
91
|
+
@scheduler
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/mt-uv-rays.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path("../lib/mt-uv-rays/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "mt-uv-rays"
|
5
|
+
gem.version = MTUV::VERSION
|
6
|
+
gem.license = 'MIT'
|
7
|
+
gem.authors = ["Giallombardo Nathan"]
|
8
|
+
gem.email = ["nathan.giallombardo@mapotempo.com"]
|
9
|
+
gem.homepage = "https://github.com/mapotempo/mt-uv-rays"
|
10
|
+
gem.summary = "Abstractions for working with Libuv"
|
11
|
+
gem.description = "Opinionated abstractions for Libuv"
|
12
|
+
|
13
|
+
gem.required_ruby_version = '>= 2.0.0'
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
|
16
|
+
gem.add_runtime_dependency 'mt-libuv', '~> 4.1', '>= 4.1.02' # Evented IO
|
17
|
+
gem.add_runtime_dependency 'bisect', '~> 0.1' # Sorted insertion
|
18
|
+
gem.add_runtime_dependency 'tzinfo', '~> 1.2' # Ruby timezones info
|
19
|
+
gem.add_runtime_dependency 'cookiejar', '~> 0.3' # HTTP cookies
|
20
|
+
gem.add_runtime_dependency 'ipaddress', '~> 0.8' # IP address validation
|
21
|
+
gem.add_runtime_dependency 'parse-cron', '~> 0.1' # CRON calculations
|
22
|
+
gem.add_runtime_dependency 'addressable', '~> 2.4' # URI parser
|
23
|
+
gem.add_runtime_dependency 'http-parser', '~> 1.2' # HTTP tokeniser
|
24
|
+
gem.add_runtime_dependency 'activesupport', '>= 4', '< 6'
|
25
|
+
|
26
|
+
# HTTP authentication helpers
|
27
|
+
gem.add_runtime_dependency 'rubyntlm', '~> 0.6'
|
28
|
+
gem.add_runtime_dependency 'net-http-digest_auth', '~> 1.4'
|
29
|
+
|
30
|
+
gem.add_development_dependency 'rspec', '~> 3.5'
|
31
|
+
gem.add_development_dependency 'rake', '~> 11.2'
|
32
|
+
gem.add_development_dependency 'yard', '~> 0.9'
|
33
|
+
gem.add_development_dependency 'httpi', '~> 2.4.2'
|
34
|
+
|
35
|
+
gem.files = Dir["{lib}/**/*"] + %w(Rakefile mt-uv-rays.gemspec README.md LICENSE)
|
36
|
+
gem.test_files = Dir["spec/**/*"]
|
37
|
+
gem.extra_rdoc_files = ["README.md"]
|
38
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'mt-uv-rays'
|
2
|
+
|
3
|
+
describe MTUV::AbstractTokenizer do
|
4
|
+
before :each do
|
5
|
+
@buffer = MTUV::AbstractTokenizer.new({
|
6
|
+
indicator: "Start",
|
7
|
+
callback: lambda { |data|
|
8
|
+
return 4 if data.length > 3
|
9
|
+
return false
|
10
|
+
},
|
11
|
+
size_limit: 10,
|
12
|
+
encoding: "ASCII-8BIT"
|
13
|
+
})
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should not return anything when a complete message is not available" do
|
17
|
+
msg1 = "test"
|
18
|
+
|
19
|
+
result = @buffer.extract(msg1)
|
20
|
+
expect(result).to eq([])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not return anything when an empty message is present" do
|
24
|
+
msg1 = "test"
|
25
|
+
|
26
|
+
result = @buffer.extract(msg1)
|
27
|
+
expect(result).to eq([])
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should tokenize messages where the data is a complete message" do
|
31
|
+
msg1 = "Start1234"
|
32
|
+
|
33
|
+
result = @buffer.extract(msg1)
|
34
|
+
expect(result).to eq(['1234'])
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return multiple complete messages" do
|
38
|
+
msg1 = "Start1234Start123456"
|
39
|
+
|
40
|
+
result = @buffer.extract(msg1)
|
41
|
+
expect(result).to eq(['1234', '1234'])
|
42
|
+
expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should not return junk data" do
|
46
|
+
msg1 = "Start123456Start123456"
|
47
|
+
|
48
|
+
result = @buffer.extract(msg1)
|
49
|
+
expect(result).to eq(['1234', '1234'])
|
50
|
+
expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should tokenize messages where the indicator is split" do
|
54
|
+
msg1 = "123Star"
|
55
|
+
msg2 = "twhoaStart1234"
|
56
|
+
|
57
|
+
result = @buffer.extract(msg1)
|
58
|
+
expect(result).to eq([])
|
59
|
+
result = @buffer.extract(msg2)
|
60
|
+
expect(result).to eq(['whoa', '1234'])
|
61
|
+
|
62
|
+
msg1 = "123Star"
|
63
|
+
msg2 = "twhoaSt"
|
64
|
+
msg3 = "art1234"
|
65
|
+
|
66
|
+
result = @buffer.extract(msg1)
|
67
|
+
expect(result).to eq([])
|
68
|
+
result = @buffer.extract(msg2)
|
69
|
+
expect(result).to eq(['whoa'])
|
70
|
+
result = @buffer.extract(msg3)
|
71
|
+
expect(result).to eq(['1234'])
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should empty the buffer if the limit is exceeded" do
|
75
|
+
result = @buffer.extract('1234567890G')
|
76
|
+
expect(result).to eq([])
|
77
|
+
|
78
|
+
# We keep enough to match a possible partial indicator
|
79
|
+
expect(@buffer.flush).to eq('890G')
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should work with regular expressions" do
|
83
|
+
@buffer = MTUV::AbstractTokenizer.new({
|
84
|
+
indicator: /Start/i,
|
85
|
+
callback: lambda { |data|
|
86
|
+
return 4 if data.length > 3
|
87
|
+
return false
|
88
|
+
}
|
89
|
+
})
|
90
|
+
|
91
|
+
result = @buffer.extract('1234567starta')
|
92
|
+
expect(result).to eq([])
|
93
|
+
result = @buffer.extract('bcd')
|
94
|
+
expect(result).to eq(['abcd'])
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should work where the indicator is part of the message" do
|
98
|
+
# i.e. We are looking for \x02\x06\xLEN\x00\x00\x02\x06\x00\xEND
|
99
|
+
# Where the indicator may appear as part of the message body
|
100
|
+
|
101
|
+
msg1 = "StartStartStart123456"
|
102
|
+
|
103
|
+
result = @buffer.extract(msg1)
|
104
|
+
expect(result).to eq(['Star', '1234'])
|
105
|
+
expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should work with messages that don't have an indicator" do
|
109
|
+
# i.e. there is a header that defines message length
|
110
|
+
msg1 = "\x0612345\x081234567\x031"
|
111
|
+
|
112
|
+
@buffer = MTUV::AbstractTokenizer.new({
|
113
|
+
callback: lambda { |data|
|
114
|
+
len = data.bytes[0]
|
115
|
+
if data.length >= len
|
116
|
+
len
|
117
|
+
else
|
118
|
+
false
|
119
|
+
end
|
120
|
+
},
|
121
|
+
size_limit: 10,
|
122
|
+
encoding: "ASCII-8BIT"
|
123
|
+
})
|
124
|
+
|
125
|
+
result = @buffer.extract(msg1)
|
126
|
+
expect(result).to eq(["\x0612345", "\x081234567"])
|
127
|
+
expect(@buffer.flush).to eq("\x031") # as we've indicated a message length of 3
|
128
|
+
end
|
129
|
+
end
|