lightstreamer 0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bf90908d8a97af1417f8391f16e2c26a10baf73d
4
+ data.tar.gz: 6e89354b80f3712ca9c4278f09342123d3844878
5
+ SHA512:
6
+ metadata.gz: a17d1e9276c986f6c436e3aad883f4f068b319e9e88e30b7e7eb844b1af33ca18c35dab74a76e2e25763263199c7f251acf45d999a07afeb749b424d6ea58985
7
+ data.tar.gz: d359e20a661d5dc953f5ba02d3984f4db0206886b38ebdc1ab555c20c5b367eab9aa8499071eef6ba6f074da116925cf2774742c043e314bdd21f419126535a9
@@ -0,0 +1,5 @@
1
+ # Lightstreamer Changelog
2
+
3
+ ### 0.1 — July 23, 2016
4
+
5
+ - Initial release
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2015-2016 Richard Viney
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,109 @@
1
+ # Ruby Lightstreamer Client Gem
2
+
3
+ [![Gem][gem-badge]][gem-link]
4
+ [![Build Status][travis-ci-badge]][travis-ci-link]
5
+ [![Test Coverage][test-coverage-badge]][test-coverage-link]
6
+ [![Code Climate][code-climate-badge]][code-climate-link]
7
+ [![Dependencies][dependencies-badge]][dependencies-link]
8
+ [![Documentation][documentation-badge]][documentation-link]
9
+ [![License][license-badge]][license-link]
10
+
11
+ Easily interface with a Lightstreamer service from Ruby. Written against the
12
+ [official API specification](http://www.lightstreamer.com/docs/client_generic_base/Network%20Protocol%20Tutorial.pdf).
13
+
14
+ ## License
15
+
16
+ Licensed under the MIT license. You must read and agree to its terms to use this software.
17
+
18
+ ## Installation
19
+
20
+ Install the latest version of the `lightstreamer` gem with the following command:
21
+
22
+ ```
23
+ $ gem install lightstreamer
24
+ ```
25
+
26
+ ## Usage — Library
27
+
28
+ The two primary classes that make up the public API are:
29
+
30
+ - [`Lightstreamer::Session`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Session)
31
+ - [`Lightstreamer::Subscription`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Subscription)
32
+
33
+ The following code snippet demonstrates how to setup a Lightstreamer session, a subscription, then print streaming
34
+ output as it comes in.
35
+
36
+ ```ruby
37
+ require 'lightstreamer'
38
+
39
+ # Create a new session that connects to the Lightstreamer demo server, which needs no authentication
40
+ session = Lightstreamer::Session.new server_url: 'http://push.lightstreamer.com',
41
+ adapter_set: 'DEMO', username: '', password: ''
42
+
43
+ # Connect the session
44
+ session.connect
45
+
46
+ # Create a new subscription that subscribes to five items and to four fields on each item
47
+ subscription = Lightstreamer::Subscription.new items: %w(item1 item2 item3 item4 item5),
48
+ fields: [:time, :stock_name, :bid, :ask],
49
+ mode: :merge, adapter: 'QUOTE_ADAPTER'
50
+
51
+ # Create a thread-safe queue
52
+ queue = Queue.new
53
+
54
+ # When new data becomes available for the subscription it will be put on the queue. This callback
55
+ # will be run on a worker thread.
56
+ subscription.add_data_callback do |subscription, item_name, item_data, new_values|
57
+ queue.push item_data
58
+ end
59
+
60
+ # Activate the subscription
61
+ session.subscribe subscription
62
+
63
+ # Loop printing out new data as soon as it becomes available on the queue
64
+ loop do
65
+ data = queue.pop
66
+ puts "#{data[:time]} - #{data[:stock_name]} - bid: #{data[:bid]}, ask: #{data[:ask]}"
67
+ end
68
+ ```
69
+
70
+ ## Usage — Command-Line Client
71
+
72
+ This gem provides a simple command-line client that can connect to a Lightstreamer server, activate a
73
+ subscription, then print streaming output from the server as it becomes available.
74
+
75
+ To print streaming data from the demo server run the following command:
76
+
77
+ ```
78
+ lightstreamer --address http://push.lightstreamer.com --adapter-set DEMO --adapter QUOTE_ADAPTER \
79
+ --items item1 item2 item3 item4 item5 --fields time stock_name bid ask bid
80
+ ```
81
+
82
+ To see a full list of available options run the following command:
83
+
84
+ ```
85
+ lightstreamer help stream
86
+ ```
87
+
88
+ ## Documentation
89
+
90
+ API documentation is available [here](http://www.rubydoc.info/github/rviney/lightstreamer).
91
+
92
+ ## Contributors
93
+
94
+ Gem created by Richard Viney. All contributions welcome.
95
+
96
+ [gem-link]: https://rubygems.org/gems/lightstreamer
97
+ [gem-badge]: https://badge.fury.io/rb/lightstreamer.svg
98
+ [travis-ci-link]: http://travis-ci.org/rviney/lightstreamer
99
+ [travis-ci-badge]: https://travis-ci.org/rviney/lightstreamer.svg?branch=master
100
+ [test-coverage-link]: https://codeclimate.com/github/rviney/lightstreamer/coverage
101
+ [test-coverage-badge]: https://codeclimate.com/github/rviney/lightstreamer/badges/coverage.svg
102
+ [code-climate-link]: https://codeclimate.com/github/rviney/lightstreamer
103
+ [code-climate-badge]: https://codeclimate.com/github/rviney/lightstreamer/badges/gpa.svg
104
+ [dependencies-link]: https://gemnasium.com/rviney/lightstreamer
105
+ [dependencies-badge]: https://gemnasium.com/rviney/lightstreamer.svg
106
+ [documentation-link]: https://inch-ci.org/github/rviney/lightstreamer
107
+ [documentation-badge]: https://inch-ci.org/github/rviney/lightstreamer.svg?branch=master
108
+ [license-link]: https://github.com/rviney/lightstreamer/blob/master/LICENSE.md
109
+ [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(File.realpath(__FILE__)) + '/../lib'
4
+
5
+ require 'lightstreamer'
6
+
7
+ Lightstreamer::CLI::Main.bootstrap ARGV
@@ -0,0 +1,18 @@
1
+ require 'rest-client'
2
+ require 'thor'
3
+
4
+ require 'lightstreamer/control_connection'
5
+ require 'lightstreamer/line_buffer'
6
+ require 'lightstreamer/protocol_error'
7
+ require 'lightstreamer/request_error'
8
+ require 'lightstreamer/session'
9
+ require 'lightstreamer/stream_connection'
10
+ require 'lightstreamer/subscription'
11
+ require 'lightstreamer/version'
12
+
13
+ require 'lightstreamer/cli/main'
14
+ require 'lightstreamer/cli/stream_command'
15
+
16
+ # This module contains all the code for the Lightstreamer gem. See `README.md` to get started with using this gem.
17
+ module Lightstreamer
18
+ end
@@ -0,0 +1,24 @@
1
+ module Lightstreamer
2
+ # This module contains the code for the CLI frontend. See `README.md` for usage details.
3
+ module CLI
4
+ # Implements the `lightstreamer` command-line client.
5
+ class Main < Thor
6
+ default_task :stream
7
+
8
+ class << self
9
+ # This is the initial entry point for the execution of the command-line client. It is responsible for the
10
+ # --version/-v options and then invoking the main application.
11
+ #
12
+ # @param [Array<String>] argv The array of command-line arguments.
13
+ def bootstrap(argv)
14
+ if argv.index('--version') || argv.index('-v')
15
+ puts VERSION
16
+ exit
17
+ end
18
+
19
+ start argv
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ module Lightstreamer
2
+ module CLI
3
+ # Implements the `lightstreamer stream` command.
4
+ class Main < Thor
5
+ desc 'stream', 'Streams a set of items and fields from a Lightstreamer server and prints the live output'
6
+
7
+ option :address, required: true, desc: 'The address of the Lightstreamer server'
8
+ option :username, desc: 'The username for the session'
9
+ option :password, desc: 'The password for the session'
10
+ option :adapter_set, desc: 'The name of the adapter set for the session'
11
+ option :adapter, desc: 'The name of the data adapter to stream data from'
12
+ option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
13
+ option :fields, type: :array, required: true, desc: 'The field(s) to stream'
14
+ option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
15
+
16
+ def stream
17
+ session = create_session
18
+ session.connect
19
+
20
+ @queue = Queue.new
21
+
22
+ session.subscribe create_subscription
23
+
24
+ loop do
25
+ puts @queue.pop
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Creates a new session from the specified options.
32
+ def create_session
33
+ Lightstreamer::Session.new server_url: options[:address], username: options[:username],
34
+ password: options[:password], adapter_set: options[:adapter_set]
35
+ end
36
+
37
+ # Creates a new subscription from the specified options.
38
+ def create_subscription
39
+ subscription = Lightstreamer::Subscription.new items: options[:items], fields: options[:fields],
40
+ mode: options[:mode], adapter: options[:adapter]
41
+
42
+ subscription.add_data_callback do |_subscription, item_name, _item_data, new_values|
43
+ @queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
44
+ end
45
+
46
+ subscription
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ module Lightstreamer
2
+ # This is an internal class used by {Session} and is responsible for sending Lightstreamer control requests.
3
+ class ControlConnection
4
+ # Initializes this class for sending Lightstreamer control requests using the specified session ID and control
5
+ # address.
6
+ #
7
+ # @param [String] session_id The Lightstreamer session ID.
8
+ # @param [String] control_url The URL of the server to send Lightstreamer control requests to.
9
+ def initialize(session_id, control_url)
10
+ @session_id = session_id
11
+ @control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
12
+ end
13
+
14
+ # Sends a Lightstreamer control request with the specified options. If an error occurs then {RequestError} or
15
+ # {ProtocolError} will be raised.
16
+ #
17
+ # @param [Hash] options The control request options.
18
+ # @option options [Fixnum] :table The ID of the table this request pertains to. Required.
19
+ # @option options [:add, :add_silent, :start, :delete] :operation The operation to perform. Required.
20
+ # @option options [String] :adapter The name of the data adapter to use. Optional.
21
+ # @option options [Array<String>] :items The names of the items that this request pertains to. Required if
22
+ # `:operation` is `:add` or `:add_silent`.
23
+ # @option options [Array<String>] :fields The names of the fields that this request pertains to. Required if
24
+ # `:operation` is `:add` or `:add_silent`.
25
+ # @option options [:raw, :merge, :distinct, :command] :mode The subscription mode.
26
+ def execute(options)
27
+ result = execute_post_request build_payload(options)
28
+
29
+ return if result.first == 'OK'
30
+ raise ProtocolError, result[2], result[1] if result.first == 'ERROR'
31
+
32
+ warn "Lightstreamer: unexpected response from server: #{result}"
33
+ end
34
+
35
+ private
36
+
37
+ # Executes a POST request to the control address with the specified payload. Raises an error if the HTTP request
38
+ # fails. Returns the response from the server split into individual lines.
39
+ def execute_post_request(payload)
40
+ response = RestClient::Request.execute method: :post, url: @control_url, payload: payload
41
+
42
+ response.body.split("\n").map(&:strip)
43
+ rescue RestClient::Exception => exception
44
+ raise RequestError, exception.message
45
+ rescue SocketError => socket_error
46
+ raise RequestError, socket_error
47
+ end
48
+
49
+ # Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
50
+ # details on the supported keys.
51
+ def build_payload(options)
52
+ params = {
53
+ LS_session: @session_id,
54
+ LS_table: options.fetch(:table),
55
+ LS_op: options.fetch(:operation)
56
+ }
57
+
58
+ build_optional_payload_fields options, params
59
+
60
+ URI.encode_www_form params
61
+ end
62
+
63
+ def build_optional_payload_fields(options, params)
64
+ params[:LS_data_adapter] = options[:adapter] if options.key? :adapter
65
+ params[:LS_id] = options[:items].join(' ') if options.key? :items
66
+ params[:LS_schema] = options[:fields].map(&:to_s).join(' ') if options.key? :fields
67
+ params[:LS_mode] = options[:mode].to_s.upcase if options.key? :mode
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,22 @@
1
+ module Lightstreamer
2
+ # Helper class that takes an incoming stream of ASCII data and yields back individual lines as they become complete.
3
+ class LineBuffer
4
+ def initialize
5
+ @buffer = ''
6
+ end
7
+
8
+ # Appends a new piece of ASCII data to this buffer. Any lines that are now complete will be yielded back.
9
+ #
10
+ # @param [String] data The new piece of ASCII data.
11
+ def process(data)
12
+ @buffer << data
13
+
14
+ lines = @buffer.split "\n"
15
+ @buffer = @buffer.end_with?("\n") ? '' : lines.pop
16
+
17
+ lines.each do |line|
18
+ yield line.strip
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Lightstreamer
2
+ # This error class is raised by {Session} when a request to the Lightstreamer API fails with a Lightstreamer-specific
3
+ # error code and error message.
4
+ class ProtocolError < StandardError
5
+ # @return [String] A description of the Lightstreamer error that occurred.
6
+ attr_reader :error
7
+
8
+ # @return [Fixnum] The numeric code of the Lightstreamer error.
9
+ attr_reader :code
10
+
11
+ # Initializes this protocol error with the specific message and code.
12
+ #
13
+ # @param [String] error The error description.
14
+ # @param [Integer] code The numeric code for the error.
15
+ def initialize(error, code)
16
+ @error = error.to_s
17
+ @http_code = code.to_i
18
+
19
+ super "Lightstreamer error: #{@error}, code: #{code}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Lightstreamer
2
+ # This error class is raised by {Session} when a request to the Lightstreamer API fails.
3
+ class RequestError < StandardError
4
+ # @return [String] A description of the error that occurred when the request was attempted.
5
+ attr_reader :error
6
+
7
+ # @return [Fixnum] The HTTP code that was returned, or `nil` if unknown.
8
+ attr_reader :http_code
9
+
10
+ # Initializes this request error with a message and an HTTP code.
11
+ #
12
+ # @param [String] error The error description.
13
+ # @param [Integer] http_code The HTTP code for the request failure, if known.
14
+ def initialize(error, http_code = nil)
15
+ @error = error.to_s
16
+ @http_code = http_code ? http_code.to_i : nil
17
+
18
+ super "Request error: #{error}#{http_code ? ", http code: #{http_code}" : ''}"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,143 @@
1
+ module Lightstreamer
2
+ # This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class is the
3
+ # primary interface for working with Lightstreamer.
4
+ class Session
5
+ # @return [String] The URL of the Lightstreamer server to connect to. Set by {#initialize}.
6
+ attr_reader :server_url
7
+
8
+ # @return [String] The username to connect to the Lightstreamer server with. Set by {#initialize}.
9
+ attr_reader :username
10
+
11
+ # @return [String] The password to connect to the Lightstreamer server with. Set by {#initialize}.
12
+ attr_reader :password
13
+
14
+ # @return [String] The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
15
+ attr_reader :adapter_set
16
+
17
+ # Initializes this new Lightstreamer session with the passed options.
18
+ #
19
+ # @param [Hash] options The options to create the session with.
20
+ # @option options [String] :server_url The URL of the Lightstreamer server. Required.
21
+ # @option options [String] :username The username to connect to the server with. Optional.
22
+ # @option options [String] :password The password to connect to the server with. Optional.
23
+ # @option options [String] :adapter_set The name of the adapter set to request from the server. Optional.
24
+ def initialize(options = {})
25
+ @subscriptions = []
26
+ @subscriptions_mutex = Mutex.new
27
+
28
+ @server_url = options.fetch :server_url
29
+ @username = options[:username]
30
+ @password = options[:password]
31
+ @adapter_set = options[:adapter_set]
32
+ end
33
+
34
+ # Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
35
+ # {ProtocolError} will be raised.
36
+ def connect
37
+ @stream_connection = StreamConnection.new self
38
+ @subscriptions = []
39
+
40
+ first_line = @stream_connection.read_line
41
+
42
+ if first_line == 'OK'
43
+ @session_id = read_session_id
44
+ create_control_connection
45
+ create_processing_thread
46
+ elsif first_line == 'ERROR'
47
+ handle_connection_error
48
+ end
49
+ end
50
+
51
+ # Subscribes this Lightstreamer session to the specified subscription.
52
+ #
53
+ # @param [Subscription] subscription The new subscription to subscribe to.
54
+ def subscribe(subscription)
55
+ subscription.clear_data
56
+
57
+ @subscriptions_mutex.synchronize { @subscriptions << subscription }
58
+
59
+ begin
60
+ @control_connection.execute table: subscription.id, operation: :add, mode: subscription.mode,
61
+ items: subscription.items, fields: subscription.fields,
62
+ adapter: subscription.adapter
63
+ rescue
64
+ @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
65
+ raise
66
+ end
67
+ end
68
+
69
+ # Unsubscribes this Lightstreamer session from the specified subscription.
70
+ #
71
+ # @param [Subscription] subscription The existing subscription to unsubscribe from.
72
+ def unsubscribe(subscription)
73
+ @subscriptions_mutex.synchronize do
74
+ raise ArgumentError, 'Unknown subscription' unless @subscriptions.detect subscription
75
+ end
76
+
77
+ @control_connection.execute table: subscription.id, operation: :delete
78
+
79
+ @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
80
+ end
81
+
82
+ private
83
+
84
+ # Parses the next line of data from the stream connection as the session ID and returns it.
85
+ def read_session_id
86
+ @stream_connection.read_line.match(/^SessionId:(.*)$/).captures.first
87
+ end
88
+
89
+ # Attempts to parses the next line of data from the stream connection as a custom control address and then uses this
90
+ # address to create the control connection. Note that the control address is optional and if it is absent then
91
+ # {#server_url} will be used instead of a custom control address.
92
+ def create_control_connection
93
+ match = @stream_connection.read_line.match(/^ControlAddress:(.*)$/)
94
+ control_address = (match && match.captures.first) || server_url
95
+
96
+ # The rest of the contents in the header is ignored, so read up until the blank line that marks its ending
97
+ loop { break if @stream_connection.read_line == '' }
98
+
99
+ # If the control URL doesn't have a schema then use the same schema as the server URL
100
+ control_address = "#{URI(server_url).scheme}://#{control_address}" unless control_address.start_with? 'http'
101
+
102
+ @control_connection = ControlConnection.new @session_id, control_address
103
+ end
104
+
105
+ # Starts the processing thread that reads and processes incoming data from the stream connection.
106
+ def create_processing_thread
107
+ @processing_thread = Thread.new do
108
+ begin
109
+ loop do
110
+ process_stream_data @stream_connection.read_line
111
+ end
112
+ rescue StandardError => error
113
+ warn "Lightstreamer: exception in processing thread: #{error}"
114
+ exit 1
115
+ end
116
+ end
117
+ end
118
+
119
+ # Processes a single line of incoming stream data by passing it to all the active subscriptions until one
120
+ # successfully processes it. This method is always run on the processing thread.
121
+ def process_stream_data(line)
122
+ was_processed = @subscriptions_mutex.synchronize do
123
+ @subscriptions.detect do |subscription|
124
+ subscription.process_stream_data line
125
+ end
126
+ end
127
+
128
+ warn "Lightstreamer: unprocessed stream data '#{line}'" unless was_processed
129
+ end
130
+
131
+ # Handles a failure to establish a stream connection by reading off the error code and error message then raising
132
+ # a {ProtocolError}.
133
+ def handle_connection_error
134
+ error_code = @stream_connection.read_line
135
+ error_message = @stream_connection.read_line
136
+
137
+ @stream_connection = nil
138
+ @control_connection = nil
139
+
140
+ raise ProtocolError, error_message, error_code
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,79 @@
1
+ module Lightstreamer
2
+ # Manages a long-running Lightstreamer connection that handles incoming streaming data on a separate thread and
3
+ # makes it available for consumption via the {#read_line} method.
4
+ class StreamConnection
5
+ # Establishes a new stream connection using the authentication details from the passed session.
6
+ #
7
+ # @param [Session] session The session to create a stream connection for.
8
+ def initialize(session)
9
+ @session = session
10
+ @queue = Queue.new
11
+
12
+ create_stream
13
+ create_stream_thread
14
+ end
15
+
16
+ # Reads the next line of streaming data. This method blocks the calling thread until a line of data is available.
17
+ def read_line
18
+ @queue.pop
19
+ end
20
+
21
+ private
22
+
23
+ def create_stream
24
+ @stream = Net::HTTP.new stream_uri.host, stream_uri.port
25
+ @stream.use_ssl = true if stream_uri.port == 443
26
+ end
27
+
28
+ def create_stream_thread
29
+ @thread = Thread.new do
30
+ begin
31
+ connect_stream_and_queue_data
32
+
33
+ warn 'Lightstreamer: stream connection closed'
34
+ rescue StandardError => error
35
+ warn "Lightstreamer: exception in stream thread: #{error}"
36
+ exit 1
37
+ end
38
+ end
39
+ end
40
+
41
+ def initiate_stream_post_request
42
+ Net::HTTP::Post.new(stream_uri.path).tap do |request|
43
+ request.body = stream_create_parameters
44
+ end
45
+ end
46
+
47
+ def connect_stream_and_queue_data
48
+ @stream.request initiate_stream_post_request do |response|
49
+ buffer = LineBuffer.new
50
+ response.read_body do |data|
51
+ buffer.process data do |line|
52
+ @queue.push line unless ignore_line? line
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def stream_uri
59
+ URI.join @session.server_url, '/lightstreamer/create_session.txt'
60
+ end
61
+
62
+ def stream_create_parameters
63
+ params = {
64
+ LS_op2: 'create',
65
+ LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg',
66
+ LS_user: @session.username,
67
+ LS_password: @session.password
68
+ }
69
+
70
+ params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
71
+
72
+ URI.encode_www_form params
73
+ end
74
+
75
+ def ignore_line?(line)
76
+ line =~ /^(PROBE|Preamble:.*)$/
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,194 @@
1
+ module Lightstreamer
2
+ # Describes a subscription that can be bound to a Lightstreamer session in order to consume its data. A subscription
3
+ # is described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous
4
+ # data callback using {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data
5
+ # only once they are attached to a Lightstreamer session using {Session#subscribe}.
6
+ class Subscription
7
+ # @return [Fixnum] The unique identification number of this subscription. This is used to identify the subscription
8
+ # in incoming Lightstreamer data.
9
+ attr_reader :id
10
+
11
+ # @return [Array<String>] The names of the items to subscribe to.
12
+ attr_reader :items
13
+
14
+ # @return [Array<String>] The names of the fields to subscribe to on the items.
15
+ attr_reader :fields
16
+
17
+ # @return [:distinct, :merge] The operation mode of this subscription.
18
+ attr_reader :mode
19
+
20
+ # @return [String] The name of the data adapter from the Lightstreamer session's adapter set that should be used.
21
+ # If `nil` then the default data adapter will be used.
22
+ attr_reader :adapter
23
+
24
+ # Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
25
+ # {Session#subscribe} to activate the subscription on a Lightstreamer session.
26
+ #
27
+ # @param [Hash] options The options to create the subscription with.
28
+ # @option options [Array<String>] :items The names of the items to subscribe to.
29
+ # @option options [Array<String>] :fields The names of the fields to subscribe to on the items.
30
+ # @option options [:distinct, :merge] :mode The operation mode of this subscription.
31
+ # @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
32
+ # should be used. If `nil` then the default data adapter will be used.
33
+ def initialize(options)
34
+ @id = self.class.next_id
35
+
36
+ @items = Array(options.fetch(:items))
37
+ @fields = Array(options.fetch(:fields))
38
+ @mode = options.fetch(:mode).to_sym
39
+ @adapter = options[:adapter]
40
+
41
+ @data_mutex = Mutex.new
42
+ clear_data
43
+
44
+ @data_callbacks = []
45
+ end
46
+
47
+ # Clears all current data stored for this subscription. New data will continue to be processed as it becomes
48
+ # available.
49
+ def clear_data
50
+ @data_mutex.synchronize do
51
+ @data = (0...items.size).map { { distinct: [], merge: {} }.fetch(mode) }
52
+ end
53
+ end
54
+
55
+ # Clears the current data stored for the specified item. This is important to do when {#mode} is `:distinct` as
56
+ # otherwise the incoming data will build up indefinitely.
57
+ #
58
+ # @param [String] item_name The name of the item to clear the current data of.
59
+ def clear_data_for_item(item_name)
60
+ index = @items.index item_name
61
+ raise ArgumentError, 'Unrecognized item name' unless index
62
+
63
+ @data_mutex.synchronize do
64
+ @data[index] = { distinct: [], merge: {} }.fetch(mode)
65
+ end
66
+ end
67
+
68
+ # Adds the passed block to the list of callbacks that will be run when new data for this subscription arrives. The
69
+ # block will be called on a worker thread and so the code that is run by the block must be thread-safe. The
70
+ # arguments passed to the block are `|subscription, item_name, item_data, new_values|`.
71
+ #
72
+ # @param [Proc] block The callback block that is to be run when new data arrives.
73
+ #
74
+ # @return [Proc] The same `Proc` object that was passed to this method. This can be used to remove this data
75
+ # callback at a later stage using {#remove_data_callback}.
76
+ def add_data_callback(&block)
77
+ raise ArgumentError, 'Data callbacks must take four arguments' unless block.arity == 4
78
+
79
+ @data_mutex.synchronize { @data_callbacks << block }
80
+
81
+ block
82
+ end
83
+
84
+ # Removes a data callback that was added by {#add_data_callback}.
85
+ #
86
+ # @param [Proc] block The data callback block to remove.
87
+ def remove_data_callback(block)
88
+ @data_mutex.synchronize { @data_callbacks.delete block }
89
+ end
90
+
91
+ # Returns the current data of one of this subscription's items.
92
+ #
93
+ # @param [String] item_name The name of the item to return the current data for.
94
+ #
95
+ # @return [Hash, Array] The item data. Will be a `Hash` if {#mode} is `:merge`, and an `Array` if {#mode} is
96
+ # `:distinct`.
97
+ def retrieve_item_data(item_name)
98
+ index = @items.index item_name
99
+ raise ArgumentError, 'Unrecognized item name' unless index
100
+
101
+ @data_mutex.synchronize do
102
+ @data[index].dup
103
+ end
104
+ end
105
+
106
+ # Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
107
+ # to be called by the session's processing thread.
108
+ #
109
+ # @param [String] line The line of stream data to process.
110
+ #
111
+ # @return [Boolean] Whether the passed line of stream data was relevant to this subscription and was successfully
112
+ # processed by it.
113
+ def process_stream_data(line)
114
+ item_index, values = parse_stream_data line
115
+ return false unless item_index
116
+
117
+ @data_mutex.synchronize do
118
+ data = @data[item_index]
119
+
120
+ data << values if mode == :distinct
121
+ data.merge!(values) if mode == :merge
122
+
123
+ @data_callbacks.each { |callback| callback.call self, @items[item_index], data, values }
124
+ end
125
+
126
+ true
127
+ end
128
+
129
+ # Returns the next unique ID to use for a new subscription.
130
+ #
131
+ # @return [Fixnum]
132
+ def self.next_id
133
+ @next_id ||= 0
134
+ @next_id += 1
135
+ end
136
+
137
+ private
138
+
139
+ # Attempts to parse an line of stream data.
140
+ #
141
+ # @param [String] line The line of stream data to parse.
142
+ #
143
+ # @return [Array] The first value is the item index, and the second is a hash of the values contained in the stream
144
+ # data. Will be nil if the stream data was not able to be parsed.
145
+ def parse_stream_data(line)
146
+ match = line.match stream_data_regex
147
+ return unless match
148
+
149
+ item_index = match.captures[0].to_i - 1
150
+ return unless item_index < @items.size
151
+
152
+ [item_index, parse_values(match.captures[1..-1])]
153
+ end
154
+
155
+ # Returns the regular expression that will match a single line of data in the incoming stream that is relevant to
156
+ # this subscription. The ID at the beginning must match, as well as the number of fields.
157
+ #
158
+ # @return [Regexp]
159
+ def stream_data_regex
160
+ Regexp.new "^#{id},(\\d+)#{'\|(.*)' * fields.size}"
161
+ end
162
+
163
+ # Parses an array of values from an incoming line of stream data into a hash where the keys are the field names
164
+ # defined for this subscription.
165
+ #
166
+ # @param [String] values The raw values from the incoming stream data.
167
+ #
168
+ # @return [Hash] The parsed values as a hash where the keys are the field names of this subscription.
169
+ def parse_values(values)
170
+ hash = {}
171
+
172
+ values.each_with_index do |value, index|
173
+ next if value == ''
174
+
175
+ # These conversions are specified in the Lightstreamer specification
176
+ value = '' if value == '$'
177
+ value = nil if value == '#'
178
+ value = value[1..-1] if value =~ /^($|#)/
179
+
180
+ hash[fields[index]] = convert_utf16_characters value
181
+ end
182
+
183
+ hash
184
+ end
185
+
186
+ # Non-ASCII characters are transmitted as UTF-16 escape sequences in the form '\uXXXX' or '\uXXXX\uYYYY'. This
187
+ # method is respnsible for converting such escape sequences into native Unicode characters.
188
+ #
189
+ # @todo Implement this method.
190
+ def convert_utf16_characters(value)
191
+ value
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,4 @@
1
+ module Lightstreamer
2
+ # The version of this gem.
3
+ VERSION = '0.1'.freeze
4
+ end
metadata ADDED
@@ -0,0 +1,213 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lightstreamer
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Richard Viney
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.19'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.19'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: codeclimate-test-reporter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_girl
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: github-markup
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: redcarpet
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.5'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.5'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-mocks
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.5'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.5'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.41'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.41'
153
+ - !ruby/object:Gem::Dependency
154
+ name: yard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.9'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.9'
167
+ description:
168
+ email: richard.viney@gmail.com
169
+ executables:
170
+ - lightstreamer
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - CHANGELOG.md
175
+ - LICENSE.md
176
+ - README.md
177
+ - bin/lightstreamer
178
+ - lib/lightstreamer.rb
179
+ - lib/lightstreamer/cli/main.rb
180
+ - lib/lightstreamer/cli/stream_command.rb
181
+ - lib/lightstreamer/control_connection.rb
182
+ - lib/lightstreamer/line_buffer.rb
183
+ - lib/lightstreamer/protocol_error.rb
184
+ - lib/lightstreamer/request_error.rb
185
+ - lib/lightstreamer/session.rb
186
+ - lib/lightstreamer/stream_connection.rb
187
+ - lib/lightstreamer/subscription.rb
188
+ - lib/lightstreamer/version.rb
189
+ homepage: https://github.com/rviney/lightstreamer
190
+ licenses:
191
+ - MIT
192
+ metadata: {}
193
+ post_install_message:
194
+ rdoc_options: []
195
+ require_paths:
196
+ - lib
197
+ required_ruby_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '2.0'
202
+ required_rubygems_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ requirements: []
208
+ rubyforge_project:
209
+ rubygems_version: 2.5.1
210
+ signing_key:
211
+ specification_version: 4
212
+ summary: Ruby client for accessing a Lightstreamer server.
213
+ test_files: []