From da61e8aae43d1e999557554e7459f3bf7ff8993a Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 16:18:57 -0600 Subject: [PATCH 1/7] added support for 'faye-websocket' --- LICENSE.txt | 2 +- README.md | 40 ++++ lib/wamp_client.rb | 3 +- lib/wamp_client/connection.rb | 24 +-- lib/wamp_client/session.rb | 4 +- .../{transport.rb => transport/base.rb} | 62 ++---- .../transport/event_machine_base.rb | 55 +++++ lib/wamp_client/transport/faye_web_socket.rb | 83 ++++++++ .../transport/web_socket_event_machine.rb | 86 ++++++++ lib/wamp_client/version.rb | 2 +- spec/spec_helper.rb | 85 +++++++- spec/transport_spec.rb | 200 ++++++++++++++++++ wamp_client.gemspec | 2 + 13 files changed, 579 insertions(+), 69 deletions(-) rename lib/wamp_client/{transport.rb => transport/base.rb} (70%) create mode 100644 lib/wamp_client/transport/event_machine_base.rb create mode 100644 lib/wamp_client/transport/faye_web_socket.rb create mode 100644 lib/wamp_client/transport/web_socket_event_machine.rb create mode 100644 spec/transport_spec.rb diff --git a/LICENSE.txt b/LICENSE.txt index 06cf598..c6d8505 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2016 Eric Chapman +Copyright (c) 2018 Eric Chapman MIT License diff --git a/README.md b/README.md index ef5f5e8..23cf534 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Please use [wamp_rails](https://github.com/ericchapman/ruby_wamp_rails) to integ ## Revision History + - v0.0.9: + - Added support for transport override and 'faye-websocket' transport - v0.0.8: - Exposed 'yield' publicly to allow higher level libraries to not use the 'defer' - Removed library version dependency @@ -141,6 +143,44 @@ connection.on_challenge do |authmethod, extra| end ``` +#### Overriding Transport +By default, the library will use the "websocket-eventmachine-client" Gem as the websocket transport. +However the library also supports overriding this. + +##### GEM: faye-websocket +To use this library, do the following + +Install the "faye-websocket" Gem: + + $ gem install faye-websocket + +Override the transport by doing the following: + +```ruby +require 'wamp_client' + +options = { + uri: 'ws://127.0.0.1:8080/ws', + realm: 'realm1', + proxy: { # See faye-websocket documentation + :origin => 'http://username:password@proxy.example.com', + :headers => {'User-Agent' => 'ruby'} + }, + transport: WampClient::Transport::FayeWebSocket +} + +connection = WampClient::Connection.new(options) + +# More code +``` + +Note that the "faye-wesbsocket" transport supports passing in a "proxy" as shown above. + +##### Custom +You can also create your own transports by wrapping them in a "Transport" object +and including as shown above. For more details on this, see the files in +"lib/wamp_client/transport" + ### Authentication The library supports authentication. Here is how to perform the different methods diff --git a/lib/wamp_client.rb b/lib/wamp_client.rb index e318086..a94217f 100644 --- a/lib/wamp_client.rb +++ b/lib/wamp_client.rb @@ -28,8 +28,7 @@ require 'wamp_client/version' require 'wamp_client/message' require 'wamp_client/serializer' -require 'wamp_client/transport' require 'wamp_client/connection' require 'wamp_client/session' require 'wamp_client/auth' -require 'wamp_client/defer' \ No newline at end of file +require 'wamp_client/defer' diff --git a/lib/wamp_client/connection.rb b/lib/wamp_client/connection.rb index 2c2e151..d9ebb1f 100644 --- a/lib/wamp_client/connection.rb +++ b/lib/wamp_client/connection.rb @@ -25,13 +25,13 @@ =end -require 'websocket-eventmachine-client' require 'wamp_client/session' -require 'wamp_client/transport' +require 'wamp_client/transport/web_socket_event_machine' +require 'wamp_client/transport/faye_web_socket' module WampClient class Connection - attr_accessor :options, :transport, :session + attr_accessor :options, :transport_class, :transport, :session @reconnect = true @@ -79,6 +79,7 @@ def on_disconnect(&on_disconnect) # @param options [Hash] The different options to pass to the connection # @option options [String] :uri The uri of the WAMP router to connect to + # @option options [String] :proxy The proxy to get to the router # @option options [String] :realm The realm to connect to # @option options [String,nil] :protocol The protocol (default if wamp.2.json) # @option options [String,nil] :authid The id to authenticate with @@ -86,6 +87,7 @@ def on_disconnect(&on_disconnect) # @option options [Hash] :headers Custom headers to include during the connection # @option options [WampClient::Serializer::Base] :serializer The serializer to use (default is json) def initialize(options) + self.transport_class = options.delete(:transport) || WampClient::Transport::WebSocket self.options = options || {} end @@ -98,7 +100,7 @@ def open @retry_timer = 1 @retrying = false - EM.run do + self.transport_class.start_event_machine do # Create the transport self._create_transport end @@ -156,11 +158,7 @@ def _create_transport end # Initialize the transport - if self.options[:transport] - self.transport = self.options[:transport] - else - self.transport = WampClient::Transport::WebSocketTransport.new(self.options) - end + self.transport = self.transport_class.new(self.options) # Setup transport callbacks self.transport.on_open do @@ -188,7 +186,7 @@ def _create_transport self._retry unless @retrying else # Stop the Event Machine - EM.stop + self.transport_class.stop_event_machine end end @@ -205,16 +203,16 @@ def _finish_retry def _retry - unless self.session and self.session.is_open? + unless self.session&.is_open? @retry_timer = 2*@retry_timer unless @retry_timer == 32 @retrying = true self._create_transport puts "Attempting Reconnect... Next attempt in #{@retry_timer} seconds" - EM.add_timer(@retry_timer) { + self.transport_clase.add_timer(@retry_timer) do self._retry if @retrying - } + end end end diff --git a/lib/wamp_client/session.rb b/lib/wamp_client/session.rb index 0405edd..fae00d6 100644 --- a/lib/wamp_client/session.rb +++ b/lib/wamp_client/session.rb @@ -25,7 +25,7 @@ =end -require 'wamp_client/transport' +require 'wamp_client/transport/base' require 'wamp_client/message' require 'wamp_client/check' require 'wamp_client/version' @@ -908,7 +908,7 @@ def call(procedure, args=nil, kwargs=nil, options={}, &callback) # Timeout Logic if options[:timeout] and options[:timeout] > 0 - self.transport.timer(options[:timeout]) do + self.transport.add_timer(options[:timeout]) do # Once the timer expires, if the call hasn't completed, cancel it if self._requests[:call][call.id] call.cancel diff --git a/lib/wamp_client/transport.rb b/lib/wamp_client/transport/base.rb similarity index 70% rename from lib/wamp_client/transport.rb rename to lib/wamp_client/transport/base.rb index ac8be60..28a4b6d 100644 --- a/lib/wamp_client/transport.rb +++ b/lib/wamp_client/transport/base.rb @@ -57,11 +57,12 @@ def on_error(&on_error) @on_error = on_error end - attr_accessor :type, :uri, :headers, :protocol, :serializer, :connected + attr_accessor :uri, :proxy, :headers, :protocol, :serializer, :connected # Constructor for the transport # @param options [Hash] The connection options. the different options are as follows # @option options [String] :uri The url to connect to + # @option options [String] :proxy The proxy to use # @option options [String] :protocol The protocol # @option options [Hash] :headers Custom headers to include during the connection # @option options [WampClient::Serializer::Base] :serializer The serializer to use @@ -70,6 +71,7 @@ def initialize(options) # Initialize the parameters self.connected = false self.uri = options[:uri] + self.proxy = options[:proxy] self.headers = options[:headers] || {} self.protocol = options[:protocol] || 'wamp.2.json' self.serializer = options[:serializer] || WampClient::Serializer::JSONSerializer.new @@ -109,62 +111,24 @@ def send_message(msg) # Process the callback when the timer expires # @param [Integer] milliseconds - The number # @param [block] callback - The callback that is fired when the timer expires - def timer(milliseconds, &callback) + def self.add_timer(milliseconds, &callback) # Implement in subclass end - - end - - # This implementation uses the 'websocket-eventmachine-client' Gem. This is the default if no transport is included - class WebSocketTransport < Base - attr_accessor :socket - - def initialize(options) - super(options) - self.type = 'websocket' - self.socket = nil - end - - def connect - self.socket = WebSocket::EventMachine::Client.connect( - :uri => self.uri, - :headers => self.headers - ) - - self.socket.onopen do - self.connected = true - @on_open.call unless @on_open.nil? - end - - self.socket.onmessage do |msg, type| - @on_message.call(self.serializer.deserialize(msg)) unless @on_message.nil? - end - - self.socket.onclose do |code, reason| - self.connected = false - @on_close.call(reason) unless @on_close.nil? - end + def add_timer(milliseconds, &callback) + self.class.add_timer(milliseconds, &callback) end - def disconnect - self.connected = !self.socket.close # close returns 'true' if the connection was closed immediately - end - - def send_message(msg) - if self.connected - self.socket.send(self.serializer.serialize(msg), {type: 'text'}) - else - raise RuntimeError, "Socket must be open to call 'send_message'" - end + # Method to start the event machine for the socket + def self.start_event_machine(&block) + # Implement in subclass end - def timer(milliseconds, &callback) - delay = (milliseconds.to_f/1000.0).ceil - EM.add_timer(delay) { - callback.call - } + # Method to stop the vent machine + def self.stop_event_machine + # Implement in subclass end end + end end \ No newline at end of file diff --git a/lib/wamp_client/transport/event_machine_base.rb b/lib/wamp_client/transport/event_machine_base.rb new file mode 100644 index 0000000..2e7b72d --- /dev/null +++ b/lib/wamp_client/transport/event_machine_base.rb @@ -0,0 +1,55 @@ +=begin + +Copyright (c) 2016 Eric Chapman + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=end + +require 'eventmachine' +require_relative 'base' + +module WampClient + module Transport + class EventMachineBase < Base + + def self.start_event_machine(&block) + EM.run do + block.call + end + end + + def self.stop_event_machine + EM.stop + end + + def self.add_timer(milliseconds, &callback) + delay = (milliseconds.to_f/1000.0).ceil + EM.add_timer(delay) { + callback.call + } + end + + end + end +end + diff --git a/lib/wamp_client/transport/faye_web_socket.rb b/lib/wamp_client/transport/faye_web_socket.rb new file mode 100644 index 0000000..7825f70 --- /dev/null +++ b/lib/wamp_client/transport/faye_web_socket.rb @@ -0,0 +1,83 @@ +=begin + +Copyright (c) 2016 Eric Chapman + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=end + +require_relative 'event_machine_base' + +# This implementation uses the 'faye-websocket' Gem. +module WampClient + module Transport + class FayeWebSocket < EventMachineBase + attr_accessor :socket + + def initialize(options) + super(options) + self.socket = nil + + # Only make them include the gem if they are going to use it + require 'faye/websocket' + end + + def connect + self.socket = Faye::WebSocket::Client.new( + self.uri, [self.protocol], + { + :proxy => self.proxy, + :headers => self.headers + } + ) + + self.socket.on(:open) do |event| + self.connected = true + @on_open.call if @on_open + end + + self.socket.on(:message) do |event| + @on_message.call(self.serializer.deserialize(event.data)) if @on_message + end + + self.socket.on(:close) do |event| + self.connected = false + @on_close.call(event.reason) if @on_close + end + end + + def disconnect + self.socket.close + self.connected = false + end + + def send_message(msg) + if self.connected + self.socket.send(self.serializer.serialize(msg)) + else + raise RuntimeError, "Socket must be open to call 'send_message'" + end + end + + end + end +end diff --git a/lib/wamp_client/transport/web_socket_event_machine.rb b/lib/wamp_client/transport/web_socket_event_machine.rb new file mode 100644 index 0000000..f5fa751 --- /dev/null +++ b/lib/wamp_client/transport/web_socket_event_machine.rb @@ -0,0 +1,86 @@ +=begin + +Copyright (c) 2016 Eric Chapman + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=end + +require_relative 'event_machine_base' + +# This implementation uses the 'websocket-eventmachine-client' Gem. +# This is the default if no transport is included +module WampClient + module Transport + class WebSocketEventMachine < EventMachineBase + attr_accessor :socket + + def initialize(options) + super(options) + self.socket = nil + + # Only make them include the gem if they are going to use it + require 'websocket-eventmachine-client' + + # Raise an exception if proxy was included (not supported) + if self.proxy != nil + raise RuntimeError, "The WebSocketEventMachine transport does not support 'proxy'. Try using 'faye-websocket' transport instead" + end + end + + def connect + self.socket = WebSocket::EventMachine::Client.connect( + :uri => self.uri, + :headers => self.headers + ) + + self.socket.onopen do + self.connected = true + @on_open.call if @on_open + end + + self.socket.onmessage do |msg, type| + @on_message.call(self.serializer.deserialize(msg)) if @on_message + end + + self.socket.onclose do |code, reason| + self.connected = false + @on_close.call(reason) if @on_close + end + end + + def disconnect + self.connected = !self.socket.close # close returns 'true' if the connection was closed immediately + end + + def send_message(msg) + if self.connected + self.socket.send(self.serializer.serialize(msg), {type: 'text'}) + else + raise RuntimeError, "Socket must be open to call 'send_message'" + end + end + + end + end +end + diff --git a/lib/wamp_client/version.rb b/lib/wamp_client/version.rb index 754a21c..b7eb806 100644 --- a/lib/wamp_client/version.rb +++ b/lib/wamp_client/version.rb @@ -26,5 +26,5 @@ =end module WampClient - VERSION = '0.0.8' + VERSION = '0.0.9' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a6fa877..601d3d1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,7 @@ end require 'wamp_client' +require "rspec/em" module SpecHelper @@ -40,10 +41,92 @@ def receive_message(msg) @on_message.call(deserialize) unless @on_message.nil? end - def timer(milliseconds, &callback) + def add_timer(milliseconds, &callback) self.timer_callback = callback end end + class WebSocketEventMachineClientStub + attr_accessor :last_message + + def initialize + EM.add_timer(1) { + @onopen&.call + } + end + + @onopen + def onopen(&onopen) + @onopen = onopen + end + + @onmessage + def onmessage(&onmessage) + @onmessage = onmessage + end + + @onclose + def onclose(&onclose) + @onclose = onclose + end + + def close + @onclose&.call + true + end + + def send(message, type) + self.last_message = message + end + + def receive(message) + @onmessage&.call(message, {type:'text'}) + end + + end + + class FayeWebSocketClientStub + class Event + attr_accessor :data, :reason + end + + attr_accessor :last_message + + def initialize + EM.add_timer(1) { + @on_open&.call(Event.new) + } + end + + @on_open + @on_message + @on_close + def on(event, &block) + if event == :open + @on_open = block + elsif event == :close + @on_close = block + elsif event == :message + @on_message = block + end + end + + def close + event = Event.new + event.reason = 'closed' + @on_close&.call(event) + end + + def send(message) + self.last_message = message + end + + def receive(message) + event = Event.new + event.data = message + @on_message&.call(event) + end + + end end \ No newline at end of file diff --git a/spec/transport_spec.rb b/spec/transport_spec.rb new file mode 100644 index 0000000..60ab3e6 --- /dev/null +++ b/spec/transport_spec.rb @@ -0,0 +1,200 @@ +require 'spec_helper' +require "rspec/em" +require 'websocket-eventmachine-client' +require 'faye/websocket' + +describe WampClient::Transport do + describe WampClient::Transport::EventMachineBase do + it '#start/stop' do + value = 0 + described_class.start_event_machine do + EM.tick_loop do + value += 1 + described_class.stop_event_machine if value == 10 + end + end + + expect(value).to eq(10) + end + + context '#add_timer' do + include RSpec::EM::FakeClock + + before { clock.stub } + after { clock.reset } + + it 'adds a timer with the class method' do + value = 0 + described_class.add_timer(3000) do + value = 1 + end + + expect(value).to eq(0) + clock.tick(1) + expect(value).to eq(0) + clock.tick(1) + expect(value).to eq(0) + clock.tick(1) + expect(value).to eq(1) + end + + it 'adds a timer with the instance method' do + transport = described_class.new({}) + + value = 0 + transport.add_timer(2000) do + value = 1 + end + + expect(value).to eq(0) + clock.tick(1) + expect(value).to eq(0) + clock.tick(1) + expect(value).to eq(1) + end + end + end + + context 'transports' do + let(:test_uri) { 'wss://router.examples.com' } + let(:test_proxy) { 'http://proxy.examples.com' } + let(:options) { + { + uri: test_uri, + headers: {} + } + } + + describe WampClient::Transport::WebSocketEventMachine do + include RSpec::EM::FakeClock + before { clock.stub } + after { clock.reset } + + let(:transport) { described_class.new(options) } + let(:socket) { transport.socket } + before(:each) { + allow(WebSocket::EventMachine::Client).to receive(:connect) { |options| + SpecHelper::WebSocketEventMachineClientStub.new + } + + transport.connect + clock.tick(1) # Simulate connecting + } + + it 'initializes' do + expect(transport.uri).to eq(test_uri) + end + + it 'connects to the router' do + expect(transport.connected?).to eq(true) + end + + it 'disconnects from the router' do + value = false + transport.on_close do + value = true + end + + transport.disconnect + + expect(transport.connected?).to eq(false) + expect(value).to eq(true) + end + + it 'sends a message' do + transport.send_message({test: 'value'}) + expect(socket.last_message).to eq('{"test":"value"}') + end + + it 'receives a message' do + message = nil + transport.on_message do |msg, type| + message = msg + end + + socket.receive('{"test":"value"}') + expect(message).to eq({test: 'value'}) + end + + it 'raises exception if sending message when closed' do + transport.disconnect + expect { + transport.send_message({test: 'value'}) + }.to raise_error(RuntimeError) + end + + it 'raises exception if proxy is included' do + expect { + options[:proxy] = { origin: 'something', headers: 'something' } + described_class.new(options) + }.to raise_error(RuntimeError) + end + end + + describe WampClient::Transport::FayeWebSocket do + include RSpec::EM::FakeClock + before { clock.stub } + after { clock.reset } + + let(:transport) { described_class.new(options) } + let(:socket) { transport.socket } + before(:each) { + allow(Faye::WebSocket::Client).to receive(:new) { |uri, protocols, options| + SpecHelper::FayeWebSocketClientStub.new + } + + transport.connect + clock.tick(1) # Simulate connecting + } + + it 'initializes' do + expect(transport.uri).to eq(test_uri) + end + + it 'connects to the router' do + expect(transport.connected?).to eq(true) + end + + it 'disconnects from the router' do + value = false + transport.on_close do + value = true + end + + transport.disconnect + + expect(transport.connected?).to eq(false) + expect(value).to eq(true) + end + + it 'sends a message' do + transport.send_message({test: 'value'}) + expect(socket.last_message).to eq('{"test":"value"}') + end + + it 'receives a message' do + message = nil + transport.on_message do |msg, type| + message = msg + end + + socket.receive('{"test":"value"}') + expect(message).to eq({test: 'value'}) + end + + it 'raises exception if sending message when closed' do + transport.disconnect + expect { + transport.send_message({test: 'value'}) + }.to raise_error(RuntimeError) + end + + it 'does not raise exception if proxy is included' do + expect { + options[:proxy] = { origin: 'something', headers: 'something' } + described_class.new(options) + }.not_to raise_error + end + end + end +end diff --git a/wamp_client.gemspec b/wamp_client.gemspec index 3a09362..2a4af50 100644 --- a/wamp_client.gemspec +++ b/wamp_client.gemspec @@ -21,8 +21,10 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.7' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec' + spec.add_development_dependency 'rspec-eventmachine', '>= 0.2.0' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'codecov' + spec.add_development_dependency 'faye-websocket' spec.add_dependency 'websocket-eventmachine-client' spec.add_dependency 'json' From 159175efef9eb738f035db4df694049f5b40daea Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 16:30:27 -0600 Subject: [PATCH 2/7] added 'on(event)' callback helper --- README.md | 29 +++++++++++++++-------------- lib/wamp_client/connection.rb | 18 ++++++++++++++++++ lib/wamp_client/transport/base.rb | 16 ++++++++++++++++ spec/transport_spec.rb | 8 ++++---- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 23cf534..f08cbc8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Please use [wamp_rails](https://github.com/ericchapman/ruby_wamp_rails) to integ - v0.0.9: - Added support for transport override and 'faye-websocket' transport + - Added "on(event)" callback (still support legacy methods) - v0.0.8: - Exposed 'yield' publicly to allow higher level libraries to not use the 'defer' - Removed library version dependency @@ -72,7 +73,7 @@ options = { } connection = WampClient::Connection.new(options) -connection.on_join do |session, details| +connection.on(:join) do |session, details| puts "Session Open" # Register for something @@ -103,42 +104,42 @@ A connection is closed by simply calling "close" connection.close ``` -Note that the connection will still call "on_leave" and "on_disconnect" as it closes the session and the transport +Note that the connection will still call "on(:leave)" and "on(:disconnect)" as it closes the session and the transport #### Callbacks A connection has the following callbacks -**on_connect** - Called when the transport is opened +**on(:connect)** - Called when the transport is opened ```ruby -connection.on_connect do +connection.on(:connect) do end ``` -**on_join** - Called when the session is established +**on(:join)** - Called when the session is established ```ruby -connection.on_join do |session, details| +connection.on(:join) do |session, details| end ``` -**on_leave** - Called when the session is terminated +**on(:leave)** - Called when the session is terminated ```ruby -connection.on_leave do |reason, details| +connection.on(:leave) do |reason, details| end ``` -**on_disconnect** - Called when the connection is terminated +**on(:disconnect)** - Called when the connection is terminated ```ruby -connection.on_disconnect do |reason| +connection.on(:disconnect) do |reason| end ``` -**on_challenge** - Called when an authentication challenge is created +**on(:challenge)** - Called when an authentication challenge is created ```ruby -connection.on_challenge do |authmethod, extra| +connection.on(:challenge) do |authmethod, extra| end ``` @@ -198,7 +199,7 @@ options = { } connection = WampClient::Connection.new(options) -connection.on_challenge do |authmethod, extra| +connection.on(:challenge) do |authmethod, extra| puts 'Challenge' if authmethod == 'wampcra' WampClient::Auth::Cra.sign('secret', extra[:challenge]) @@ -207,7 +208,7 @@ connection.on_challenge do |authmethod, extra| end end -connection.on_join do |session, details| +connection.on(:join) do |session, details| puts "Session Open" end diff --git a/lib/wamp_client/connection.rb b/lib/wamp_client/connection.rb index d9ebb1f..953322d 100644 --- a/lib/wamp_client/connection.rb +++ b/lib/wamp_client/connection.rb @@ -77,6 +77,24 @@ def on_disconnect(&on_disconnect) @on_disconnect = on_disconnect end + # Simple setter for callbacks + def on(event, &callback) + case event + when :connect + self.on_connect(&callback) + when :join + self.on_join(&callback) + when :challenge + self.on_challenge(&callback) + when :leave + self.on_leave(&callback) + when :disconnect + self.on_disconnect(&callback) + else + raise RuntimeError, "Unknown on(event) '#{event}'" + end + end + # @param options [Hash] The different options to pass to the connection # @option options [String] :uri The uri of the WAMP router to connect to # @option options [String] :proxy The proxy to get to the router diff --git a/lib/wamp_client/transport/base.rb b/lib/wamp_client/transport/base.rb index 28a4b6d..4819418 100644 --- a/lib/wamp_client/transport/base.rb +++ b/lib/wamp_client/transport/base.rb @@ -57,6 +57,22 @@ def on_error(&on_error) @on_error = on_error end + # Simple setter for callbacks + def on(event, &callback) + case event + when :open + self.on_open(&callback) + when :close + self.on_close(&callback) + when :message + self.on_message(&callback) + when :error + self.on_error(&callback) + else + raise RuntimeError, "Unknown on(event) '#{event}'" + end + end + attr_accessor :uri, :proxy, :headers, :protocol, :serializer, :connected # Constructor for the transport diff --git a/spec/transport_spec.rb b/spec/transport_spec.rb index 60ab3e6..1eb0324 100644 --- a/spec/transport_spec.rb +++ b/spec/transport_spec.rb @@ -91,7 +91,7 @@ it 'disconnects from the router' do value = false - transport.on_close do + transport.on(:close) do value = true end @@ -108,7 +108,7 @@ it 'receives a message' do message = nil - transport.on_message do |msg, type| + transport.on(:message) do |msg, type| message = msg end @@ -157,7 +157,7 @@ it 'disconnects from the router' do value = false - transport.on_close do + transport.on(:close) do value = true end @@ -174,7 +174,7 @@ it 'receives a message' do message = nil - transport.on_message do |msg, type| + transport.on(:message) do |msg, type| message = msg end From 49a30d26474c400879ed0e075cf3f0bd58e5fb46 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 20:27:11 -0600 Subject: [PATCH 3/7] added test coverage for 'Connection' and 'Transport' --- README.md | 1 + lib/wamp_client/connection.rb | 24 ++-- lib/wamp_client/session.rb | 15 ++ lib/wamp_client/transport/base.rb | 1 + lib/wamp_client/transport/faye_web_socket.rb | 14 +- spec/connection_spec.rb | 138 +++++++++++++++++++ spec/session_spec.rb | 10 +- spec/spec_helper.rb | 32 +++-- 8 files changed, 209 insertions(+), 26 deletions(-) create mode 100644 spec/connection_spec.rb diff --git a/README.md b/README.md index f08cbc8..c75c707 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Please use [wamp_rails](https://github.com/ericchapman/ruby_wamp_rails) to integ - v0.0.9: - Added support for transport override and 'faye-websocket' transport - Added "on(event)" callback (still support legacy methods) + - Increased Test Coverage for 'Transport' and 'Connection' classes - v0.0.8: - Exposed 'yield' publicly to allow higher level libraries to not use the 'defer' - Removed library version dependency diff --git a/lib/wamp_client/connection.rb b/lib/wamp_client/connection.rb index 953322d..f890af8 100644 --- a/lib/wamp_client/connection.rb +++ b/lib/wamp_client/connection.rb @@ -31,7 +31,7 @@ module WampClient class Connection - attr_accessor :options, :transport_class, :transport, :session + attr_accessor :options, :transport_class, :transport, :session, :verbose @reconnect = true @@ -105,8 +105,9 @@ def on(event, &callback) # @option options [Hash] :headers Custom headers to include during the connection # @option options [WampClient::Serializer::Base] :serializer The serializer to use (default is json) def initialize(options) - self.transport_class = options.delete(:transport) || WampClient::Transport::WebSocket + self.transport_class = options.delete(:transport) || WampClient::Transport::WebSocketEventMachine self.options = options || {} + self.verbose = options[:verbose] || false end # Opens the connection @@ -132,6 +133,7 @@ def close # Leave the session @reconnect = false + @retrying = false session.leave end @@ -140,17 +142,17 @@ def _create_session self.session = WampClient::Session.new(self.transport, self.options) # Setup session callbacks - self.session.on_challenge do |authmethod, extra| + self.session.on(:challenge) do |authmethod, extra| self._finish_retry @on_challenge.call(authmethod, extra) if @on_challenge end - self.session.on_join do |details| + self.session.on(:join) do |details| self._finish_retry @on_join.call(self.session, details) if @on_join end - self.session.on_leave do |reason, details| + self.session.on(:leave) do |reason, details| unless @retrying @on_leave.call(reason, details) if @on_leave @@ -179,7 +181,8 @@ def _create_transport self.transport = self.transport_class.new(self.options) # Setup transport callbacks - self.transport.on_open do + self.transport.on(:open) do + puts "TRANSPORT OPEN" if self.verbose # Call the callback @on_connect.call if @on_connect @@ -189,7 +192,8 @@ def _create_transport end - self.transport.on_close do |reason| + self.transport.on(:close) do |reason| + puts "TRANSPORT CLOSED: #{reason}" if self.verbose @open = false unless @retrying @@ -208,6 +212,10 @@ def _create_transport end end + self.transport.on(:error) do |message| + puts "TRANSPORT ERROR: #{message}" + end + @open = true self.transport.connect @@ -228,7 +236,7 @@ def _retry self._create_transport puts "Attempting Reconnect... Next attempt in #{@retry_timer} seconds" - self.transport_clase.add_timer(@retry_timer) do + self.transport_class.add_timer(@retry_timer*1000) do self._retry if @retrying end end diff --git a/lib/wamp_client/session.rb b/lib/wamp_client/session.rb index fae00d6..8e9d9e0 100644 --- a/lib/wamp_client/session.rb +++ b/lib/wamp_client/session.rb @@ -167,6 +167,20 @@ def on_challenge(&on_challenge) @on_challenge = on_challenge end + # Simple setter for callbacks + def on(event, &callback) + case event + when :join + self.on_join(&callback) + when :challenge + self.on_challenge(&callback) + when :leave + self.on_leave(&callback) + else + raise RuntimeError, "Unknown on(event) '#{event}'" + end + end + attr_accessor :id, :realm, :transport, :verbose, :options # Private attributes @@ -212,6 +226,7 @@ def initialize(transport, options={}) # Setup session callbacks @on_join = nil @on_leave = nil + @on_challenge = nil end diff --git a/lib/wamp_client/transport/base.rb b/lib/wamp_client/transport/base.rb index 4819418..5e4c8fc 100644 --- a/lib/wamp_client/transport/base.rb +++ b/lib/wamp_client/transport/base.rb @@ -52,6 +52,7 @@ def on_message(&on_message) end # Callback when there is an error. Parameters are + # @param msg [String] The message from the error @on_error def on_error(&on_error) @on_error = on_error diff --git a/lib/wamp_client/transport/faye_web_socket.rb b/lib/wamp_client/transport/faye_web_socket.rb index 7825f70..36ea827 100644 --- a/lib/wamp_client/transport/faye_web_socket.rb +++ b/lib/wamp_client/transport/faye_web_socket.rb @@ -42,13 +42,9 @@ def initialize(options) end def connect - self.socket = Faye::WebSocket::Client.new( - self.uri, [self.protocol], - { - :proxy => self.proxy, - :headers => self.headers - } - ) + options = { :headers => self.headers } + options[:proxy] = self.proxy if self.proxy != nil + self.socket = Faye::WebSocket::Client.new(self.uri, [self.protocol], options) self.socket.on(:open) do |event| self.connected = true @@ -63,6 +59,10 @@ def connect self.connected = false @on_close.call(event.reason) if @on_close end + + self.socket.on(:error) do |event| + @on_error.call(event.message) if @on_error + end end def disconnect diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb new file mode 100644 index 0000000..de2b2a7 --- /dev/null +++ b/spec/connection_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe WampClient::Connection do + include RSpec::EM::FakeClock + + before { clock.stub } + after { clock.reset } + + before(:each) { SpecHelper::TestTransport.stop_event_machine } + + let(:options) { + { + uri: 'wss://example.com', + realm: 'realm1', + transport: SpecHelper::TestTransport, + } + } + let(:connection) { described_class.new(options) } + let(:transport) { connection.transport } + let(:session) { connection.session } + + def open_connection + connection.open + clock.tick(1) + end + + def open_session_from_server + # Send welcome form server + welcome = WampClient::Message::Welcome.new(1234, {}) + transport.receive_message(welcome.payload) + end + + def close_session_from_server + # Send goodbye from server + goodbye = WampClient::Message::Goodbye.new({}, 'felt.like.it') + transport.receive_message(goodbye.payload) + end + + def check_em_on + expect(SpecHelper::TestTransport.event_machine_on?).to eq(true) + end + + def check_em_off + expect(SpecHelper::TestTransport.event_machine_on?).to eq(false) + end + + it 'opens the transport/session and sends the hello message' do + called = false + connection.on(:connect) do + called = true + end + + open_connection + + expect(transport).not_to be_nil + expect(session).not_to be_nil + expect(transport.messages.count).to eq(1) + expect(transport.messages[0][0]).to eq(WampClient::Message::Types::HELLO) + + expect(called).to eq(true) + end + + it 'opens the transport/session and joins' do + called = false + connection.on(:join) do + called = true + end + + open_connection + open_session_from_server + + check_em_on + expect(called).to eq(true) + end + + it 'closes the connection' do + left = false + connection.on(:leave) do + left = true + end + disconnected = false + connection.on(:disconnect) do + disconnected = true + end + + open_connection + open_session_from_server + + connection.close + + # Nothing happens until the server responds + check_em_on + expect(left).to eq(false) + expect(disconnected).to eq(false) + + close_session_from_server + + check_em_off + expect(left).to eq(true) + expect(disconnected).to eq(true) + end + + it 'retries if the session is closed from the server' do + left = false + connection.on(:leave) do + left = true + end + + joined = false + connection.on(:join) do + joined = true + end + + open_connection + open_session_from_server + + check_em_on + expect(joined).to eq(true) + joined = false + expect(left).to eq(false) + + close_session_from_server + + check_em_on + expect(joined).to eq(false) + expect(left).to eq(true) + left = false + + clock.tick(5) + open_session_from_server + + check_em_on + + expect(joined).to eq(true) + joined = false + expect(left).to eq(false) + end +end diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 98a8533..b93f7ea 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -1115,6 +1115,11 @@ end context 'timeout' do + include RSpec::EM::FakeClock + + before { clock.stub } + after { clock.reset } + it 'does not cancel a call if no timeout specified' do @defer = WampClient::Defer::ProgressiveCallDefer.new @@ -1123,7 +1128,7 @@ count += 1 end - expect(transport.timer_callback).to be_nil + clock.tick(2) expect(transport.messages.count).to eq(1) end @@ -1135,8 +1140,7 @@ count += 1 end - expect(transport.timer_callback).not_to be_nil - transport.timer_callback.call + clock.tick(2) expect(transport.messages.count).to eq(2) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 601d3d1..75b9e34 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,9 +13,9 @@ module SpecHelper - class TestTransport < WampClient::Transport::Base - - attr_accessor :messages, :timer_callback + class TestTransport < WampClient::Transport::EventMachineBase + @@event_machine_on = false + attr_accessor :messages def initialize(options) super(options) @@ -24,7 +24,27 @@ def initialize(options) end def connect + self.add_timer(1000) do + @on_open.call if @on_open + end + end + def disconnect + @connected = false + @on_close.call if @on_close + end + + def self.start_event_machine(&block) + @@event_machine_on = true + block.call + end + + def self.stop_event_machine + @@event_machine_on = false + end + + def self.event_machine_on? + @@event_machine_on end def send_message(msg) @@ -38,11 +58,7 @@ def receive_message(msg) deserialize = self.serializer.deserialize(serialize) # Call the received message - @on_message.call(deserialize) unless @on_message.nil? - end - - def add_timer(milliseconds, &callback) - self.timer_callback = callback + @on_message.call(deserialize) if @on_message end end From bda4e030ea2d1264a1172910535693b3923a6ce6 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 20:58:02 -0600 Subject: [PATCH 4/7] update gemspec versions --- wamp_client.gemspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wamp_client.gemspec b/wamp_client.gemspec index 2a4af50..c200d8c 100644 --- a/wamp_client.gemspec +++ b/wamp_client.gemspec @@ -18,14 +18,14 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_development_dependency 'bundler', '~> 1.7' - spec.add_development_dependency 'rake', '~> 10.0' - spec.add_development_dependency 'rspec' + spec.add_development_dependency 'bundler', '>= 1.7' + spec.add_development_dependency 'rake', '>= 10.0' + spec.add_development_dependency 'rspec', '>= 3.5.0' spec.add_development_dependency 'rspec-eventmachine', '>= 0.2.0' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'codecov' - spec.add_development_dependency 'faye-websocket' + spec.add_development_dependency 'faye-websocket', '>= 0.10.4' - spec.add_dependency 'websocket-eventmachine-client' + spec.add_dependency 'websocket-eventmachine-client', '>= 1.1.0' spec.add_dependency 'json' end From 97e43d94f97ff07e706b1981250004cc9825fea0 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 21:00:37 -0600 Subject: [PATCH 5/7] more version updates --- wamp_client.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/wamp_client.gemspec b/wamp_client.gemspec index c200d8c..026f2ee 100644 --- a/wamp_client.gemspec +++ b/wamp_client.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov' spec.add_development_dependency 'codecov' spec.add_development_dependency 'faye-websocket', '>= 0.10.4' + spec.required_ruby_version = '>= 2.0' spec.add_dependency 'websocket-eventmachine-client', '>= 1.1.0' spec.add_dependency 'json' From f8be9450ab1d7758899b726ce152a65c38274844 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Thu, 22 Feb 2018 21:11:06 -0600 Subject: [PATCH 6/7] added ruby-version file for circleCI --- .ruby-version | 1 + lib/wamp_client/connection.rb | 2 +- spec/spec_helper.rb | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..359a5b9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.0.0 \ No newline at end of file diff --git a/lib/wamp_client/connection.rb b/lib/wamp_client/connection.rb index f890af8..0c1285d 100644 --- a/lib/wamp_client/connection.rb +++ b/lib/wamp_client/connection.rb @@ -229,7 +229,7 @@ def _finish_retry def _retry - unless self.session&.is_open? + if self.session == nil or not self.session.is_open? @retry_timer = 2*@retry_timer unless @retry_timer == 32 @retrying = true diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 75b9e34..53b4a59 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -68,7 +68,7 @@ class WebSocketEventMachineClientStub def initialize EM.add_timer(1) { - @onopen&.call + @onopen.call if @onopen != nil } end @@ -88,7 +88,7 @@ def onclose(&onclose) end def close - @onclose&.call + @onclose.call if @onclose != nil true end @@ -97,7 +97,7 @@ def send(message, type) end def receive(message) - @onmessage&.call(message, {type:'text'}) + @onmessage.call(message, {type:'text'}) if @onmessage != nil end end @@ -111,7 +111,7 @@ class Event def initialize EM.add_timer(1) { - @on_open&.call(Event.new) + @on_open.call(Event.new) if @on_open != nil } end @@ -131,7 +131,7 @@ def on(event, &block) def close event = Event.new event.reason = 'closed' - @on_close&.call(event) + @on_close.call(event) if @on_close != nil end def send(message) @@ -141,7 +141,7 @@ def send(message) def receive(message) event = Event.new event.data = message - @on_message&.call(event) + @on_message.call(event) if @on_message != nil end end From cd69da917cee4fc00499d1fc2d6ed6925254d73d Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sat, 24 Feb 2018 02:20:33 -0600 Subject: [PATCH 7/7] added 'default transport' test coverage --- lib/wamp_client/connection.rb | 2 +- spec/connection_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/wamp_client/connection.rb b/lib/wamp_client/connection.rb index 0c1285d..a6067db 100644 --- a/lib/wamp_client/connection.rb +++ b/lib/wamp_client/connection.rb @@ -235,7 +235,7 @@ def _retry self._create_transport - puts "Attempting Reconnect... Next attempt in #{@retry_timer} seconds" + puts "Attempting Reconnect... Next attempt in #{@retry_timer} seconds" if self.verbose self.transport_class.add_timer(@retry_timer*1000) do self._retry if @retrying end diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb index de2b2a7..624fe4f 100644 --- a/spec/connection_spec.rb +++ b/spec/connection_spec.rb @@ -44,6 +44,18 @@ def check_em_off expect(SpecHelper::TestTransport.event_machine_on?).to eq(false) end + describe 'transport' do + it 'selects the default transport' do + connection = described_class.new({}) + expect(connection.transport_class).to be(WampClient::Transport::WebSocketEventMachine) + end + + it 'overrides the default transport' do + connection = described_class.new({ transport: WampClient::Transport::FayeWebSocket }) + expect(connection.transport_class).to be(WampClient::Transport::FayeWebSocket) + end + end + it 'opens the transport/session and sends the hello message' do called = false connection.on(:connect) do