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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 83172a1213c66ea2b83c177a7f7a374498d8167a286ce71bd829fc5786ccd0bb
|
4
|
+
data.tar.gz: 7b9b3620628d7587e06f38ac57bfc9aa7037d1442d3defdf0d6ba95d5cadebde
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2225d0202e4ff24e1da86b9be15c1d81baf01a00548006f557da22c3032c9486a0b78124bf2853a253c70f9aed538411fb8dbe2f0c7d3ed2987a5956d2d22caf
|
7
|
+
data.tar.gz: 4ce9366261183a0ab2699da88fc404e8d3c52ef9d7073180ebf7547e3f90cf940b256755d61c45c159f51e9954072e6b666ebcc665786e7ff46ef0d4dffe62bd
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 CoTag Media
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# uv-rays
|
2
|
+
|
3
|
+
[](https://travis-ci.org/cotag/uv-rays)
|
4
|
+
|
5
|
+
UV-Rays was designed to eliminate the complexities of high-performance threaded network programming, allowing engineers to concentrate on their application logic.
|
6
|
+
|
7
|
+
|
8
|
+
## Core Features
|
9
|
+
|
10
|
+
1. TCP (and UDP) Connection abstractions
|
11
|
+
2. Advanced stream tokenization
|
12
|
+
3. Scheduled events (in, at, every, cron)
|
13
|
+
4. HTTP 1.1 compatible client support
|
14
|
+
|
15
|
+
This adds to the features already available from [Libuv](https://github.com/cotag/libuv) on which the gem is based
|
16
|
+
|
17
|
+
|
18
|
+
## Support
|
19
|
+
|
20
|
+
UV-Rays supports all platforms where ruby is available. Linux, OSX, BSD and Windows. MRI, jRuby and Rubinius.
|
21
|
+
|
22
|
+
Run `gem install uv-rays` to install
|
23
|
+
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
Here's a fully-functional echo server written with UV-Rays:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'uv-rays'
|
31
|
+
|
32
|
+
module EchoServer
|
33
|
+
def on_connect(socket)
|
34
|
+
@ip, @port = socket.peername
|
35
|
+
logger.info "-- #{@ip}:#{@port} connected"
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_read(data, socket)
|
39
|
+
write ">>>you sent: #{data}"
|
40
|
+
close_connection if data =~ /quit/i
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_close
|
44
|
+
puts "-- #{@ip}:#{@port} disconnected"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
reactor {
|
49
|
+
UV.start_server "127.0.0.1", 8081, EchoServer
|
50
|
+
}
|
51
|
+
|
52
|
+
```
|
53
|
+
|
54
|
+
# Integrations
|
55
|
+
|
56
|
+
UV-Rays works with many existing GEMs by integrating into common HTTP abstraction libraries
|
57
|
+
|
58
|
+
* [Faraday](https://github.com/lostisland/faraday)
|
59
|
+
* [HTTPI](https://github.com/savonrb/httpi)
|
60
|
+
* [Handsoap](https://github.com/unwire/handsoap)
|
61
|
+
|
62
|
+
|
63
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec/core/rake_task' # testing framework
|
3
|
+
require 'yard' # yard documentation
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
# By default we don't run network tests
|
8
|
+
task :default => :limited_spec
|
9
|
+
RSpec::Core::RakeTask.new(:limited_spec) do |t|
|
10
|
+
# Exclude network tests
|
11
|
+
t.rspec_opts = "--tag ~mri_only --tag ~travis_skip"
|
12
|
+
end
|
13
|
+
RSpec::Core::RakeTask.new(:spec)
|
14
|
+
|
15
|
+
|
16
|
+
desc "Run all tests"
|
17
|
+
task :test => [:spec]
|
18
|
+
|
19
|
+
|
20
|
+
YARD::Rake::YardocTask.new do |t|
|
21
|
+
t.files = ['lib/**/*.rb', '-', 'ext/README.md', 'README.md']
|
22
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'mt-uv-rays'
|
3
|
+
|
4
|
+
|
5
|
+
module Faraday
|
6
|
+
class Adapter < Middleware
|
7
|
+
register_middleware libuv: :MTLibuv
|
8
|
+
|
9
|
+
class MTLibuv < Faraday::Adapter
|
10
|
+
def initialize(app, connection_options = {})
|
11
|
+
@connection_options = connection_options
|
12
|
+
super(app)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
super
|
17
|
+
|
18
|
+
opts = {}
|
19
|
+
if env[:url].scheme == 'https' && ssl = env[:ssl]
|
20
|
+
tls_opts = opts[:tls_options] = {}
|
21
|
+
|
22
|
+
# opts[:ssl_verify_peer] = !!ssl.fetch(:verify, true)
|
23
|
+
# TODO:: Need to provide verify callbacks
|
24
|
+
|
25
|
+
tls_opts[:cert_chain] = ssl[:ca_path] if ssl[:ca_path]
|
26
|
+
tls_opts[:client_ca] = ssl[:ca_file] if ssl[:ca_file]
|
27
|
+
#tls_opts[:client_cert] = ssl[:client_cert] if ssl[:client_cert]
|
28
|
+
#tls_opts[:client_key] = ssl[:client_key] if ssl[:client_key]
|
29
|
+
#tls_opts[:certificate] = ssl[:certificate] if ssl[:certificate]
|
30
|
+
tls_opts[:private_key] = ssl[:private_key] if ssl[:private_key]
|
31
|
+
end
|
32
|
+
|
33
|
+
if (req = env[:request])
|
34
|
+
opts[:inactivity_timeout] = (req[:timeout] * 1000) if req[:timeout]
|
35
|
+
end
|
36
|
+
|
37
|
+
if proxy = env[:request][:proxy]
|
38
|
+
opts[:proxy] = {
|
39
|
+
host: proxy[:uri].host,
|
40
|
+
port: proxy[:uri].port,
|
41
|
+
username: proxy[:user],
|
42
|
+
password: proxy[:password]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
error = nil
|
47
|
+
thread = reactor
|
48
|
+
if thread.running?
|
49
|
+
error = perform_request(env, opts)
|
50
|
+
else
|
51
|
+
# Pretty much here for testing
|
52
|
+
thread.run {
|
53
|
+
error = perform_request(env, opts)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Re-raise the error out of the event loop
|
58
|
+
# Really this is only required for tests as this will always run on the reactor
|
59
|
+
raise error if error
|
60
|
+
@app.call env
|
61
|
+
rescue ::CoroutineRejection => err
|
62
|
+
if err.value == :timeout
|
63
|
+
raise Error::TimeoutError, err
|
64
|
+
else
|
65
|
+
raise Error::ConnectionFailed, err
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: support streaming requests
|
70
|
+
def read_body(env)
|
71
|
+
env[:body].respond_to?(:read) ? env[:body].read : env[:body]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def perform_request(env, opts)
|
76
|
+
conn = ::UV::HttpEndpoint.new(env[:url].to_s, opts.merge!(@connection_options))
|
77
|
+
resp = conn.request(env[:method].to_s.downcase.to_sym,
|
78
|
+
headers: env[:request_headers],
|
79
|
+
path: "/#{env[:url].to_s.split('/', 4)[-1]}",
|
80
|
+
keepalive: false,
|
81
|
+
body: read_body(env)).value
|
82
|
+
|
83
|
+
save_response(env, resp.status.to_i, resp.body, resp) #, resp.reason_phrase)
|
84
|
+
nil
|
85
|
+
rescue Exception => e
|
86
|
+
e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'handsoap'
|
5
|
+
|
6
|
+
module Handsoap
|
7
|
+
module Http
|
8
|
+
module Drivers
|
9
|
+
class MTLibuvDriver < AbstractDriver
|
10
|
+
def self.load!
|
11
|
+
require 'mt-uv-rays'
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_http_request_async(request)
|
15
|
+
endp = ::UV::HttpEndpoint.new(request.url)
|
16
|
+
|
17
|
+
if request.username && request.password
|
18
|
+
request.headers['Authorization'] = [request.username, request.password]
|
19
|
+
end
|
20
|
+
|
21
|
+
req = endp.request(request.http_method, {
|
22
|
+
headers: request.headers,
|
23
|
+
body: request.body
|
24
|
+
})
|
25
|
+
|
26
|
+
deferred = ::Handsoap::Deferred.new
|
27
|
+
req.then do |resp|
|
28
|
+
# Downcase headers and convert values to arrays
|
29
|
+
headers = Hash[resp.map { |k, v| [k.to_s.downcase, Array(v)] }]
|
30
|
+
http_response = parse_http_part(headers, resp.body, resp.status)
|
31
|
+
deferred.trigger_callback http_response
|
32
|
+
end
|
33
|
+
req.catch do |err|
|
34
|
+
deferred.trigger_errback err
|
35
|
+
end
|
36
|
+
deferred
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@@drivers[:libuv] = ::Handsoap::Http::Drivers::MTLibuvDriver
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'httpi'
|
2
|
+
|
3
|
+
module HTTPI; end
|
4
|
+
module HTTPI::Adapter; end
|
5
|
+
class HTTPI::Adapter::MTLibuv < HTTPI::Adapter::Base
|
6
|
+
register :mtlibuv, deps: %w(mt-uv-rays)
|
7
|
+
|
8
|
+
def initialize(request)
|
9
|
+
@request = request
|
10
|
+
@client = ::MTUV::HttpEndpoint.new request.url
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :client
|
14
|
+
|
15
|
+
def request(method)
|
16
|
+
@client.inactivity_timeout = (@request.read_timeout * 1000).to_i if @request.read_timeout && @request.read_timeout > 0
|
17
|
+
|
18
|
+
req = {
|
19
|
+
path: @request.url,
|
20
|
+
headers: @request.headers,
|
21
|
+
body: @request.body
|
22
|
+
}
|
23
|
+
|
24
|
+
if proxy = @request.proxy
|
25
|
+
req[:proxy] = {
|
26
|
+
host: proxy.host,
|
27
|
+
port: proxy.port,
|
28
|
+
username: proxy.user,
|
29
|
+
password: proxy.password
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Apply authentication settings
|
34
|
+
auth = @request.auth
|
35
|
+
type = auth.type
|
36
|
+
if auth.type
|
37
|
+
creds = auth.credentials
|
38
|
+
|
39
|
+
case auth.type
|
40
|
+
when :basic
|
41
|
+
req[:headers][:Authorization] = creds
|
42
|
+
when :digest
|
43
|
+
req[:digest] = {
|
44
|
+
user: creds[0],
|
45
|
+
password: creds[1]
|
46
|
+
}
|
47
|
+
when :ntlm
|
48
|
+
req[:ntlm] = {
|
49
|
+
username: creds[0],
|
50
|
+
password: creds[1],
|
51
|
+
domain: creds[2] || ''
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Apply Client certificates
|
57
|
+
ssl = auth.ssl
|
58
|
+
if ssl.verify_mode == :peer
|
59
|
+
tls_opts = req[:tls_options] = {}
|
60
|
+
tls_opts[:cert_chain] = ssl.cert.to_pem if ssl.cert
|
61
|
+
tls_opts[:client_ca] = ssl.ca_cert_file if ssl.ca_cert_file
|
62
|
+
tls_opts[:private_key] = ssl.cert_key.to_pem if ssl.cert_key
|
63
|
+
end
|
64
|
+
|
65
|
+
# Use co-routines to make non-blocking requests
|
66
|
+
response = @client.request(method, req).value
|
67
|
+
::HTTPI::Response.new(response.status, response, response.body)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MTUV
|
5
|
+
|
6
|
+
# AbstractTokenizer is similar to BufferedTokernizer however should
|
7
|
+
# only be used when there is no delimiter to work with. It uses a
|
8
|
+
# callback based system for application level tokenization without
|
9
|
+
# the heavy lifting.
|
10
|
+
class AbstractTokenizer
|
11
|
+
DEFAULT_ENCODING = 'ASCII-8BIT'
|
12
|
+
|
13
|
+
attr_accessor :callback, :indicator, :size_limit, :verbose
|
14
|
+
|
15
|
+
# @param [Hash] options
|
16
|
+
def initialize(options)
|
17
|
+
@callback = options[:callback]
|
18
|
+
@indicator = options[:indicator]
|
19
|
+
@size_limit = options[:size_limit]
|
20
|
+
@verbose = options[:verbose] if @size_limit
|
21
|
+
@encoding = options[:encoding] || DEFAULT_ENCODING
|
22
|
+
|
23
|
+
raise ArgumentError, 'no callback provided' unless @callback
|
24
|
+
|
25
|
+
reset
|
26
|
+
if @indicator.is_a?(String)
|
27
|
+
@indicator = String.new(@indicator).force_encoding(@encoding).freeze
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Extract takes an arbitrary string of input data and returns an array of
|
32
|
+
# tokenized entities using a message start indicator
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
#
|
36
|
+
# tokenizer.extract(data).
|
37
|
+
# map { |entity| Decode(entity) }.each { ... }
|
38
|
+
#
|
39
|
+
# @param [String] data
|
40
|
+
def extract(data)
|
41
|
+
data.force_encoding(@encoding)
|
42
|
+
@input << data
|
43
|
+
|
44
|
+
entities = []
|
45
|
+
|
46
|
+
loop do
|
47
|
+
found = false
|
48
|
+
|
49
|
+
last = if @indicator
|
50
|
+
check = @input.partition(@indicator)
|
51
|
+
break unless check[1].length > 0
|
52
|
+
|
53
|
+
check[2]
|
54
|
+
else
|
55
|
+
@input
|
56
|
+
end
|
57
|
+
|
58
|
+
result = @callback.call(last)
|
59
|
+
|
60
|
+
if result
|
61
|
+
found = true
|
62
|
+
|
63
|
+
# Check for multi-byte indicator edge case
|
64
|
+
case result
|
65
|
+
when Integer
|
66
|
+
entities << last[0...result]
|
67
|
+
@input = last[result..-1]
|
68
|
+
else
|
69
|
+
entities << last
|
70
|
+
reset
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
break if not found
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check to see if the buffer has exceeded capacity, if we're imposing a limit
|
78
|
+
if @size_limit && @input.size > @size_limit
|
79
|
+
if @indicator.respond_to?(:length) # check for regex
|
80
|
+
# save enough of the buffer that if one character of the indicator were
|
81
|
+
# missing we would match on next extract (very much an edge case) and
|
82
|
+
# best we can do with a full buffer.
|
83
|
+
@input = @input[-(@indicator.length - 1)..-1]
|
84
|
+
else
|
85
|
+
reset
|
86
|
+
end
|
87
|
+
raise 'input buffer exceeded limit' if @verbose
|
88
|
+
end
|
89
|
+
|
90
|
+
return entities
|
91
|
+
end
|
92
|
+
|
93
|
+
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
94
|
+
# a token has not yet been encountered.
|
95
|
+
#
|
96
|
+
# @return [String]
|
97
|
+
def flush
|
98
|
+
buffer = @input
|
99
|
+
reset
|
100
|
+
buffer
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Boolean]
|
104
|
+
def empty?
|
105
|
+
@input.empty?
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Integer]
|
109
|
+
def bytesize
|
110
|
+
@input.bytesize
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
|
117
|
+
def reset
|
118
|
+
@input = String.new.force_encoding(@encoding)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# BufferedTokenizer takes a delimiter upon instantiation.
|
5
|
+
# It allows input to be spoon-fed from some outside source which receives
|
6
|
+
# arbitrary length datagrams which may-or-may-not contain the token by which
|
7
|
+
# entities are delimited.
|
8
|
+
#
|
9
|
+
# @example Using BufferedTokernizer to parse lines out of incoming data
|
10
|
+
#
|
11
|
+
# module LineBufferedConnection
|
12
|
+
# def receive_data(data)
|
13
|
+
# (@buffer ||= BufferedTokenizer.new(delimiter: "\n")).extract(data).each do |line|
|
14
|
+
# receive_line(line)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
module MTUV
|
19
|
+
class BufferedTokenizer
|
20
|
+
DEFAULT_ENCODING = 'ASCII-8BIT'
|
21
|
+
|
22
|
+
attr_accessor :delimiter, :indicator, :size_limit, :verbose
|
23
|
+
|
24
|
+
# @param [Hash] options
|
25
|
+
def initialize(options)
|
26
|
+
@delimiter = options[:delimiter]
|
27
|
+
@indicator = options[:indicator]
|
28
|
+
@msg_length = options[:msg_length]
|
29
|
+
@size_limit = options[:size_limit]
|
30
|
+
@min_length = options[:min_length] || 1
|
31
|
+
@verbose = options[:verbose] if @size_limit
|
32
|
+
@encoding = options[:encoding] || DEFAULT_ENCODING
|
33
|
+
|
34
|
+
if @delimiter
|
35
|
+
@extract_method = method(:delimiter_extract)
|
36
|
+
elsif @indicator && @msg_length
|
37
|
+
@extract_method = method(:length_extract)
|
38
|
+
else
|
39
|
+
raise ArgumentError, 'no delimiter provided'
|
40
|
+
end
|
41
|
+
|
42
|
+
init_buffer
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extract takes an arbitrary string of input data and returns an array of
|
46
|
+
# tokenized entities, provided there were any available to extract.
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
#
|
50
|
+
# tokenizer.extract(data).
|
51
|
+
# map { |entity| Decode(entity) }.each { ... }
|
52
|
+
#
|
53
|
+
# @param [String] data
|
54
|
+
def extract(data)
|
55
|
+
data.force_encoding(@encoding)
|
56
|
+
@input << data
|
57
|
+
|
58
|
+
@extract_method.call
|
59
|
+
end
|
60
|
+
|
61
|
+
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
62
|
+
# a token has not yet been encountered.
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
def flush
|
66
|
+
buffer = @input
|
67
|
+
reset
|
68
|
+
buffer
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Boolean]
|
72
|
+
def empty?
|
73
|
+
@input.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer]
|
77
|
+
def bytesize
|
78
|
+
@input.bytesize
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
|
85
|
+
def delimiter_extract
|
86
|
+
# Extract token-delimited entities from the input string with the split command.
|
87
|
+
# There's a bit of craftiness here with the -1 parameter. Normally split would
|
88
|
+
# behave no differently regardless of if the token lies at the very end of the
|
89
|
+
# input buffer or not (i.e. a literal edge case) Specifying -1 forces split to
|
90
|
+
# return "" in this case, meaning that the last entry in the list represents a
|
91
|
+
# new segment of data where the token has not been encountered
|
92
|
+
messages = @input.split(@delimiter, -1)
|
93
|
+
|
94
|
+
if @indicator
|
95
|
+
@input = messages.pop || empty_string
|
96
|
+
entities = []
|
97
|
+
messages.each do |msg|
|
98
|
+
res = msg.split(@indicator, -1)
|
99
|
+
entities << res.last if res.length > 1
|
100
|
+
end
|
101
|
+
else
|
102
|
+
entities = messages
|
103
|
+
@input = entities.pop || empty_string
|
104
|
+
end
|
105
|
+
|
106
|
+
check_buffer_limits
|
107
|
+
|
108
|
+
# Check min-length is met
|
109
|
+
entities.select! {|msg| msg.length >= @min_length}
|
110
|
+
|
111
|
+
return entities
|
112
|
+
end
|
113
|
+
|
114
|
+
def length_extract
|
115
|
+
messages = @input.split(@indicator, -1)
|
116
|
+
messages.shift # discard junk data
|
117
|
+
|
118
|
+
last = messages.pop || empty_string
|
119
|
+
|
120
|
+
# Select messages of the right size then remove junk data
|
121
|
+
messages.select! { |msg| msg.length >= @msg_length ? true : false }
|
122
|
+
messages.map! { |msg| msg[0...@msg_length] }
|
123
|
+
|
124
|
+
if last.length >= @msg_length
|
125
|
+
messages << last[0...@msg_length]
|
126
|
+
@input = last[@msg_length..-1]
|
127
|
+
else
|
128
|
+
reset("#{@indicator}#{last}")
|
129
|
+
end
|
130
|
+
|
131
|
+
check_buffer_limits
|
132
|
+
|
133
|
+
return messages
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check to see if the buffer has exceeded capacity, if we're imposing a limit
|
137
|
+
def check_buffer_limits
|
138
|
+
if @size_limit && @input.size > @size_limit
|
139
|
+
if @indicator && @indicator.respond_to?(:length) # check for regex
|
140
|
+
# save enough of the buffer that if one character of the indicator were
|
141
|
+
# missing we would match on next extract (very much an edge case) and
|
142
|
+
# best we can do with a full buffer. If we were one char short of a
|
143
|
+
# delimiter it would be unfortunate
|
144
|
+
@input = @input[-(@indicator.length - 1)..-1]
|
145
|
+
else
|
146
|
+
reset
|
147
|
+
end
|
148
|
+
raise 'input buffer exceeded limit' if @verbose
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def init_buffer
|
153
|
+
@input = empty_string
|
154
|
+
|
155
|
+
if @delimiter.is_a?(String)
|
156
|
+
@delimiter = String.new(@delimiter).force_encoding(@encoding).freeze
|
157
|
+
end
|
158
|
+
|
159
|
+
if @indicator.is_a?(String)
|
160
|
+
@indicator = String.new(@indicator).force_encoding(@encoding).freeze
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def reset(value = nil)
|
165
|
+
@input = String.new(value || '').force_encoding(@encoding)
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
|
172
|
+
def empty_string
|
173
|
+
String.new.force_encoding(@encoding)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|