mt-uv-rays 2.4.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/cotag/uv-rays.svg?branch=master)](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
|