spire_io 1.0.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
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
+