nutella_lib 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/nutella_lib/core.rb +65 -11
- data/lib/nutella_lib/ext/kernel.rb +1 -0
- data/lib/nutella_lib/net.rb +264 -140
- data/lib/nutella_lib/net_app.rb +271 -0
- data/lib/nutella_lib/persist.rb +15 -3
- data/lib/nutella_lib.rb +1 -12
- data/lib/simple_mqtt_client/simple_mqtt_client.rb +77 -57
- data/nutella_lib.gemspec +7 -5
- data/test/test_nutella_net.rb +79 -0
- data/test/test_nutella_net_app.rb +104 -0
- data/test/test_simple_mqtt_client.rb +50 -2
- metadata +6 -4
- data/test/test_nutella_lib.rb +0 -78
@@ -0,0 +1,271 @@
|
|
1
|
+
module Nutella
|
2
|
+
module Net
|
3
|
+
|
4
|
+
# This module implements the pub/sub and request/response APIs at the application level
|
5
|
+
module App
|
6
|
+
|
7
|
+
|
8
|
+
# @!group Application-level communication APIs
|
9
|
+
|
10
|
+
# Subscribes to a channel or to a set of channels at the application-level.
|
11
|
+
#
|
12
|
+
# @param [String] channel the application-level channel or filter we are subscribing to. Can contain wildcard(s)
|
13
|
+
# @param [Proc] callback a lambda expression that is fired whenever a message is received.
|
14
|
+
# The passed callback takes the following parameters:
|
15
|
+
# - [String] message: the received message. Messages that are not JSON are discarded.
|
16
|
+
# - [String] channel: the application-level channel the message was received on (optional, only for wildcard subscriptions)
|
17
|
+
# - [Hash] from: the sender's identifiers (run_id, app_id, component_id and optionally resource_id)
|
18
|
+
def App.subscribe (channel, callback)
|
19
|
+
Nutella::Net.subscribe_to(channel, callback, Nutella.app_id, nil)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Un-subscribes from an application-level channel
|
24
|
+
#
|
25
|
+
# @param [String] channel the application level channel we want to unsubscribe from. Can contain wildcard(s).
|
26
|
+
def App.unsubscribe( channel )
|
27
|
+
Nutella::Net.unsubscribe_to(channel, Nutella.app_id, nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Publishes a message to an application-level channel
|
32
|
+
#
|
33
|
+
# @param [String] channel the application-level channel we want to publish the message to. *CANNOT* contain wildcard(s)!
|
34
|
+
# @param [Object] message the message we are publishing. This can be,
|
35
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
36
|
+
def App.publish(channel, message=nil)
|
37
|
+
Nutella::Net.publish_to(channel, message, Nutella.app_id, nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# Performs a synchronous request at the application-level
|
42
|
+
#
|
43
|
+
# @param [String] channel the application-level channel we want to make the request to. *CANNOT* contain wildcard(s)!
|
44
|
+
# @param [Object] message the body of request. This can be,
|
45
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
46
|
+
def App.sync_request ( channel, message=nil )
|
47
|
+
Nutella::Net.sync_request_to(channel, message, Nutella.app_id, nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Performs an asynchronous request at the application-level
|
52
|
+
#
|
53
|
+
# @param [String] channel the application-level channel we want to make the request to. *CANNOT* contain wildcard(s)!
|
54
|
+
# @param [Object] message the body of request. This can be,
|
55
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
56
|
+
# @param [Proc] callback the callback that is fired whenever a response is received. It takes one parameter (response).
|
57
|
+
def App.async_request ( channel, message=nil, callback )
|
58
|
+
Nutella::Net.async_request_to(channel, message, callback, Nutella.app_id, nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Handles requests on a certain application-level channel
|
63
|
+
#
|
64
|
+
# @param [String] channel tha application-level channel we want to listen for requests on. Can contain wildcard(s).
|
65
|
+
# @param [Proc] callback a lambda expression that is fired whenever a message is received.
|
66
|
+
# The passed callback takes the following parameters:
|
67
|
+
# - [String] the received message (payload). Messages that are not JSON are discarded.
|
68
|
+
# - [Hash] the sender's identifiers (run_id, app_id, component_id and optionally resource_id)
|
69
|
+
# - [*returns* Hash] The response sent back to the client that performed the request. Whatever is returned by the callback is marshaled into a JSON string and sent via MQTT.
|
70
|
+
def App.handle_requests( channel, callback )
|
71
|
+
Nutella::Net.handle_requests_on(channel, callback, Nutella.app_id, nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!endgroup
|
75
|
+
|
76
|
+
|
77
|
+
# @!group Application-level APIs to communicate at the run-level
|
78
|
+
|
79
|
+
# Allows application-level APIs to subscribe to a run-level channel within a specific run
|
80
|
+
#
|
81
|
+
# @param [String] run_id the specific run we are subscribing to
|
82
|
+
# @param [String] channel the run-level channel we are subscribing to. Can be wildcard.
|
83
|
+
# @param [Proc] callback the callback that is fired whenever a message is received on the channel.
|
84
|
+
# The passed callback takes the following parameters:
|
85
|
+
# - [String] message: the received message. Messages that are not JSON are discarded.
|
86
|
+
# - [String] channel: the application-level channel the message was received on (optional, only for wildcard subscriptions)
|
87
|
+
# - [Hash] from: the sender's identifiers (run_id, app_id, component_id and optionally resource_id)
|
88
|
+
def App.subscribe_to_run( run_id, channel, callback )
|
89
|
+
Nutella::Net.subscribe_to(channel, callback, Nutella.app_id, run_id)
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Allows application-level APIs to unsubscribe from a run-level channel within a specific run
|
94
|
+
#
|
95
|
+
# @param [String] run_id the specific run we are un-subscribing from
|
96
|
+
# @param [String] channel the run-level channel we want to unsubscribe from. Can contain wildcard(s).
|
97
|
+
def App.unsubscribe_to_run( run_id, channel )
|
98
|
+
Nutella::Net.unsubscribe_to(channel, Nutella.app_id, run_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Allows application-level APIs to publish to a run-level channel within a specific run
|
103
|
+
#
|
104
|
+
# @param [String] run_id the specific run we are publishing to
|
105
|
+
# @param [String] channel the run-level channel we want to publish the message to. *CANNOT* contain wildcard(s)!
|
106
|
+
# @param [String] message the message we are publishing. This can be,
|
107
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
108
|
+
def App.publish_to_run( run_id, channel, message )
|
109
|
+
Nutella::Net.publish_to(channel, message, Nutella.app_id, run_id)
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# Allows application-level APIs to make a synchronous request to a run-level channel within a specific run
|
114
|
+
#
|
115
|
+
# @param [String] run_id the specific run we are making the request to
|
116
|
+
# @param [String] channel the channel we want to make the request to. *CANNOT* contain wildcard(s)!
|
117
|
+
# @param [Object] request the body of request. This can be,
|
118
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
119
|
+
def App.sync_request_to_run( run_id, channel, request)
|
120
|
+
Nutella::Net.sync_request_to(channel, request, Nutella.app_id, run_id)
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# Allows application-level APIs to make an asynchronous request to a run-level channel within a specific run
|
125
|
+
#
|
126
|
+
# @param [String] run_id the specific run we are making the request to
|
127
|
+
# @param [String] channel the channel we want to make the request to. *CANNOT* contain wildcard(s)!
|
128
|
+
# @param [Object] request the body of request. This can be,
|
129
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
130
|
+
# @param [Proc] callback the callback that is fired whenever a response is received. It takes one parameter (response).
|
131
|
+
def App.async_request_to_run( run_id, channel, request, callback)
|
132
|
+
Nutella::Net.async_request_to(channel, request, callback, Nutella.app_id, run_id)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Allows application-level APIs to handle requests on a run-level channel within a specific run
|
137
|
+
#
|
138
|
+
# @param [String] run_id the specific run requests are coming from
|
139
|
+
# @param [String] channel we want to listen for requests on. Can contain wildcard(s).
|
140
|
+
# @param [Proc] callback a lambda expression that is fired whenever a message is received.
|
141
|
+
# The passed callback takes the following parameters:
|
142
|
+
# - [String] the received message (payload). Messages that are not JSON are discarded.
|
143
|
+
# - [Hash] the sender's identifiers (run_id, app_id, component_id and optionally resource_id)
|
144
|
+
# - [*returns* Hash] The response sent back to the client that performed the request. Whatever is returned by the callback is marshaled into a JSON string and sent via MQTT.
|
145
|
+
def App.handle_requests_on_run( run_id, channel, callback )
|
146
|
+
Nutella::Net.handle_requests_on(channel, callback, Nutella.app_id, run_id)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @!endgroup
|
150
|
+
|
151
|
+
|
152
|
+
# @!group Application level APIs to communicate at the run-level (broadcast)
|
153
|
+
|
154
|
+
# Allows application-level APIs to subscribe to a run-level channel *for ALL runs*
|
155
|
+
#
|
156
|
+
# @param [String] channel the run-level channel we are subscribing to. Can be wildcard.
|
157
|
+
# @param [Proc] callback the callback that is fired whenever a message is received on the channel.
|
158
|
+
# The passed callback takes the following parameters:
|
159
|
+
# - [String] message: the received message. Messages that are not JSON are discarded.
|
160
|
+
# - [String] run_id: the run_id of the channel the message was sent on
|
161
|
+
# - [Hash] from: the sender's identifiers (run_id, app_id, component_id and optionally resource_id)
|
162
|
+
def App.subscribe_to_all_runs( channel, callback )
|
163
|
+
# Check the passed callback has the right number of arguments
|
164
|
+
raise 'You need to pass a callback with 3 parameters (payload, run_id, from) when subscribing to all runs!' if callback.parameters.length!=3
|
165
|
+
# Pad channel
|
166
|
+
padded_channel = Nutella::Net.pad_channel(channel, Nutella.app_id, '+')
|
167
|
+
mqtt_cb = lambda do |mqtt_message, mqtt_channel|
|
168
|
+
begin
|
169
|
+
type, from, payload, _ = Nutella::Net.extract_fields_from_message mqtt_message
|
170
|
+
run_id = extract_run_id_from_ch(Nutella.app_id, mqtt_channel)
|
171
|
+
callback.call(payload, run_id, from) if type=='publish'
|
172
|
+
rescue JSON::ParserError
|
173
|
+
# Make sure the message is JSON, if not drop the message
|
174
|
+
return
|
175
|
+
rescue
|
176
|
+
# Check the passed callback has the right number of arguments
|
177
|
+
STDERR.puts "The callback you passed to subscribe has the #{$!}: it needs 'payload', 'run_id' and 'from'"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
# Add to subscriptions, save mqtt callback and subscribe
|
181
|
+
Nutella::Net.subscriptions.push padded_channel
|
182
|
+
Nutella::Net.callbacks.push mqtt_cb
|
183
|
+
Nutella.mqtt.subscribe( padded_channel, mqtt_cb )
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# Allows application-level APIs to unsubscribe from a run-level channel *for ALL runs*
|
188
|
+
#
|
189
|
+
# @param [String] channel the run-level channel we want to unsubscribe from. Can contain wildcard(s).
|
190
|
+
def App.unsubscribe_from_all_runs( channel )
|
191
|
+
Nutella::Net.unsubscribe_to(channel, Nutella.app_id, '+')
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
# Allows application-level APIs to publish a message to a run-level channel *for ALL runs*
|
196
|
+
#
|
197
|
+
# @param [String] channel the run-level channel we want to publish the message to. *CANNOT* contain wildcard(s)!
|
198
|
+
# @param [Object] message the message we are publishing. This can be,
|
199
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
200
|
+
def App.publish_to_all_runs( channel, message )
|
201
|
+
Nutella.app_runs_list.each do |run_id|
|
202
|
+
Nutella::Net.publish_to(channel, message, Nutella.app_id, run_id)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Allows application-level APIs to send a request to a run-level channel *for ALL runs*
|
208
|
+
#
|
209
|
+
# @param [String] channel the run-level channel we want to make the request to. *CANNOT* contain wildcard(s)!
|
210
|
+
# @param [Object] request the body of request. This can be,
|
211
|
+
# nil/empty (default), a string, a hash and, in general, anything with a .to_json method.
|
212
|
+
# @param [Proc] callback the callback that is fired whenever a response is received. It takes one parameter (response).
|
213
|
+
def App.async_request_to_all_runs(channel, request, callback)
|
214
|
+
Nutella.app_runs_list.each do |run_id|
|
215
|
+
Nutella::Net.async_request_to(channel, request, callback, Nutella.app_id, run_id)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
# Allows application-level APIs to handle requests to a run-level channel *for ALL runs*
|
221
|
+
#
|
222
|
+
# @param [String] channel tha run-level channel we want to listen for requests on. Can contain wildcard(s).
|
223
|
+
# @param [Proc] callback a lambda expression that is fired whenever a message is received.
|
224
|
+
# The passed callback takes the following parameters:
|
225
|
+
# - [String] the received message (request). Messages that are not JSON are discarded.
|
226
|
+
# - [String] run_id: the run_id of the channel the message was sent on
|
227
|
+
# - [Hash] the sender's identifiers (from containing, run_id, app_id, component_id and optionally resource_id)
|
228
|
+
# - [*returns* Hash] The response sent back to the client that performed the request. Whatever is returned by the callback is marshaled into a JSON string and sent via MQTT.
|
229
|
+
def App.handle_requests_on_all_runs(channel, callback)
|
230
|
+
# Check the passed callback has the right number of arguments
|
231
|
+
raise 'You need to pass a callback with 3 parameters (request, run_id, from) when handling requests!' if callback.parameters.length!=3
|
232
|
+
# Pad channel
|
233
|
+
padded_channel = Nutella::Net.pad_channel(channel, Nutella.app_id, '+')
|
234
|
+
mqtt_cb = lambda do |request, mqtt_channel|
|
235
|
+
begin
|
236
|
+
# Extract nutella fields
|
237
|
+
type, from, payload, id = Nutella::Net.extract_fields_from_message request
|
238
|
+
run_id = extract_run_id_from_ch(Nutella.app_id, mqtt_channel)
|
239
|
+
# Only handle requests that have proper id set
|
240
|
+
return if type!='request' || id.nil?
|
241
|
+
# Execute callback and send response
|
242
|
+
m = Net.prepare_message_for_response( callback.call( payload, run_id, from), id )
|
243
|
+
Nutella.mqtt.publish( mqtt_channel, m )
|
244
|
+
rescue JSON::ParserError
|
245
|
+
# Make sure that request contains JSON, if not drop the message
|
246
|
+
return
|
247
|
+
rescue
|
248
|
+
# Check the passed callback has the right number of arguments
|
249
|
+
STDERR.puts "The callback you passed to subscribe has the #{$!}: it needs 'request', 'run_id' and 'from'"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
# Subscribe to the channel
|
253
|
+
Nutella.mqtt.subscribe( padded_channel, mqtt_cb )
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
# @!endgroup
|
258
|
+
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def App.extract_run_id_from_ch( app_id, mqtt_channel )
|
263
|
+
head = "/nutella/apps/#{app_id}/runs/"
|
264
|
+
mqtt_channel.gsub(head, '').split('/')[0]
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
end
|
data/lib/nutella_lib/persist.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'pstore'
|
3
3
|
require 'fileutils'
|
4
|
+
require 'thread'
|
4
5
|
|
5
6
|
module Nutella
|
6
7
|
|
8
|
+
# Implements basic run-dependent persistence for nutella bots.
|
7
9
|
module Persist
|
8
10
|
|
9
11
|
# This module exposes a single method to retrieve a JSONStore
|
@@ -32,6 +34,11 @@ module Nutella
|
|
32
34
|
|
33
35
|
class JSONStore < PStore
|
34
36
|
|
37
|
+
def initialize(path)
|
38
|
+
super
|
39
|
+
@semaphore = Mutex.new
|
40
|
+
end
|
41
|
+
|
35
42
|
def dump(table)
|
36
43
|
table.to_json
|
37
44
|
end
|
@@ -40,6 +47,13 @@ module Nutella
|
|
40
47
|
JSON.parse(content)
|
41
48
|
end
|
42
49
|
|
50
|
+
def transaction(read_only=false, &block)
|
51
|
+
@semaphore.synchronize {
|
52
|
+
# access shared resource
|
53
|
+
super
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
43
57
|
# Dumps the whole store to hash
|
44
58
|
# example:
|
45
59
|
# store = JSONStore.new("my_file.json")
|
@@ -68,6 +82,4 @@ module Nutella
|
|
68
82
|
end
|
69
83
|
|
70
84
|
|
71
|
-
end
|
72
|
-
|
73
|
-
|
85
|
+
end
|
data/lib/nutella_lib.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'nutella_lib/core'
|
2
2
|
require 'nutella_lib/net'
|
3
|
+
require 'nutella_lib/net_app'
|
3
4
|
require 'nutella_lib/persist'
|
4
5
|
require 'simple_mqtt_client/simple_mqtt_client'
|
5
6
|
|
@@ -12,15 +13,3 @@ require 'set'
|
|
12
13
|
unless defined?(Nutella::NO_EXT)
|
13
14
|
require 'nutella_lib/ext/kernel'
|
14
15
|
end
|
15
|
-
|
16
|
-
# Adding a convenience method to the string class
|
17
|
-
# to test if it contains properly formatted JSON
|
18
|
-
class String
|
19
|
-
def is_json?
|
20
|
-
begin
|
21
|
-
!!JSON.parse(self)
|
22
|
-
rescue
|
23
|
-
false
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,35 +1,45 @@
|
|
1
1
|
require 'mqtt'
|
2
2
|
|
3
|
-
#
|
4
|
-
# @author Alessandro Gnoli <tebemis@gmail.com>
|
3
|
+
# A simple MQTT client
|
5
4
|
class SimpleMQTTClient
|
6
|
-
|
5
|
+
|
6
|
+
## Private instance variables
|
7
|
+
# `@channels` is a hash that associates to each channels a set of callbacks
|
8
|
+
# `@client` low-level mqtt client
|
9
|
+
# `@thread` the packet reader thread
|
10
|
+
|
11
|
+
|
7
12
|
# Creates a new MQTT client
|
8
13
|
# @param [String] host the hostname of the MQTT broker we are connecting to
|
9
14
|
# @param [String] client_id the **unique** client identifier
|
10
|
-
def initialize(host, client_id=nil)
|
11
|
-
@host = host
|
15
|
+
def initialize( host, client_id=nil )
|
12
16
|
@channels = Hash.new
|
13
17
|
@client = client_id.nil? ? MQTT::Client.connect(:host => host) : MQTT::Client.connect(host: host, client_id: client_id)
|
14
18
|
@thread = Thread.new('mqtt') do
|
15
19
|
@client.get do |channel, message|
|
20
|
+
# Execute all the appropriate callbacks:
|
21
|
+
# the ones specific to this channel with a single parameter (message)
|
22
|
+
# the ones associated to a wildcard channel, with two parameters (message and channel)
|
16
23
|
cbs = get_callbacks channel
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
24
|
+
begin
|
25
|
+
cbs.each { |cb| cb.parameters.length==1 ? cb.call(message) : cb.call(message, channel) }
|
26
|
+
rescue ArgumentError
|
27
|
+
STDERR.puts "The callback you passed for #{channel} has the #{$!}"
|
21
28
|
end
|
22
29
|
end
|
23
30
|
end
|
24
31
|
end
|
25
32
|
|
26
33
|
# Subscribes to a channel and registers a callback
|
27
|
-
# Single channel callbacks take only one parameter: the received message
|
28
|
-
# Wildcard callbacks take two parameters: the received message and the channel the message was sent to
|
34
|
+
# Single channel callbacks take only one parameter: the received message.
|
35
|
+
# Wildcard callbacks take two parameters: the received message and the channel the message was sent to.
|
36
|
+
# It is possible to register multiple callbacks per channel. All of them will be executed whenever a message
|
37
|
+
# on that channel is received.
|
38
|
+
# Note that overlaps between channel-specific callbacks and wildcard-filters are allowed.
|
29
39
|
# @param [String] channel the channel or filter we are subscribing to
|
30
40
|
# @param [Proc] callback the callback that gets called
|
31
41
|
# whenever a messages is received
|
32
|
-
def subscribe(channel, callback)
|
42
|
+
def subscribe( channel, callback )
|
33
43
|
if @channels.include?(channel)
|
34
44
|
@channels[channel] << callback
|
35
45
|
else
|
@@ -41,7 +51,7 @@ class SimpleMQTTClient
|
|
41
51
|
# Un-subscribes a specific callback from a channel
|
42
52
|
# @param [String] channel the channel we are un-subscribing from
|
43
53
|
# @param [Proc] callback the specific callback we want to remove
|
44
|
-
def unsubscribe(channel, callback)
|
54
|
+
def unsubscribe( channel, callback )
|
45
55
|
if @channels.include? channel
|
46
56
|
@channels[channel].delete(callback)
|
47
57
|
end
|
@@ -51,14 +61,29 @@ class SimpleMQTTClient
|
|
51
61
|
end
|
52
62
|
end
|
53
63
|
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# @
|
64
|
+
# Publishes a message to a channel
|
65
|
+
# @param [String] channel the channel we are publishing to
|
66
|
+
# @param [String] message the message we are publishing
|
67
|
+
def publish( channel, message )
|
68
|
+
# Check we are not publishing to a wildcard channel
|
69
|
+
STDERR.puts 'Can\'t publish to a wildcard channel!' if is_channel_wildcard? channel
|
70
|
+
@client.publish(channel, message)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Disconnects this simple MQTT client instance from the broker
|
74
|
+
def disconnect
|
75
|
+
@thread.exit
|
76
|
+
@client.disconnect
|
77
|
+
@channels.clear
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a hash of all the channels this client is currently subscribed to with relative callbacks
|
81
|
+
# @return [Hash] all channels this client is currently subscribed to, and relative callbacks
|
57
82
|
def get_subscribed_channels
|
58
83
|
@channels
|
59
84
|
end
|
60
85
|
|
61
|
-
# Returns true
|
86
|
+
# Returns true if a channel is a wildcard channel
|
62
87
|
# @return [Boolean] true if the channel is a wildcard channel. See MQTT specification for wildcard channels
|
63
88
|
# {http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106 here}
|
64
89
|
# @param [String] channel the channel we are testing for wildcard
|
@@ -66,53 +91,48 @@ class SimpleMQTTClient
|
|
66
91
|
channel.include?('#') || channel.include?('+')
|
67
92
|
end
|
68
93
|
|
69
|
-
|
70
|
-
# @param [String] channel the channel we are publishing to
|
71
|
-
# @param [String] message the message we are publishing
|
72
|
-
def publish(channel, message)
|
73
|
-
@client.publish(channel, message)
|
74
|
-
end
|
94
|
+
private
|
75
95
|
|
76
|
-
#
|
77
|
-
def
|
78
|
-
|
96
|
+
# Gets all the callbacks associated to a channel
|
97
|
+
def get_callbacks( channel )
|
98
|
+
cbs = Array.new
|
99
|
+
# First, fetch all the channel-specific callbacks...
|
100
|
+
cbs.concat @channels[channel] if @channels.has_key? channel
|
101
|
+
# ...then fetch the callbacks matching all wildcard-filters
|
102
|
+
cbs.concat wildcard_callbacks channel
|
103
|
+
cbs
|
79
104
|
end
|
80
105
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
#
|
87
|
-
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
nil
|
106
|
+
# Gets all wildcard callbacks associated to a channel
|
107
|
+
# Among the filters we are subscribed to, which ones match the channel?
|
108
|
+
def wildcard_callbacks( channel )
|
109
|
+
# First select all filters
|
110
|
+
filters = @channels.keys.select { |ch| is_channel_wildcard? ch}
|
111
|
+
# Then select filters that match channel
|
112
|
+
matching_filters = filters.select { |filter| matches_wildcard_pattern(channel, filter) }
|
113
|
+
# Add all callbacks that are associated to matching filters
|
114
|
+
cbs = Array.new
|
115
|
+
matching_filters.each { |ch| cbs.concat @channels[ch] }
|
116
|
+
cbs
|
93
117
|
end
|
94
118
|
|
95
|
-
# Returns
|
96
|
-
#
|
97
|
-
#
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
nil
|
119
|
+
# Returns true if the string matches a pattern
|
120
|
+
# See http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718107
|
121
|
+
# for a formal description of the rules
|
122
|
+
def matches_wildcard_pattern(str, pattern)
|
123
|
+
# First we need to build a regex out of the pattern
|
124
|
+
regex = build_regex_from_pattern pattern
|
125
|
+
# Then we check if the regex matches the string
|
126
|
+
!!(regex =~ str)
|
104
127
|
end
|
105
128
|
|
106
|
-
#
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
return true if pattern[-1, 1]=='#' && p_wo_wildcard==str_wo_details
|
114
|
-
# TODO Handle single-level wildcards (+)
|
115
|
-
false
|
129
|
+
# Escape '/'
|
130
|
+
# Substitute '+' for '[^"\/"]+' (a string of one or more characters that is are not '/')
|
131
|
+
# Substitute '/#' with '.*' (a string of zero or more characters)
|
132
|
+
# Substitute '#' for '.*' (a string of zero or more characters)
|
133
|
+
def build_regex_from_pattern( pattern )
|
134
|
+
regex_str = pattern.gsub('/','\\/').gsub('+','[^"\/"]+').gsub('\/#','.*').gsub('#','.*')
|
135
|
+
Regexp.new regex_str
|
116
136
|
end
|
117
137
|
|
118
138
|
end
|
data/nutella_lib.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: nutella_lib 0.
|
5
|
+
# stub: nutella_lib 0.4.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "nutella_lib"
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.4.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Alessandro Gnoli"]
|
14
|
-
s.date = "2015-
|
14
|
+
s.date = "2015-03-13"
|
15
15
|
s.description = "Implements the nutella protocol and exposes it natively to ruby developers"
|
16
16
|
s.email = "tebemis@gmail.com"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -30,17 +30,19 @@ Gem::Specification.new do |s|
|
|
30
30
|
"lib/nutella_lib/core.rb",
|
31
31
|
"lib/nutella_lib/ext/kernel.rb",
|
32
32
|
"lib/nutella_lib/net.rb",
|
33
|
+
"lib/nutella_lib/net_app.rb",
|
33
34
|
"lib/nutella_lib/noext.rb",
|
34
35
|
"lib/nutella_lib/persist.rb",
|
35
36
|
"lib/simple_mqtt_client/simple_mqtt_client.rb",
|
36
37
|
"nutella_lib.gemspec",
|
37
38
|
"test/helper.rb",
|
38
|
-
"test/
|
39
|
+
"test/test_nutella_net.rb",
|
40
|
+
"test/test_nutella_net_app.rb",
|
39
41
|
"test/test_simple_mqtt_client.rb"
|
40
42
|
]
|
41
43
|
s.homepage = "https://github.com/nutella-framework/nutella_lib.rb"
|
42
44
|
s.licenses = ["MIT"]
|
43
|
-
s.rubygems_version = "2.
|
45
|
+
s.rubygems_version = "2.4.3"
|
44
46
|
s.summary = "nutella protocol library for ruby"
|
45
47
|
|
46
48
|
if s.respond_to? :specification_version then
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestNutellaNet < MiniTest::Test
|
4
|
+
|
5
|
+
|
6
|
+
# def test_send_receive
|
7
|
+
# nutella.init('localhost', 'my_app_id', 'my_run_id' , 'my_component_id')
|
8
|
+
# cb_executed = false
|
9
|
+
# cb = lambda do |message, from|
|
10
|
+
# cb_executed = true
|
11
|
+
# puts "Received message from #{from['component_id']}/#{from['resource_id']}. Message: #{message}"
|
12
|
+
# end
|
13
|
+
# nutella.net.subscribe('demo0', cb)
|
14
|
+
# sleep 1
|
15
|
+
# nutella.net.publish('demo0', 'test_message')
|
16
|
+
# # Make sure we wait for the message to be delivered
|
17
|
+
# sleep 1
|
18
|
+
# assert cb_executed
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
#
|
22
|
+
# def test_send_receive_wildcard
|
23
|
+
# cb_executed = false
|
24
|
+
# nutella.init('localhost', 'my_app_id', 'my_run_id' , 'my_component_id')
|
25
|
+
# nutella.set_resource_id 'my_resource_id_1'
|
26
|
+
# cb = lambda do |message, channel, from|
|
27
|
+
# cb_executed = true
|
28
|
+
# puts "Received message on #{channel} from #{from['component_id']}/#{from['resource_id']}. Message: #{message}"
|
29
|
+
# end
|
30
|
+
# nutella.net.subscribe('demo1/#', cb)
|
31
|
+
# sleep 1
|
32
|
+
# nutella.net.publish('demo1/demo', 'test_message')
|
33
|
+
# # Make sure we wait for the message to be delivered
|
34
|
+
# sleep 1
|
35
|
+
# assert cb_executed
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
#
|
39
|
+
# def test_multiple_subscriptions
|
40
|
+
# nutella.init('localhost', 'my_app_id', 'my_run_id' , 'my_component_id')
|
41
|
+
# nutella.set_resource_id 'my_resource_id_2'
|
42
|
+
# cb = lambda do |message, from|
|
43
|
+
# puts "Received message #{from['component_id']}/#{from['resource_id']}. Message: #{message}"
|
44
|
+
# end
|
45
|
+
# assert_raises RuntimeError do
|
46
|
+
# nutella.net.subscribe('demo2', cb)
|
47
|
+
# nutella.net.subscribe('demo2', cb)
|
48
|
+
# end
|
49
|
+
# nutella.net.unsubscribe('demo2')
|
50
|
+
# nutella.net.subscribe('demo2', cb)
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
#
|
54
|
+
# def test_request_response
|
55
|
+
# nutella.init('localhost', 'my_app_id', 'my_run_id' , 'my_component_id')
|
56
|
+
# nutella.set_resource_id 'my_resource_id_3'
|
57
|
+
#
|
58
|
+
# nutella.net.subscribe('demo3', lambda do |message, from|
|
59
|
+
# puts "Received a message from #{from['component_id']}/#{from['resource_id']}. Message: #{message}"
|
60
|
+
# end)
|
61
|
+
#
|
62
|
+
# nutella.net.handle_requests( 'demo3', lambda do |message, from|
|
63
|
+
# puts "We received a request: message #{message}, from #{from['component_id']}/#{from['resource_id']}."
|
64
|
+
# #Then we are going to return some random JSON
|
65
|
+
# {my:'json'}
|
66
|
+
# end)
|
67
|
+
#
|
68
|
+
# response = nutella.net.sync_request( 'demo3', 'my request is a string' )
|
69
|
+
# assert_equal({'my' => 'json'}, response)
|
70
|
+
#
|
71
|
+
# nutella.net.async_request( 'demo3', 'my request is a string', lambda do |response|
|
72
|
+
# assert_equal({'my' => 'json'}, response)
|
73
|
+
# end)
|
74
|
+
#
|
75
|
+
# sleep(2)
|
76
|
+
# end
|
77
|
+
|
78
|
+
|
79
|
+
end
|