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.
Files changed (3) hide show
  1. data/lib/requestable.rb +70 -0
  2. data/lib/spire_io.rb +681 -0
  3. metadata +135 -0
@@ -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
+