lightstreamer 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []