spire_io 1.0.0.alpha.2
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/lib/requestable.rb +70 -0
- data/lib/spire_io.rb +681 -0
- metadata +135 -0
data/lib/requestable.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Requestable
|
2
|
+
|
3
|
+
def self.included(mod)
|
4
|
+
mod.module_eval do
|
5
|
+
extend(ClassMethods)
|
6
|
+
include(InstanceMethods)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def requests
|
12
|
+
@requests ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def define_request(name, &block)
|
16
|
+
requests[name] = block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module InstanceMethods
|
21
|
+
def prepare_request(name, *args)
|
22
|
+
if block = self.class.requests[name]
|
23
|
+
options = self.instance_exec(*args, &block)
|
24
|
+
Request.new(@client, options)
|
25
|
+
else
|
26
|
+
raise ArgumentError, "No request has been defined for #{name.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def request(name, *args)
|
31
|
+
prepare_request(name, *args).exec
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Request
|
36
|
+
attr_accessor :url
|
37
|
+
def initialize(client, options)
|
38
|
+
@client = client
|
39
|
+
@method = options.delete(:method)
|
40
|
+
@url = options.delete(:url)
|
41
|
+
@options = options
|
42
|
+
@options[:headers] = {
|
43
|
+
"User-Agent" => "Ruby spire.io client"
|
44
|
+
}.merge(@options[:headers])
|
45
|
+
end
|
46
|
+
|
47
|
+
def headers
|
48
|
+
@options[:headers]
|
49
|
+
end
|
50
|
+
|
51
|
+
def body
|
52
|
+
@options[:body]
|
53
|
+
end
|
54
|
+
|
55
|
+
def body=(val)
|
56
|
+
@options[:body] = val
|
57
|
+
end
|
58
|
+
|
59
|
+
def query
|
60
|
+
@options[:query]
|
61
|
+
end
|
62
|
+
|
63
|
+
def exec
|
64
|
+
@client.send(@method, @url, @options)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
data/lib/spire_io.rb
ADDED
@@ -0,0 +1,681 @@
|
|
1
|
+
gem "excon"
|
2
|
+
require "excon"
|
3
|
+
gem "json"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
require "requestable"
|
7
|
+
|
8
|
+
class Spire
|
9
|
+
|
10
|
+
#How many times we will try to create a channel or subscription after getting a 409
|
11
|
+
RETRY_CREATION_LIMIT = 3
|
12
|
+
|
13
|
+
include Requestable
|
14
|
+
|
15
|
+
define_request(:discover) do
|
16
|
+
{
|
17
|
+
:method => :get,
|
18
|
+
:url => @url,
|
19
|
+
:headers => {"Accept" => "application/json"}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
define_request(:start) do |key|
|
24
|
+
{
|
25
|
+
:method => :post,
|
26
|
+
:url => @description["resources"]["sessions"]["url"],
|
27
|
+
:body => {:key => key}.to_json,
|
28
|
+
:headers => {
|
29
|
+
"Accept" => mediaType("session"),
|
30
|
+
"Content-Type" => mediaType("account")
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
define_request(:login) do |email, password|
|
36
|
+
{
|
37
|
+
:method => :post,
|
38
|
+
:url => @description["resources"]["sessions"]["url"],
|
39
|
+
:body => { :email => email, :password => password }.to_json,
|
40
|
+
:headers => {
|
41
|
+
"Accept" => mediaType("session"),
|
42
|
+
"Content-Type" => mediaType("account")
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
define_request(:register) do |info|
|
48
|
+
{
|
49
|
+
:method => :post,
|
50
|
+
:url => @description["resources"]["accounts"]["url"],
|
51
|
+
:body => {
|
52
|
+
:email => info[:email],
|
53
|
+
:password => info[:password],
|
54
|
+
:password_confirmation => info[:password_confirmation]
|
55
|
+
}.to_json,
|
56
|
+
:headers => {
|
57
|
+
"Accept" => mediaType("session"),
|
58
|
+
"Content-Type" => mediaType("account")
|
59
|
+
}
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
define_request(:session) do
|
64
|
+
{
|
65
|
+
:method => :get,
|
66
|
+
:url => @session["url"],
|
67
|
+
:headers => {
|
68
|
+
"Accept" => mediaType("session"),
|
69
|
+
"Authorization" => "Capability #{@session["capability"]}"
|
70
|
+
}
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
define_request(:password_reset) do |email|
|
75
|
+
{
|
76
|
+
:method => :post,
|
77
|
+
:url => @description["resources"]["accounts"]["url"],
|
78
|
+
:body => ""
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
define_request(:delete_account) do
|
83
|
+
{
|
84
|
+
:method => :delete,
|
85
|
+
:url => @resources["account"]["url"],
|
86
|
+
:headers => {
|
87
|
+
"Accept" => mediaType("account"),"Content-Type" => mediaType("account"),
|
88
|
+
"Authorization" =>
|
89
|
+
"Capability #{@resources["account"]["capability"]}"
|
90
|
+
}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
define_request(:update_account) do |info|
|
95
|
+
{
|
96
|
+
:method => :put,
|
97
|
+
:url => @resources["account"]["url"],
|
98
|
+
:body => info.to_json,
|
99
|
+
:headers => {
|
100
|
+
"Accept" => mediaType("account"),"Content-Type" => mediaType("account"),
|
101
|
+
"Authorization" => "Capability #{@resources["account"]["capability"]}"
|
102
|
+
}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
define_request(:create_channel) do |name|
|
107
|
+
{
|
108
|
+
:method => :post,
|
109
|
+
:url => @resources["channels"]["url"],
|
110
|
+
:body => { :name => name }.to_json,
|
111
|
+
:headers => {
|
112
|
+
"Authorization" =>
|
113
|
+
"Capability #{@resources["channels"]["capability"]}",
|
114
|
+
"Accept" => mediaType("channel"),
|
115
|
+
"Content-Type" => mediaType("channel")
|
116
|
+
}
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
define_request(:channels) do
|
121
|
+
{
|
122
|
+
:method => :get,
|
123
|
+
:url => @resources["channels"]["url"],
|
124
|
+
:headers => {
|
125
|
+
"Authorization" =>
|
126
|
+
"Capability #{@resources["channels"]["capability"]}",
|
127
|
+
"Accept" => mediaType("channels"),
|
128
|
+
}
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
define_request(:subscribe) do |subscription_name, channels|
|
133
|
+
{
|
134
|
+
:method => :post,
|
135
|
+
:url => @resources["subscriptions"]["url"],
|
136
|
+
:body => {
|
137
|
+
:channels => channels.flatten.map { |name| self[name].url },
|
138
|
+
:name => subscription_name
|
139
|
+
}.to_json,
|
140
|
+
:headers => {
|
141
|
+
"Authorization" => "Capability #{@resources["subscriptions"]["capability"]}",
|
142
|
+
"Accept" => mediaType("subscription"),
|
143
|
+
"Content-Type" => mediaType("subscription")
|
144
|
+
}
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
define_request(:billing) do
|
149
|
+
{
|
150
|
+
:method => :get,
|
151
|
+
:url => @description["resources"]["billing"]["url"],
|
152
|
+
:headers => {
|
153
|
+
"Accept" => "application/json"
|
154
|
+
}
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
define_request(:billing_subscription) do |info|
|
159
|
+
{
|
160
|
+
:method => :put,
|
161
|
+
:url => @resources["account"]["billing"]["url"],
|
162
|
+
:body => info.to_json,
|
163
|
+
:headers => {
|
164
|
+
"Accept" => mediaType("account"),"Content-Type" => mediaType("account"),
|
165
|
+
"Authorization" => "Capability #{@resources["account"]["billing"]["capability"]}"
|
166
|
+
}
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
define_request(:billing_invoices) do
|
171
|
+
{
|
172
|
+
:method => :get,
|
173
|
+
:url => @resources["account"]["billing"]["invoices"]["url"],
|
174
|
+
:headers => {
|
175
|
+
"Accept" => "application/json",
|
176
|
+
"Authorization" => "Capability #{@resources["account"]["billing"]["invoices"]["capability"]}"
|
177
|
+
}
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
define_request(:billing_invoices_upcoming) do
|
182
|
+
{
|
183
|
+
:method => :get,
|
184
|
+
:url => @resources["account"]["billing"]["invoices"]["upcoming"]["url"],
|
185
|
+
:headers => {
|
186
|
+
"Accept" => "application/json",
|
187
|
+
"Authorization" => "Capability #{@resources["account"]["billing"]["invoices"]["upcoming"]["capability"]}"
|
188
|
+
}
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
attr_accessor :client, :channels, :session, :resources
|
193
|
+
|
194
|
+
def initialize(url="https://api.spire.io")
|
195
|
+
@client = Excon
|
196
|
+
@url = url
|
197
|
+
@channels = {}
|
198
|
+
@subscriptions = {}
|
199
|
+
@channel_error_counts = {}
|
200
|
+
@subscription_error_counts = {}
|
201
|
+
# @headers = { "User-Agent" => "Ruby spire.io client" }
|
202
|
+
# @timeout = 1
|
203
|
+
discover
|
204
|
+
end
|
205
|
+
|
206
|
+
def key
|
207
|
+
@resources["account"]["key"]
|
208
|
+
end
|
209
|
+
|
210
|
+
def mediaType(name)
|
211
|
+
@description["schema"]["1.0"][name]["mediaType"]
|
212
|
+
end
|
213
|
+
|
214
|
+
def discover
|
215
|
+
response = request(:discover)
|
216
|
+
raise "Error during discovery: #{response.status}" if response.status != 200
|
217
|
+
@description = JSON.parse(response.body)
|
218
|
+
#pp @description["schema"]["1.0"]
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
def start(key)
|
223
|
+
response = request(:start, key)
|
224
|
+
raise "Error starting a key-based session" if response.status != 201
|
225
|
+
cache_session(JSON.parse(response.body))
|
226
|
+
self
|
227
|
+
end
|
228
|
+
|
229
|
+
# Authenticates a session using a login and password
|
230
|
+
def login(login, password)
|
231
|
+
response = request(:login, login, password)
|
232
|
+
raise "Error attemping to login: (#{response.status}) #{response.body}" if response.status != 201
|
233
|
+
cache_session(JSON.parse(response.body))
|
234
|
+
self
|
235
|
+
end
|
236
|
+
|
237
|
+
# Register for a new spire account, and authenticates as the newly created account
|
238
|
+
# @param [String] :email Email address of new account
|
239
|
+
# @param [String] :password Password of new account
|
240
|
+
# @param [String] :password_confirmation Password confirmation (optional)
|
241
|
+
def register(info)
|
242
|
+
response = request(:register, info)
|
243
|
+
raise "Error attempting to register: (#{response.status}) #{response.body}" if response.status != 201
|
244
|
+
cache_session(JSON.parse(response.body))
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
def password_reset_request(email)
|
249
|
+
response = request(:password_reset)
|
250
|
+
unless response.status == 202
|
251
|
+
raise "Error requesting password reset: (#{response.status}) #{response.body}"
|
252
|
+
end
|
253
|
+
response
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
# Deletes the currently authenticated account
|
258
|
+
def delete_account
|
259
|
+
request(:delete_account)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Updates the current account with the new account information
|
263
|
+
# See Spire docs for available settings
|
264
|
+
def update(info)
|
265
|
+
response = request(:update_account, info)
|
266
|
+
raise "Error attempting to update account: (#{response.status}) #{response.body}" if response.status != 200
|
267
|
+
@resources["account"] = JSON.parse(response.body)
|
268
|
+
self
|
269
|
+
end
|
270
|
+
|
271
|
+
def retrieve_session
|
272
|
+
response = request(:session)
|
273
|
+
cache_session(JSON.parse(response.body))
|
274
|
+
raise "Error reloading session: #{response.status}" if response.status != 200
|
275
|
+
self
|
276
|
+
end
|
277
|
+
|
278
|
+
def cache_session(data)
|
279
|
+
@session = data
|
280
|
+
@resources = @session["resources"]
|
281
|
+
retrieve_channels
|
282
|
+
end
|
283
|
+
|
284
|
+
def retrieve_channels
|
285
|
+
response = request(:channels)
|
286
|
+
unless response.status == 200
|
287
|
+
raise "Error retrieving channels: (#{response.status}) #{response.body}"
|
288
|
+
end
|
289
|
+
cache_channels(JSON.parse(response.body))
|
290
|
+
end
|
291
|
+
|
292
|
+
def cache_channels(data)
|
293
|
+
@channels = {}
|
294
|
+
data.each do |name, properties|
|
295
|
+
@channels[name] = Channel.new(self, properties)
|
296
|
+
cache_channel_subscriptions(properties["subscriptions"])
|
297
|
+
end
|
298
|
+
@channels
|
299
|
+
end
|
300
|
+
|
301
|
+
def cache_channel_subscriptions(data)
|
302
|
+
data.each do |name, properties|
|
303
|
+
@subscriptions[name] = Subscription.new(self, properties)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Returns a channel object for the named channel
|
308
|
+
# @param [String] name Name of channel returned
|
309
|
+
# @return [Channel]
|
310
|
+
def [](name)
|
311
|
+
return @channels[name] if @channels[name]
|
312
|
+
create_channel(name)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Creates a channel on spire. Returns a Channel object. Note that this will
|
316
|
+
# fail with a 409 if a channel with the same name exists.
|
317
|
+
def create_channel(name)
|
318
|
+
@channel_error_counts[name] ||= 0
|
319
|
+
response = request(:create_channel, name)
|
320
|
+
return find_existing_channel(name) if response.status == 409 and @channel_error_counts[name] < RETRY_CREATION_LIMIT
|
321
|
+
if !(response.status == 201 || response.status == 200)
|
322
|
+
raise "Error creating or accessing a channel: (#{response.status}) #{response.body}"
|
323
|
+
end
|
324
|
+
new_channel = Channel.new(self,JSON.parse(response.body))
|
325
|
+
@channels[name] = new_channel
|
326
|
+
new_channel
|
327
|
+
end
|
328
|
+
|
329
|
+
def find_existing_channel(name)
|
330
|
+
@channel_error_counts[name] += 1
|
331
|
+
retrieve_session
|
332
|
+
self[name]
|
333
|
+
end
|
334
|
+
|
335
|
+
# Returns a subscription object for the given channels
|
336
|
+
# @param [String] subscription_name Name for the subscription
|
337
|
+
# @param [String] channels One or more channel names for the subscription to listen on
|
338
|
+
# @return [Subscription]
|
339
|
+
def subscribe(subscription_name, *channels)
|
340
|
+
@subscription_error_counts[subscription_name] ||= 0
|
341
|
+
return @subscriptions[subscription_name] if subscription_name and @subscriptions[subscription_name]
|
342
|
+
response = request(:subscribe, subscription_name, channels)
|
343
|
+
return find_existing_subscription(subscription_name, channels) if response.status == 409 and
|
344
|
+
@subscription_error_counts[subscription_name] < RETRY_CREATION_LIMIT
|
345
|
+
raise "Error creating a subscription: (#{response.status}) #{response.body}" if !(response.status == 201 || response.status == 200)
|
346
|
+
s = Subscription.new(self,JSON.parse(response.body))
|
347
|
+
@subscriptions[s.name] = s
|
348
|
+
s
|
349
|
+
end
|
350
|
+
alias :subscription :subscribe #For compatibility with other clients
|
351
|
+
|
352
|
+
def find_existing_subscription(name, channels)
|
353
|
+
@subscription_error_counts[name] += 1
|
354
|
+
retrieve_session
|
355
|
+
self.subscribe(name, *channels)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns a billing object than contains a list of all the plans available
|
359
|
+
# @param [String] info optional object description
|
360
|
+
# @return [Billing]
|
361
|
+
def billing(info=nil)
|
362
|
+
response = request(:billing)
|
363
|
+
raise "Error getting billing plans: #{response.status}" if response.status != 200
|
364
|
+
Billing.new(self,JSON.parse(response.body))
|
365
|
+
end
|
366
|
+
|
367
|
+
# Updates and subscribe the account to a billing plan
|
368
|
+
# @param [Object] info data containing billing description
|
369
|
+
# @return [Account]
|
370
|
+
def billing_subscription(info)
|
371
|
+
response = request(:billing_subscription)
|
372
|
+
raise "Error attempting to update account billing: (#{response.status}) #{response.body}" if response.status != 200
|
373
|
+
@resources["account"] = JSON.parse(response.body)
|
374
|
+
self
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
# Object representing a Spire channel
|
379
|
+
#
|
380
|
+
# You can get a channel object by calling [] on a Spire object
|
381
|
+
# * spire = Spire.new
|
382
|
+
# * spire.start("your api key")
|
383
|
+
# * channel = spire["channel name"]
|
384
|
+
class Channel
|
385
|
+
include Requestable
|
386
|
+
|
387
|
+
define_request(:publish) do |body|
|
388
|
+
{
|
389
|
+
:method => :post,
|
390
|
+
:url => url,
|
391
|
+
:body => body,
|
392
|
+
:headers => {
|
393
|
+
"Authorization" => "Capability #{@properties["capability"]}",
|
394
|
+
"Accept" => mediaType("message"),
|
395
|
+
"Content-Type" => mediaType("message")
|
396
|
+
}
|
397
|
+
}
|
398
|
+
end
|
399
|
+
|
400
|
+
define_request(:delete) do
|
401
|
+
{
|
402
|
+
:method => :delete,
|
403
|
+
:url => url,
|
404
|
+
:headers => {
|
405
|
+
"Authorization" => "Capability #{capability}"
|
406
|
+
}
|
407
|
+
}
|
408
|
+
end
|
409
|
+
|
410
|
+
def initialize(spire, properties)
|
411
|
+
@spire = spire
|
412
|
+
@client = spire.client
|
413
|
+
@properties = properties
|
414
|
+
end
|
415
|
+
|
416
|
+
def url
|
417
|
+
@properties["url"]
|
418
|
+
end
|
419
|
+
|
420
|
+
def key
|
421
|
+
@properties["key"]
|
422
|
+
end
|
423
|
+
|
424
|
+
def name
|
425
|
+
@properties["name"]
|
426
|
+
end
|
427
|
+
|
428
|
+
def capability
|
429
|
+
@properties["capability"]
|
430
|
+
end
|
431
|
+
|
432
|
+
def delete
|
433
|
+
response = request(:delete)
|
434
|
+
raise "Error deleting a channel" if response.status != 204
|
435
|
+
end
|
436
|
+
|
437
|
+
# Obtain a subscription for the channel
|
438
|
+
# @param [String] subscription_name Name of the subscription
|
439
|
+
# @return [Subscription]
|
440
|
+
def subscribe(subscription_name = nil)
|
441
|
+
@spire.subscribe(subscription_name, self.name)
|
442
|
+
end
|
443
|
+
|
444
|
+
#Publishes a message to the channel
|
445
|
+
# @param [String] message Message to be posted
|
446
|
+
# @return [Hash] response from the server
|
447
|
+
def publish(message)
|
448
|
+
response = request(:publish, {:content => message}.to_json)
|
449
|
+
raise "Error publishing a message: (#{response.status}) #{response.body}" if response.status != 201
|
450
|
+
JSON.parse(response.body)
|
451
|
+
end
|
452
|
+
|
453
|
+
def mediaType(name)
|
454
|
+
@spire.mediaType(name)
|
455
|
+
end
|
456
|
+
|
457
|
+
end
|
458
|
+
|
459
|
+
# The subscription class represents a read connection to a Spire channel
|
460
|
+
#
|
461
|
+
# You can get a subscription by calling subscribe on a spire object with the name of the channel or
|
462
|
+
# by calling subscribe on a channel object
|
463
|
+
#
|
464
|
+
# * spire = Spire.new
|
465
|
+
# * spire.start("your api key")
|
466
|
+
# *THEN*
|
467
|
+
# * subscription = spire.subscribe("subscription name", "channel name")
|
468
|
+
# *OR*
|
469
|
+
# * channel = spire["channel name"]
|
470
|
+
# * subscription = channel.subscribe("subscription name")
|
471
|
+
class Subscription
|
472
|
+
include Requestable
|
473
|
+
|
474
|
+
define_request(:listen) do |options|
|
475
|
+
timeout = options[:timeout]||30
|
476
|
+
delay = options[:delay]||0
|
477
|
+
order_by = options[:order_by]||'desc'
|
478
|
+
{
|
479
|
+
:method => :get,
|
480
|
+
:url => @properties["url"],
|
481
|
+
:query => {
|
482
|
+
"timeout" => timeout,
|
483
|
+
"last-message" => @last||'0',
|
484
|
+
"order-by" => order_by,
|
485
|
+
"delay" => delay
|
486
|
+
},
|
487
|
+
:headers => {
|
488
|
+
"Authorization" => "Capability #{@properties["capability"]}",
|
489
|
+
"Accept" => mediaType("events")
|
490
|
+
}
|
491
|
+
}
|
492
|
+
end
|
493
|
+
|
494
|
+
define_request(:delete) do
|
495
|
+
{
|
496
|
+
:method => :delete,
|
497
|
+
:url => url,
|
498
|
+
:headers => {
|
499
|
+
"Authorization" => "Capability #{capability}"
|
500
|
+
}
|
501
|
+
}
|
502
|
+
end
|
503
|
+
|
504
|
+
attr_accessor :messages, :last
|
505
|
+
|
506
|
+
def initialize(spire,properties)
|
507
|
+
@spire = spire
|
508
|
+
@client = spire.client
|
509
|
+
@properties = properties
|
510
|
+
@messages = []
|
511
|
+
@listening_thread = nil
|
512
|
+
@listeners = {}
|
513
|
+
@listening_threads = {}
|
514
|
+
@listener_mutex = Mutex.new
|
515
|
+
@listener_thread_mutex = Mutex.new
|
516
|
+
end
|
517
|
+
|
518
|
+
def key
|
519
|
+
@properties["key"]
|
520
|
+
end
|
521
|
+
|
522
|
+
def name
|
523
|
+
@properties["name"]
|
524
|
+
end
|
525
|
+
|
526
|
+
def capability
|
527
|
+
@properties["capability"]
|
528
|
+
end
|
529
|
+
|
530
|
+
def url
|
531
|
+
@properties["url"]
|
532
|
+
end
|
533
|
+
|
534
|
+
def delete
|
535
|
+
response = request(:delete)
|
536
|
+
raise "Error deleting a subscription" if response.status != 204
|
537
|
+
end
|
538
|
+
|
539
|
+
# Adds a listener (ruby block) to be called each time a message is received on the channel
|
540
|
+
#
|
541
|
+
# You must call #start_listening to actually start listening for messages
|
542
|
+
# @note Listeners are executed in their own thread, so practice proper thread safety!
|
543
|
+
# @param [String] name Name for the listener. One will be generated if not provided
|
544
|
+
# @return [String] Name of the listener
|
545
|
+
def add_listener(listener_name = nil, &block)
|
546
|
+
@listener_mutex.synchronize do
|
547
|
+
while !listener_name
|
548
|
+
new_name = "Listener-#{rand(9999999)}"
|
549
|
+
listener_name = new_name unless @listeners.has_key?(new_name)
|
550
|
+
end
|
551
|
+
@listeners[listener_name] = block
|
552
|
+
end
|
553
|
+
listener_name
|
554
|
+
end
|
555
|
+
|
556
|
+
# Removes a listener by name
|
557
|
+
#
|
558
|
+
# @param [String] name Name of the listener to remove
|
559
|
+
# @param [Boolean] kill_current_threads Kill any currently running threads of the removed listener
|
560
|
+
# @return [Proc] Listener that was removed
|
561
|
+
def remove_listener(name, kill_current_threads = true)
|
562
|
+
l = nil #scope
|
563
|
+
@listener_mutex.synchronize do
|
564
|
+
l = @listeners.delete(name)
|
565
|
+
end
|
566
|
+
kill_listening_threads(name) if kill_current_threads
|
567
|
+
l
|
568
|
+
end
|
569
|
+
|
570
|
+
# Removes all current listeners
|
571
|
+
# @param [Boolean] kill_current_threads Kill any currently running threads of the removed listener.
|
572
|
+
def remove_all_listeners(kill_current_threads = true)
|
573
|
+
@listener_mutex.synchronize do
|
574
|
+
@listeners = {}
|
575
|
+
end
|
576
|
+
kill_listening_threads if kill_current_threads
|
577
|
+
true
|
578
|
+
end
|
579
|
+
|
580
|
+
# Starts the listening thread. This must be called to enable any listeners you have added.
|
581
|
+
#
|
582
|
+
# You can continue to add more listeners after starting the listening process
|
583
|
+
# @note Will raise an exception if listening has already been started
|
584
|
+
def start_listening
|
585
|
+
raise "Already listening" if @listening_thread
|
586
|
+
@listening_thread = Thread.new {
|
587
|
+
while true
|
588
|
+
new_messages = self.listen
|
589
|
+
next unless new_messages.size > 0
|
590
|
+
current_listeners.each do |name, listener|
|
591
|
+
new_messages.each do |m|
|
592
|
+
thread = Thread.new {
|
593
|
+
begin
|
594
|
+
listener.call(m)
|
595
|
+
rescue
|
596
|
+
puts "Error while running listener #{name}: #{$!.inspect}"
|
597
|
+
puts $!.backtrace.join("\n")
|
598
|
+
end
|
599
|
+
}
|
600
|
+
@listener_thread_mutex.synchronize do
|
601
|
+
@listening_threads[name] ||= []
|
602
|
+
@listening_threads[name] << thread
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
}
|
608
|
+
end
|
609
|
+
|
610
|
+
# Stops the listening process
|
611
|
+
# @param [Boolean] kill_current_threads Kills any currently running listener threads
|
612
|
+
def stop_listening(kill_current_threads = true)
|
613
|
+
@listener_thread_mutex.synchronize do
|
614
|
+
@listening_thread.kill if @listening_thread
|
615
|
+
@listening_thread = nil
|
616
|
+
end
|
617
|
+
kill_listening_threads if kill_current_threads
|
618
|
+
end
|
619
|
+
|
620
|
+
# Kills any currently executing listeners
|
621
|
+
# @param [String] name_to_kill Kill only currently executing listeners that have this name
|
622
|
+
def kill_listening_threads(name_to_kill = nil)
|
623
|
+
@listener_thread_mutex.synchronize do
|
624
|
+
@listening_threads.each do |name, threads|
|
625
|
+
next if name_to_kill and name_to_kill != name
|
626
|
+
threads.each {|t| t.kill }
|
627
|
+
@listening_threads[name] = []
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Listen (and block) for any new incoming messages.
|
633
|
+
# @params [Hash] A hash of containing:
|
634
|
+
# [Integer] timeout Max time to wait for a new message before returning
|
635
|
+
# [String] order_by Either "desc" or "asc"
|
636
|
+
# @return [Array] An array of messages received
|
637
|
+
def listen(options={})
|
638
|
+
response = request(:listen, options)
|
639
|
+
raise "Error listening for messages: (#{response.status}) #{response.body}" if response.status != 200
|
640
|
+
new_messages = JSON.parse(response.body)["messages"]
|
641
|
+
@listener_mutex.synchronize do
|
642
|
+
@last = new_messages.last["timestamp"] unless new_messages.empty?
|
643
|
+
new_messages.map! { |m| m["content"] }
|
644
|
+
@messages += new_messages
|
645
|
+
end
|
646
|
+
new_messages
|
647
|
+
end
|
648
|
+
|
649
|
+
def mediaType(name)
|
650
|
+
@spire.mediaType(name)
|
651
|
+
end
|
652
|
+
|
653
|
+
private
|
654
|
+
def current_listeners
|
655
|
+
@listener_mutex.synchronize do #To prevent synch problems adding a new listener while looping
|
656
|
+
@listeners.dup
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
# Object representing a Spire billing
|
662
|
+
#
|
663
|
+
# You can get all the billing plans by calling the method billing in Spire object
|
664
|
+
# * spire = Spire.new
|
665
|
+
# * billing = spire.billing()
|
666
|
+
# * plans = billing.plans
|
667
|
+
class Billing
|
668
|
+
def initialize(spire,properties)
|
669
|
+
@spire = spire
|
670
|
+
@properties = properties
|
671
|
+
end
|
672
|
+
|
673
|
+
def url
|
674
|
+
@properties["url"]
|
675
|
+
end
|
676
|
+
|
677
|
+
def plans
|
678
|
+
@properties["plans"]
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spire_io
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: -3702664328
|
5
|
+
prerelease: true
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
- alpha
|
11
|
+
- 2
|
12
|
+
version: 1.0.0.alpha.2
|
13
|
+
platform: ruby
|
14
|
+
authors:
|
15
|
+
- Dan Yoder
|
16
|
+
- Daniel Lockhart dlockhart@spire.io
|
17
|
+
autorequire:
|
18
|
+
bindir: bin
|
19
|
+
cert_chain: []
|
20
|
+
|
21
|
+
date: 2012-01-23 00:00:00 -08:00
|
22
|
+
default_executable:
|
23
|
+
dependencies:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: json
|
26
|
+
prerelease: false
|
27
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
hash: 3
|
33
|
+
segments:
|
34
|
+
- 1
|
35
|
+
- 6
|
36
|
+
version: "1.6"
|
37
|
+
type: :runtime
|
38
|
+
version_requirements: *id001
|
39
|
+
- !ruby/object:Gem::Dependency
|
40
|
+
name: excon
|
41
|
+
prerelease: false
|
42
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
hash: 5
|
48
|
+
segments:
|
49
|
+
- 0
|
50
|
+
- 7
|
51
|
+
version: "0.7"
|
52
|
+
type: :runtime
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rspec
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 13
|
63
|
+
segments:
|
64
|
+
- 2
|
65
|
+
- 7
|
66
|
+
version: "2.7"
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yard
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 5
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
- 7
|
81
|
+
version: "0.7"
|
82
|
+
type: :development
|
83
|
+
version_requirements: *id004
|
84
|
+
description: "\t\tThe spire_io gem allows you to quickly and easily use the spire.io service\n\
|
85
|
+
\t\tusing Ruby. See http://www.spire.io/ for more.\n"
|
86
|
+
email:
|
87
|
+
- dan@spire.io
|
88
|
+
-
|
89
|
+
executables: []
|
90
|
+
|
91
|
+
extensions: []
|
92
|
+
|
93
|
+
extra_rdoc_files: []
|
94
|
+
|
95
|
+
files:
|
96
|
+
- lib/requestable.rb
|
97
|
+
- lib/spire_io.rb
|
98
|
+
has_rdoc: true
|
99
|
+
homepage: https://github.com/spire-io/spire.io.rb
|
100
|
+
licenses: []
|
101
|
+
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 3
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ">"
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
hash: 25
|
122
|
+
segments:
|
123
|
+
- 1
|
124
|
+
- 3
|
125
|
+
- 1
|
126
|
+
version: 1.3.1
|
127
|
+
requirements: []
|
128
|
+
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 1.3.7
|
131
|
+
signing_key:
|
132
|
+
specification_version: 3
|
133
|
+
summary: Ruby client for spire.io
|
134
|
+
test_files: []
|
135
|
+
|