stomp_out 0.1.0

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.
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ cache: bundler
3
+ bundler_args: --without debugger development
4
+ rvm:
5
+ - 1.8
6
+ - 1.9
7
+ - 2.0
8
+ - 2.1
9
+ script:
10
+ - bundle exec rake ci:spec
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ === 0.1.0 (2015-01-21)
2
+
3
+ Initial commit
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 RightScale, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,91 @@
1
+ = Introduction
2
+
3
+ StompOut is an implementation of the Simple Text Orientated Messaging Protocol (STOMP[http://stomp.github.io]).
4
+ Both a client and server interface to the protocol are provided.
5
+ They are implemented independent of the underlying network protocol over which they are applied.
6
+ In that sense they run outboard of the underlying connection, hence "Out" in the name.
7
+ The Client and Server classes provide a function for receiving blocks of STOMP encoded
8
+ data from the connection and they expect the subclass to implement a function
9
+ for sending data that has been STOMP encoded over the connection.
10
+
11
+ The Client and Server classes support version 1.0, 1.1, and 1.2 of STOMP. The built-in version
12
+ negotiation in the protocol determines which is actually applied for a given connection.
13
+
14
+ = Interface
15
+
16
+ == Client
17
+
18
+ There are two parts to the interface: the functions the StompOut::Client[link:lib/stomp_out/client.rb] class exposes
19
+ and the functions the Client subclass must implement.
20
+
21
+ The Client exposes the following functions:
22
+ * <b>receive_data</b> - process data received over connection from server
23
+ * <b>connect</b> - connect to server
24
+ * <b>message</b> - send message to given destination
25
+ * <b>subscribe</b> - register to listen to given destination
26
+ * <b>unsubscribe</b> - remove an existing subscription
27
+ * <b>ack</b> - acknowledge consumption of a message from a subscription
28
+ * <b>nack</b> - tell the server that a message was not consumed
29
+ * <b>begin</b> - start a transaction
30
+ * <b>commit</b> - commit a transaction
31
+ * <b>abort</b> - roll back a transaction
32
+ * <b>disconnect</b> - disconnect from the server
33
+ * <b>subscriptions</b> - list active subscriptions
34
+ * <b>transactions</b> - list active transactions
35
+
36
+ The Client subclass must implement the following functions:
37
+ * <b>send_data</b> - send data over connection to server
38
+ * <b>on_connected</b> - handle notification that now connected
39
+ * <b>on_message</b> - handle message received from server
40
+ * <b>on_receipt</b> - handle notification that request was successfully handled by server
41
+ * <b>on_error</b> - handle notification that a client request failed and connection should be closed
42
+
43
+ == Server
44
+
45
+ There are two parts to the interface: the functions the StompOut::Server[link:lib/stomp_out/server.rb] class exposes
46
+ and the functions the Server subclass must implement.
47
+
48
+ The Server exposes the following functions:
49
+ * <b>receive_data</b> - process data received over connection from client
50
+ * <b>disconnect</b> - stop service in anticipation of connection being closed
51
+ * <b>message</b> - send message from a subscribed destination to client
52
+
53
+ The Server subsclass must implement the following functions:
54
+ * <b>send_data</b> - send data over connection to client
55
+ * <b>on_connect</b> - handle connect request from client including any authentication
56
+ * <b>on_message</b> - handle delivery of message from client to given destination
57
+ * <b>on_subscribe</b> - subscribe client to messages from given destination
58
+ * <b>on_unsubscribe</b> - unsubscribe client from given destination
59
+ * <b>on_ack</b> - handle acknowledgement from client that a message has been successfully processed
60
+ * <b>on_nack</b> - handle negative acknowledgement from client for a message
61
+ * <b>on_error</b> - handle notification that a client or server request failed and connection should be closed
62
+ * <b>on_disconnect</b> - handle request from client to close connection
63
+
64
+ The above functions should raise StompOut::ApplicationError[link:lib/stomp_out/errors.rb] for requests
65
+ that violate any application specific constraints.
66
+
67
+ = Example
68
+
69
+ The gem contains client[link:examples/websocket_client.rb] and server[link:examples/websocket_server.rb]
70
+ examples of how it can be applied in a WebSocket environment. The server example can be run in the
71
+ examples directory using thin:
72
+ thin -R config.ru start
73
+ The client example is a script that can executed against the server, e.g., to produce messages:
74
+ ./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -m "hi"
75
+ ./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -m "bye"
76
+ and to consume messages:
77
+ ./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -a client -r
78
+
79
+ = Dependencies
80
+
81
+ The simple_uuid gem is used by the Server for generating subscription identifiers.
82
+ The eventmachine gem is used only if the STOMP heartbeat feature is used.
83
+ The the json gem is used by the Client only if a content-type is "application/json"
84
+ and the :auto_json option is enabled.
85
+ The examples make use of the faye-websocket, eventmachine, trollop, and json gems.
86
+
87
+ = License
88
+
89
+ This software is released under the {MIT License}[http://www.opensource.org/licenses/MIT]. Please see link:LICENSE for further details.
90
+
91
+ Copyright (c) 2015 RightScale
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ # -*-ruby-*-
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'right_develop'
5
+ require 'spec/rake/spectask'
6
+ require 'rubygems/package_task'
7
+ require 'rake/clean'
8
+
9
+ task :default => [:spec]
10
+
11
+ desc "Run unit tests"
12
+ Spec::Rake::SpecTask.new do |t|
13
+ t.spec_files = Dir['**/*_spec.rb']
14
+ t.spec_opts = lambda do
15
+ IO.readlines(File.join(File.dirname(__FILE__), 'spec', 'spec.opts')).map {|l| l.chomp.split " "}.flatten
16
+ end
17
+ end
18
+
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |gem|
21
+ # gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options
22
+ gem.name = "stomp_out"
23
+ gem.homepage = "https://github.com/rightscale/stomp_out"
24
+ gem.license = "MIT"
25
+ gem.summary = %Q{Client and server for STOMP protocol that operate outboard of separately supplied network connection.}
26
+ gem.description = %Q{This implementation of STOMP is aimed at environments where a network connection, such as a WebSocket or TCP socket, is created and then raw data from that connection is passed to/from the STOMP client or server messaging layer provided by this gem.}
27
+ gem.email = "support@rightscale.com"
28
+ gem.authors = ["Lee Kirchhoff"]
29
+ gem.files.exclude "Gemfile*"
30
+ gem.files.exclude "spec/**/*"
31
+ end
32
+ Jeweler::RubygemsDotOrgTasks.new
33
+
34
+ CLEAN.include("pkg")
35
+
36
+ RightDevelop::CI::RakeTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
3
+ require 'bundler/setup'
4
+ require 'rack'
5
+ require 'rack/server'
6
+ require 'eventmachine'
7
+ require 'rack/websocket'
8
+ require 'stomp_out'
9
+ require 'websocket_server'
10
+
11
+ map '/' do
12
+ run WebSocketServerApp.new
13
+ end
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
5
+ require 'bundler/setup'
6
+ require 'eventmachine'
7
+ require 'faye/websocket'
8
+ require 'trollop'
9
+ require 'json'
10
+ require 'stomp_out'
11
+
12
+ # Example of a StompOut::Client subclass in a WebSocket environment
13
+ class WebSocketClient < StompOut::Client
14
+
15
+ def initialize(options = {})
16
+ options = options.dup
17
+ @parent = options.delete(:parent)
18
+ @destination = options.delete(:destination)
19
+ @ack = options.delete(:ack) || "auto"
20
+ @message = options.delete(:message)
21
+ @receipts = {}
22
+
23
+ super(options)
24
+ end
25
+
26
+ def send_data(data)
27
+ @parent.send_data(data)
28
+ end
29
+
30
+ def on_connected(frame, session_id, server_name)
31
+ puts "connected to #{server_name} for session #{session_id}"
32
+ if @message
33
+ receipt_id = message(@destination, @message)
34
+ @receipts[receipt_id] = "message to #{@destination}" if receipt_id
35
+ close
36
+ else
37
+ puts "subscribing to #{@destination} with ack #{@ack}"
38
+ receipt_id = subscribe(@destination, @ack, receipt = true)
39
+ @receipts[receipt_id] = "subscribe to #{@destination} with ack #{@ack}" if receipt_id
40
+ end
41
+ end
42
+
43
+ def on_message(frame, destination, message, content_type, message_id, ack_id)
44
+ puts "received #{content_type} message #{message_id} from #{destination} " +
45
+ "with ack #{ack_id.inspect}: #{message.inspect}"
46
+ if @ack != "auto"
47
+ receipt_id = ack(ack_id)
48
+ @receipts[receipt_id] = "ack #{ack_id}" if receipt_id
49
+ end
50
+ end
51
+
52
+ def on_receipt(frame, receipt_id)
53
+ @subscribed = true if @receipts[receipt_id].to_s =~ /subscribe to/
54
+ puts "received receipt #{receipt_id} for #{@receipts.delete(receipt_id).inspect}"
55
+ end
56
+
57
+ def on_error(frame, error, details, receipt_id)
58
+ receipt = receipt_id ? " with receipt #{receipt_id.inspect}" : nil
59
+ puts "error#{receipt}: #{error}" + (details ? "\n#{details}" : "")
60
+ close
61
+ end
62
+
63
+ def close
64
+ if connected?
65
+ if @subscribed
66
+ @subscribed = false
67
+ puts "unsubscribing from #{@destination}"
68
+ receipt_id = unsubscribe(@destination)
69
+ @receipts[receipt_id] = "unsubscribe from #{@destination}" if receipt_id
70
+ end
71
+ receipt_id = disconnect
72
+ @receipts[receipt_id] = "disconnect" if receipt_id
73
+ @parent.stop
74
+ end
75
+ end
76
+ end
77
+
78
+ # Simple application using WebSocketClient
79
+ class WebSocketClientApp
80
+
81
+ def self.run
82
+ r = WebSocketClientApp.new
83
+ options = r.parse_args
84
+ r.start(options)
85
+ end
86
+
87
+ def start(options)
88
+ ['INT', 'TERM'].each do |signal|
89
+ trap(signal) do stop end
90
+ end
91
+
92
+ EM.run do
93
+ @client = WebSocketClient.new(options.merge(:parent => self, :name => self.class.name, :auto_json => true))
94
+ @websocket = Faye::WebSocket::Client.new(options[:url])
95
+ @websocket.onerror = lambda { |e| puts "error #{e.message}"; stop }
96
+ @websocket.onclose = lambda { |e| puts "close #{e.code} #{e.reason}"; stop }
97
+ @websocket.onmessage = lambda { |e| puts "received #{e.data}"; @client.receive_data(JSON.load(e.data)) }
98
+ @client.connect
99
+ end
100
+ end
101
+
102
+ def send_data(data)
103
+ data = JSON.dump(data)
104
+ puts "sending: #{data}"
105
+ @websocket.send(data)
106
+ end
107
+
108
+ def stop
109
+ if EM.reactor_running?
110
+ @client.close if @client
111
+
112
+ EM.next_tick do
113
+ @websocket.close if @websocket
114
+ EM.next_tick { EM.stop }
115
+ end
116
+ end
117
+ end
118
+
119
+ def parse_args
120
+ options = {}
121
+
122
+ parser = Trollop::Parser.new do
123
+ opt :url, "server WebSocket URL", :default => nil, :type => String, :short => "-u", :required => true
124
+ opt :destination, "messaging destination", :default => nil, :type => String, :short => "-d", :required => true
125
+ opt :host, "server virtual host name", :default => nil, :type => String, :short => "-h"
126
+ opt :ack, "auto, client, or client-individual acks", :default => nil, :type => String, :short => "-a"
127
+ opt :receipt, "enable receipts", :default => nil, :short => "-r"
128
+ opt :message, "send message to destination", :default => nil, :type => String, :short => "-m"
129
+ version ""
130
+ end
131
+
132
+ parse do
133
+ options.merge!(parser.parse)
134
+ end
135
+ end
136
+
137
+ def parse
138
+ begin
139
+ yield
140
+ rescue Trollop::VersionNeeded
141
+ puts version
142
+ exit 0
143
+ rescue Trollop::HelpNeeded
144
+ puts "Usage: websocket_client --url <s> --destination <s> [--host <s> --ack <s> --receipt --message <s>]"
145
+ exit 0
146
+ rescue Trollop::CommandlineError => e
147
+ STDERR.puts e.message + "\nUse --help for additional information"
148
+ exit 1
149
+ end
150
+ end
151
+ end
152
+
153
+ WebSocketClientApp.run
@@ -0,0 +1,120 @@
1
+
2
+ # Example of a StompOut::Server subclass in a WebSocket environment
3
+ class WebSocketServer < StompOut::Server
4
+
5
+ def initialize(options = {})
6
+ options = options.dup
7
+ @parent = options.delete(:parent)
8
+ @subscriptions = {}
9
+ @message_ids = {}
10
+ super(options.merge(:name => self.class.name))
11
+ end
12
+
13
+ def send_data(data)
14
+ @parent.send_data(data)
15
+ end
16
+
17
+ def on_connect(frame, login, passcode, host, session_id)
18
+ true
19
+ end
20
+
21
+ def on_message(frame, destination, message, content_type)
22
+ @parent.deliver_message(destination, message, content_type)
23
+ end
24
+
25
+ def on_subscribe(frame, id, destination, ack_setting)
26
+ @subscriptions[id] = {:destination => destination, :ack_setting => ack_setting}
27
+ end
28
+
29
+ def on_unsubscribe(frame, id, destination)
30
+ @subscriptions.delete(id)
31
+ end
32
+
33
+ def on_ack(frame, id)
34
+ @parent.delete_message(@message_ids.delete(id))
35
+ end
36
+
37
+ def on_nack(frame, id)
38
+ @message_ids.delete(id)
39
+ end
40
+
41
+ def on_error(frame, error)
42
+ EM.next_tick { @parent.close_websocket }
43
+ end
44
+
45
+ def on_disconnect(frame, reason)
46
+ EM.next_tick { @parent.close_websocket }
47
+ end
48
+
49
+ def deliver_messages(destination, messages)
50
+ @subscriptions.each do |id, s|
51
+ if s[:destination] == destination
52
+ messages.each do |m|
53
+ headers = {
54
+ "subscription" => id,
55
+ "destination" => destination,
56
+ "message-id" => m[:id].to_s,
57
+ "content-type" => m[:content_type] }
58
+ message_id, ack_id = message(headers, m[:message])
59
+ if s[:ack_setting] == "auto"
60
+ @parent.delete_message(message_id)
61
+ else
62
+ @message_ids[ack_id] = message_id
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Simple WebSocket Rack application using WebSocketServer
71
+ class WebSocketServerApp < Rack::WebSocket::Application
72
+
73
+ @@servers = {}
74
+ @@connections = {}
75
+ @@connection_id = 0
76
+ @@messages = Hash.new { |h, k| h[k] = [] }
77
+ @@message_id = 0
78
+
79
+ def on_open(env)
80
+ socket = env["async.connection"]
81
+ @@connections[socket] = @@connection_id += 1
82
+ puts "opened connection #{@@connection_id}"
83
+ @@servers[socket] = WebSocketServer.new(:parent => self)
84
+ end
85
+
86
+ def on_close(env)
87
+ socket = env["async.connection"]
88
+ connection = @@connections.delete(socket)
89
+ puts "closed connection #{connection}"
90
+ @@servers.delete(socket)
91
+ end
92
+
93
+ def on_error(env, error)
94
+ socket = env["async.connection"]
95
+ connection = @@connections[socket]
96
+ STDERR.puts "error on connection #{connection} (#{error})"
97
+ end
98
+
99
+ def on_message(env, message)
100
+ socket = env["async.connection"]
101
+ connection = @@connections[socket]
102
+ puts "received #{message} on connection #{connection}"
103
+ @@servers[socket].receive_data(JSON.load(message))
104
+ end
105
+
106
+ def send_data(data)
107
+ data = JSON.dump(data)
108
+ puts "sending #{data}"
109
+ super(data)
110
+ end
111
+
112
+ def deliver_message(destination, message, content_type)
113
+ @@messages[destination] << {:id => (@@message_id += 1).to_s, :message => message, :content_type => content_type}
114
+ @@servers.each_value { |s| s.deliver_messages(destination, @@messages[destination]) }
115
+ end
116
+
117
+ def delete_message(id)
118
+ @@messages.each_value { |ms| ms.reject! { |m| m[:id] == id } }
119
+ end
120
+ end