wamp_server 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ wamp_server
2
+ ===========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/rubencaro/wamp_server.png?branch=master)](http://travis-ci.org/rubencaro/wamp_server)
5
+
6
+ [WAMPv1](http://wamp.ws/spec) compliant server to be used
7
+ as a template for nice and shining Ruby apps, based on
8
+ [WebSocket EventMachine Server](https://github.com/imanel/websocket-eventmachine-server).
@@ -0,0 +1,9 @@
1
+
2
+ require 'rake/testtask'
3
+ Rake::TestTask.new(:test) do |test|
4
+ test.libs << 'test'
5
+ test.warning = false
6
+ test.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ module WAMP
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,122 @@
1
+ require 'websocket-eventmachine-server'
2
+ require 'json'
3
+ require_relative 'version'
4
+
5
+ #
6
+ # see http://wamp.ws/spec
7
+ #
8
+ module WAMP
9
+
10
+ WELCOME = 0
11
+ PREFIX = 1
12
+ CALL = 2
13
+ CALLRESULT = 3
14
+ CALLERROR = 4
15
+ SUBSCRIBE = 5
16
+ UNSUBSCRIBE = 6
17
+ PUBLISH = 7
18
+ EVENT = 8
19
+
20
+ RESPONSES = { :not_json => { :error => 'Message is not JSON !' }.to_json }
21
+
22
+ def self.stamp
23
+ "#{self.name} #{VERSION}"
24
+ end
25
+
26
+ def self.stop_server
27
+ puts "Terminating WebSocket Server"
28
+ EM.stop
29
+ end
30
+
31
+ # Start a new Server
32
+ #
33
+ # You should pass some callbacks:
34
+ #
35
+ # :before_start => To be called before actually starting the server, but already within EM reactor.
36
+ # :onwelcome => To be called for WELCOME calls.
37
+ # :onprefix => To be called for PREFIX calls.
38
+ # :oncall => To be called when server receives a CALL call.
39
+ # :onpublish => To be called when server receives a PUBLISH call.
40
+ # :onclose => To be called when a client closes connection.
41
+ # :onsubscribe => Called for SUBSCRIBE calls.
42
+ # :onunsubscribe => On UNSUBSCRIBE calls.
43
+ #
44
+ def self.start_server(a_module, opts = {})
45
+
46
+ # avoid more instances
47
+ a = a_module
48
+ a.extend a
49
+
50
+ host = opts[:host] || '0.0.0.0'
51
+ port = opts[:port] || '3000'
52
+
53
+ EM.run do
54
+
55
+ trap("INT") { WAMP.stop_server }
56
+ trap("TERM") { WAMP.stop_server }
57
+ trap("KILL") { WAMP.stop_server }
58
+
59
+ a.before_start if a.respond_to?(:before_start)
60
+
61
+ puts "Listening on #{host}:#{port}"
62
+ WebSocket::EventMachine::Server.start(:host => host, :port => port) do |ws|
63
+
64
+ ws.onopen do
65
+ Fiber.new do
66
+ sid = a.onwelcome ws: ws
67
+ ws.send [WAMP::WELCOME, sid, 1, WAMP.stamp].to_json
68
+ end.resume
69
+ end
70
+
71
+ ws.onmessage do |msg, type|
72
+ Fiber.new do
73
+ begin
74
+ call = JSON.parse msg
75
+ rescue JSON::ParserError
76
+ ws.send( WAMP::RESPONSES[:not_json] )
77
+ end
78
+
79
+ if call.first == WAMP::CALL then
80
+ # [ TYPE_ID_CALL , callID , procURI , ... ]
81
+ _, callid, curie, *args = call
82
+ begin
83
+ result = a.oncall ws: ws, curie: curie, args: args
84
+ ws.send [WAMP::CALLRESULT, callid, result].to_json
85
+ rescue => ex
86
+ ws.send [WAMP::CALLERROR, callid, "http://error", ex.to_s].to_json
87
+ end
88
+ elsif call.first == WAMP::PUBLISH then
89
+ # [ TYPE_ID_PUBLISH , topicURI , event , exclude , eligible ]
90
+ _, curie, event, excluded, eligible = call
91
+ result = a.onpublish curie: curie
92
+ dest = (result[:subscribed] & eligible) - excluded
93
+ package = [WAMP::EVENT, result[:uri], event].to_json
94
+ dest.each do |d|
95
+ ObjectSpace._id2ref(d).send( package )
96
+ end
97
+ elsif call.first == WAMP::SUBSCRIBE then
98
+ # [ TYPE_ID_SUBSCRIBE , topicURI ]
99
+ a.onsubscribe ws: ws, curie: call[1]
100
+ elsif call.first == WAMP::UNSUBSCRIBE then
101
+ # [ TYPE_ID_UNSUBSCRIBE , topicURI ]
102
+ a.onunsubscribe ws: ws, curie: call[1]
103
+ elsif call.first == WAMP::PREFIX then
104
+ # [ TYPE_ID_PREFIX , prefix, URI ]
105
+ _, prefix, uri = call
106
+ a.onprefix ws: ws, uri: uri, prefix: prefix
107
+ end
108
+ end.resume
109
+ end
110
+
111
+ ws.onclose do
112
+ Fiber.new do
113
+ a.onclose ws: ws
114
+ end.resume
115
+ end
116
+
117
+ end # Server.start
118
+ end # EM.run
119
+
120
+ end
121
+
122
+ end
@@ -0,0 +1,104 @@
1
+
2
+ module Helpers
3
+ module Log
4
+
5
+ @@next_color = 0 # for alternate
6
+ @@palette = [:dark_gray, :light_gray]
7
+
8
+ def log(msg, opts = {})
9
+
10
+ if not opts[:clean] then
11
+ opts[:location] ||= caller_locations(1,1)[0].label # much faster than 'caller'
12
+ msg = "[#{opts[:location]}] #{msg}"
13
+ end
14
+
15
+ if opts[:color] then
16
+ msg = send(opts[:color],msg)
17
+ end
18
+
19
+ msg = "[#{Time.now.strftime('%F %T')}]" + msg if not opts[:clean]
20
+ puts msg
21
+ $stdout.flush # as soon as possible
22
+ end
23
+
24
+ def log_ex(ex, opts = {})
25
+ trace = opts[:trace] || true
26
+ msg = opts[:msg] || ''
27
+ msg += light_purple(" \n Exception: #{ex} \n ")
28
+ msg << purple(" Backtrace: #{ex.backtrace.join("\n")} ") if trace
29
+ log msg
30
+ end
31
+
32
+ def spit(msg, opts = {}) # allow hashes as msg
33
+ opts[:color] ||= :light_red
34
+ opts[:clean] = true
35
+ l = caller_locations(1,1)[0]
36
+ log "\n[#{l.label}:#{l.lineno}] \n " + msg.inspect + " \n ", opts
37
+ end
38
+
39
+ def announce(msg = nil, opts = {})
40
+ label = caller_locations(1,1)[0].label
41
+ place = File.basename(caller_locations(1,1)[0].path) + ":" + caller_locations(1,1)[0].lineno.to_s
42
+ msg ||= "\n ==> Entering #{label} (#{place})..."
43
+ opts[:color] ||= :cyan
44
+ opts[:clean] = true
45
+ log msg, opts
46
+ end
47
+
48
+ def yellow(str)
49
+ " \033[1;33m " + str + " \033[00m "
50
+ end
51
+
52
+ def cyan(str)
53
+ " \033[0;36m " + str + " \033[00m "
54
+ end
55
+
56
+ def light_cyan(str)
57
+ " \033[1;36m " + str + " \033[00m "
58
+ end
59
+
60
+ def purple(str)
61
+ " \033[0;35m " + str + " \033[00m "
62
+ end
63
+
64
+ def light_purple(str)
65
+ " \033[1;35m " + str + " \033[00m "
66
+ end
67
+
68
+ def brown(str)
69
+ " \033[0;33m " + str + " \033[00m "
70
+ end
71
+
72
+ def red(str)
73
+ " \033[0;31m " + str + " \033[00m "
74
+ end
75
+
76
+ def light_red(str)
77
+ " \033[1;31m " + str + " \033[00m "
78
+ end
79
+
80
+ def light_gray(str)
81
+ " \033[0;37m " + str + " \033[00m "
82
+ end
83
+
84
+ def dark_gray(str)
85
+ " \033[1;30m " + str + " \033[00m "
86
+ end
87
+
88
+ def white(str)
89
+ " \033[1;37m " + str + " \033[00m "
90
+ end
91
+
92
+ def alternate(str)
93
+ color = @@palette[@@next_color]
94
+ @@next_color += 1
95
+ @@next_color = 0 if @@next_color >= @@palette.size
96
+ send(color,str)
97
+ end
98
+
99
+ end
100
+
101
+ extend Log
102
+ end
103
+
104
+ H = Helpers if not defined? H
@@ -0,0 +1,187 @@
1
+ require_relative 'test_helper'
2
+ require_relative '../lib/wamp_server'
3
+
4
+ class TestMain < TestCase
5
+
6
+ def test_plain_ws
7
+ cb = lambda do |ws,msg,type,info|
8
+ ws.close
9
+ end
10
+ info = run_ws_client onmessage: cb
11
+ assert_equal 1, info[:messages].count
12
+ end
13
+
14
+ def test_welcome
15
+ cb = lambda do |ws,msg,type,info|
16
+ data = check_is_json msg
17
+ check_is_welcome data
18
+ result = call(ws,'get_db','sessions')
19
+ assert_equal 1, result.count, "#{result}"
20
+ assert result.first['_id'], result
21
+ ws.close
22
+ end
23
+ info = run_ws_client onmessage: cb
24
+ assert_equal 1, info[:messages].count, info
25
+ end
26
+
27
+ def test_prefix
28
+ cb = lambda do |ws,msg,type,info|
29
+ data = check_is_json msg
30
+
31
+ assert_equal WAMP::WELCOME, data.first, "#{data}"
32
+
33
+ uri = "http://test#"
34
+ prefix = 'test'
35
+
36
+ ws.send [WAMP::PREFIX, prefix, uri].to_json
37
+ result = call(ws,'get_db','sessions')
38
+ assert_equal 1, result.count, result
39
+ assert_equal uri, result.first['prefixes'][prefix], "#{result}"
40
+ ws.close
41
+ end
42
+ info = run_ws_client onmessage: cb
43
+ assert_equal 1, info[:messages].count, info
44
+ end
45
+
46
+ def test_rpc
47
+ controller = 'test' # TestController
48
+ action = 'get_db' # get_db_action
49
+ args = ['sessions'] # 'table' argument for get_db_action
50
+ callid = 'right one'
51
+ cmd = [WAMP::CALL, callid, "http://#{controller}##{action}"] + args
52
+
53
+ cb = lambda do |ws,msg,type,info|
54
+ data = check_is_json msg
55
+ if data.first == WAMP::WELCOME then
56
+ # send the right request right after welcome
57
+ ws.send cmd.to_json
58
+ elsif data.first == WAMP::CALLRESULT then
59
+ # assert the right answer for the right request
60
+ assert_equal 'right one', data[1], data
61
+ result = data.last
62
+ assert_equal 1, result.count, "#{result}"
63
+ assert result.first['_id'], result
64
+
65
+ # send the wrong request to force error
66
+ callid = 'wrong one'
67
+ controller = 'not_existing'
68
+ cmd = [WAMP::CALL, callid, "http://#{controller}##{action}"] + args
69
+ ws.send cmd.to_json
70
+ else # WAMP::CALLERROR
71
+ # assert the right answer for the wrong request
72
+ assert_equal WAMP::CALLERROR, data.first, "#{data}"
73
+ assert_equal 'wrong one', data[1], data
74
+ assert data.last =~ /Not Found/
75
+ ws.close
76
+ end
77
+ end
78
+ info = run_ws_client onmessage: cb
79
+ assert_equal 3, info[:messages].count, info
80
+ end
81
+
82
+ def test_subscribe_unsubscribe
83
+
84
+ uri = 'http://test/subscribe_test'
85
+
86
+ cb = lambda do |ws,msg,type,info|
87
+ data = check_is_json msg
88
+ assert_equal WAMP::WELCOME, data.first, data
89
+
90
+ # send the subscribe request
91
+ ws.send [WAMP::SUBSCRIBE,uri].to_json
92
+
93
+ # ensure it's subscribed
94
+ # subscription is saved within the session
95
+ # in a hash where the uri is the key
96
+ result = call(ws,'get_db','sessions')
97
+ session = result.select{|r| r['_id'] == data[1]}.first
98
+ assert session, "#{result} \n #{data}"
99
+ subs = session['subscriptions']
100
+ assert subs && (subs.count == 1), session
101
+ assert subs[uri], subs
102
+
103
+ # send the unsubscribe request
104
+ ws.send [WAMP::UNSUBSCRIBE,uri].to_json
105
+
106
+ # ensure it's subscribed
107
+ result = call(ws,'get_db','sessions')
108
+ session = result.select{|r| r['_id'] == data[1]}.first
109
+ subs = session['subscriptions']
110
+ assert subs && subs.none?, session
111
+
112
+ ws.close
113
+ end
114
+ info = run_ws_client onmessage: cb
115
+ assert_equal 1, info[:messages].count, info
116
+ end
117
+
118
+ def test_publish
119
+
120
+ sat_info = { :sats => {}, :sent => 0, :received => {} }
121
+ n = 5
122
+ uri = 'http://test/subscribe_test'
123
+ lucky_ones = banned_ones = []
124
+
125
+ cb = lambda do |ws,msg,type,info|
126
+ data = check_is_json msg
127
+ assert_equal WAMP::WELCOME, data.first, data
128
+ sid = data[1]
129
+
130
+ # run n satellite clients and subscribe them to our topic
131
+ subscribe_cb = lambda do |ws_sat,msg,type,info|
132
+ data = check_is_json msg
133
+ if data.first == WAMP::WELCOME then
134
+ sat_info[:sats][data[1]] = ws_sat # save it to be closed afterwards
135
+ ws_sat.send [WAMP::SUBSCRIBE,uri].to_json
136
+ sat_info[:sent] += 1
137
+ elsif data.first == WAMP::EVENT then
138
+ sat_info[:received][ws_sat.object_id] ||= []
139
+ sat_info[:received][ws_sat.object_id] << msg
140
+ end
141
+ end
142
+ n.times do
143
+ run_satellite_ws_client info, :onmessage => subscribe_cb
144
+ end
145
+
146
+ # ensure we have audience
147
+ wait_for { sat_info[:sent] >= n } # until everyone makes the request
148
+ sessions = call(ws,'get_db','sessions')
149
+ assert_equal n+1, sessions.count, sessions
150
+ sessions.reject{|s| s['_id'] == sid}.each do |s|
151
+ subs = s['subscriptions']
152
+ assert subs && (subs.count == 1), s
153
+ assert subs[uri], subs
154
+ end
155
+
156
+ # publish only within the lucky_ones, but not the banned_ones
157
+ lucky_ones = sat_info[:sats].keys[0..-1] # all but last are allowed
158
+ banned_ones = sat_info[:sats].keys[0..0] # first is banned
159
+ ws.send [WAMP::PUBLISH,uri,'hello',banned_ones,lucky_ones].to_json
160
+
161
+ # wait until everyone get the event
162
+ wait_for { sat_info[:received].count >= (lucky_ones - banned_ones).count }
163
+
164
+ # test is received by the lucky_ones, not the banned_ones
165
+ int = sat_info[:received].keys & lucky_ones
166
+ assert_equal 0, int.count, int
167
+
168
+ sat_info[:sats].values.each{ |s| s.close }
169
+ ws.close
170
+ end
171
+ info = run_ws_client onmessage: cb
172
+
173
+ # test is published to the lucky_ones, not the banned_ones
174
+ events = info[:messages].select{ |i| i.first == WAMP::EVENT }
175
+ assert_equal (lucky_ones - banned_ones).count, events.count, events
176
+ end
177
+
178
+ private
179
+
180
+ def check_is_welcome(data)
181
+ # [ TYPE_ID_WELCOME , sessionId , protocolVersion, serverIdent ]
182
+ assert_equal 4, data.count
183
+ assert_equal WAMP::WELCOME, data[0]
184
+ assert_equal 1, data[2] # protocol version
185
+ end
186
+
187
+ end
@@ -0,0 +1,11 @@
1
+
2
+ class String
3
+ # simplified version of rails', only get asdf_gfre from AsdfGfre
4
+ def underscore
5
+ word = dup
6
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
7
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
8
+ word.tr!("-", "_")
9
+ word.downcase!
10
+ end
11
+ end