nutella_lib 0.3.1 → 0.4.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.
- 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
|