love 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- love (0.0.4)
4
+ love (0.0.5)
5
5
  activesupport
6
6
  yajl-ruby
7
7
 
@@ -22,12 +22,12 @@ I previously used HTTParty to connect to the Tender API, but I ran into two issu
22
22
 
23
23
  == Installation
24
24
 
25
- Run <tt>gem install love</tt> or add <tt>gem 'love'</tt> to your Gemfile.
25
+ Run <tt>gem install love</tt> or add <tt>gem "love"</tt> to your Gemfile.
26
26
 
27
27
  == Usage
28
28
 
29
29
  require 'love'
30
- tender = Love.connect('account', 'api_key')
30
+ tender = Love.connect('site', 'api_key')
31
31
 
32
32
  # Loop over all discussions:
33
33
  tender.each_discussion do |discussion|
@@ -35,11 +35,15 @@ Run <tt>gem install love</tt> or add <tt>gem 'love'</tt> to your Gemfile.
35
35
  end
36
36
 
37
37
  # Also available:
38
- tender.each_user { |c| ... }
38
+ tender.each_user { |u| ... }
39
39
  tender.each_queue { |q| ... }
40
40
  tender.each_category { |c| ... }
41
41
 
42
+ # Or get a single record, using an ID or HREF:
43
+ d = tender.get_discussion('12345')
44
+ u = tender.get_user('https://api.tenderapp.com/site/users/12345')
45
+
42
46
  == About
43
47
 
44
- This library is Written by Willem van Bergen for Shopify, and is MIT licensed.
48
+ This library is written by Willem van Bergen for Shopify, and is MIT licensed.
45
49
 
@@ -1,143 +1,313 @@
1
1
  require 'uri'
2
+ require 'cgi'
2
3
  require 'net/https'
3
- require 'active_support/core_ext/module/attribute_accessors.rb'
4
+ require 'active_support/core_ext/module/attribute_accessors'
5
+ require 'active_support/core_ext/hash/keys'
4
6
  require 'yajl'
5
7
 
8
+ # Love is a small Ruby library to interact with the Tender REST API.
9
+ # The main object to work with is {Love::Client}, which is returned
10
+ # by calling {Love.connect}.
11
+ #
12
+ # It is dedicated to the awesome work Aaeron Patterson has been giving
13
+ # to the Ruby community.
6
14
  module Love
7
15
 
8
- VERSION = "0.0.4"
16
+ # The current gem version. Will be updated automatically by the gem
17
+ # release script.
18
+ VERSION = "0.0.5"
9
19
 
10
- # Create a custom exception class.
20
+ # Exception class a custom exception class.
11
21
  class Exception < StandardError; end
12
22
 
13
- # Class for unauthorized exceptions
23
+ # Exception class for unauthorized requests or resources that are forbidden
14
24
  class Unauthorized < Love::Exception; end
15
25
 
26
+ # Exception class for resources that are not found
27
+ class NotFound < Love::Exception; end
16
28
 
17
- def self.connect(account, api_key, options = {})
18
- Love::Client.new(account, api_key, options)
29
+
30
+ # Connects to the Tender API to access a site using an API key.
31
+ #
32
+ # This method doesn't do any communication yet with the Tender API, it will
33
+ # only set up the client with the provided credentials and options.
34
+ #
35
+ # By default, the client will use a new connection for every request. It can also
36
+ # open a persistent connection if you want to do multiple requests, by passing
37
+ # <tt>:persistent => true</tt> as option. In this case, you need to call {Love::Client#close_connection}
38
+ # when you are done with your session. You can also provide a block to this method
39
+ # which will automatically open up a persistent connection and close it at the end
40
+ # of the block.
41
+ #
42
+ # @param (see Love::Client#initialize)
43
+ # @option (see Love::Client#initialize)
44
+ # @overload connect(site, api_key, options = {}) { ... }
45
+ # @yield [Love::Client] An API client to work with using a persistent connection.
46
+ # @return The return value of the block. The persistent connection will be closed
47
+ # @overload connect(site, api_key, options = {})
48
+ # @return [Love::Client] An API client to work with. By default, the returned
49
+ # client will not use a persistent TCP connection, so a new connection will
50
+ # be
51
+ # @see Love::Client#initialize
52
+ def self.connect(site, api_key, options = {}, &block)
53
+ if block_given?
54
+ begin
55
+ client = Love::Client.new(site, api_key, options.merge(:persistent => true))
56
+ block.call(client)
57
+ ensure
58
+ client.close_connection
59
+ end
60
+ else
61
+ Love::Client.new(site, api_key, options)
62
+ end
19
63
  end
20
64
 
65
+ # @return [Logger] Set this attribute to a Logger instance to log the
66
+ # HTTP connectivity somewhere.
21
67
  mattr_accessor :logger
22
68
 
69
+ # Module to work with Tender REST URIs.
23
70
  module ResourceURI
71
+
72
+ # Returns a collection URI, based on an URI instance, a complete URI string or just a resource name.
73
+ # @return [URI] The URI on which the REST resource collection is accessible through the Tender REST API.
74
+ # @raise [Love::Exception] If the input cannot be converted into a resource collection URI.
24
75
  def collection_uri(input)
25
76
  case input.to_s
26
77
  when /^[\w-]+$/
27
- ::URI.parse("https://api.tenderapp.com/#{account}/#{input}")
28
- when %r[^https?://api\.tenderapp\.com/#{account}/[\w-]+]
78
+ ::URI.parse("https://api.tenderapp.com/#{site}/#{input}")
79
+ when %r[^https?://api\.tenderapp\.com/#{site}/[\w-]+]
29
80
  ::URI.parse(input.to_s)
30
81
  else
31
82
  raise Love::Exception, "This does not appear to be a valid Tender category URI!"
32
83
  end
33
84
  end
34
-
85
+
86
+ # Returns a resource URI, based on an URI instance, a complete URI string or just a resource ID.
87
+ # @param [Object input The complete URI or just resource ID as URI, String or Integer.
88
+ # @param [String] kind The kind of resource.
89
+ # @return [URI] The URI on which the REST resource is accessible through the Tender REST API.
90
+ # @raise [Love::Exception] If the input cannot be converted into a resource URI.
35
91
  def singleton_uri(input, kind)
36
92
  case input.to_s
37
93
  when /^\d+/
38
- ::URI.parse("https://api.tenderapp.com/#{account}/#{kind}/#{input}")
39
- when %r[^https?://api\.tenderapp\.com/#{account}/#{kind}/\d+]
94
+ ::URI.parse("https://api.tenderapp.com/#{site}/#{kind}/#{input}")
95
+ when %r[^https?://api\.tenderapp\.com/#{site}/#{kind}/\d+]
40
96
  ::URI.parse(input.to_s)
41
97
  else
42
98
  raise Love::Exception, "This does not appear to be a Tender #{kind} URI or ID!"
43
99
  end
44
100
  end
45
101
 
46
- def request_uri(base_uri, added_params = {})
102
+ # Appends GET parameters to a URI instance. Duplicate parameters will
103
+ # be replaced with the new value.
104
+ # @param [URI] base_uri The original URI to work with (will not be modified)
105
+ # @param [Hash] added_params To GET params to add.
106
+ # @return [URI] The URI with appended GET parameters
107
+ def append_query(base_uri, added_params = {})
47
108
  base_params = base_uri.query ? CGI.parse(base_uri.query) : {}
48
- get_params = base_params.merge(added_params || {})
109
+ get_params = base_params.merge(added_params.stringify_keys)
49
110
  base_uri.dup.tap do |uri|
50
- uri.query = get_params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"}.join('&')
111
+ assignments = get_params.map do |k, v|
112
+ case v
113
+ when Array; v.map { |val| "#{::CGI.escape(k.to_s)}=#{::CGI.escape(val.to_s)}" }.join('&')
114
+ else "#{::CGI.escape(k.to_s)}=#{::CGI.escape(v.to_s)}"
115
+ end
116
+ end
117
+ uri.query = assignments.join('&')
51
118
  end
52
119
  end
53
120
  end
54
-
121
+
122
+ # The Love::Client class acts as a client to the Tender REST API. Obtain an instance of this
123
+ # class by calling {Love.connect} instead of instantiating this class directly.
124
+ #
125
+ # You can either fetch individual resources using {#get_user}, {#get_discussion}, and
126
+ # similar methods, or iterate over collections using {#each_discussion}, {#each_category}
127
+ # and similar methods.
55
128
  class Client
56
129
 
130
+ # The Tender API host to connect to.
131
+ TENDER_API_HOST = 'api.tenderapp.com'
132
+
57
133
  include Love::ResourceURI
58
134
 
59
- attr_reader :account
135
+ # @return [String] The site to work with
136
+ attr_reader :site
137
+
138
+ # @return [String] The API key to authenticate with.
60
139
  attr_reader :api_key
61
140
 
141
+ # @return [Float] The number of seconds to sleep between paged requests.
62
142
  attr_accessor :sleep_between_requests
63
143
 
64
- def initialize(account, api_key, options = {})
65
- @account, @api_key = account, api_key
144
+ # Initializes the client.
145
+ # @param [String] site The site to work with.
146
+ # @param [String] api_key The API key for this site.
147
+ # @param [Hash] options Connectivity options.
148
+ # @option options [Boolean] :persistent (false) Whether to create a persistent TCP connection.
149
+ # @option options [Float] :sleep_between_requests (0.5) The time between requests in seconds.
150
+ # @see Love.connect
151
+ def initialize(site, api_key, options = {})
152
+ @site, @api_key = site, api_key
66
153
 
67
154
  # Handle options
155
+ @persistent = !!options[:persistent]
68
156
  @sleep_between_requests = options[:sleep_between_requests] || 0.5
69
157
  end
70
158
 
71
- def get_user(id_or_href, options = {})
159
+ # Returns a single Tender user.
160
+ # @param [URI, String, Integer] id_or_href The user ID or HREF. Can be either a URI
161
+ # instance, a string containing a URI, or a user ID as a numeric string or integer.
162
+ # @return [Hash] The user attributes in a Hash.
163
+ def get_user(id_or_href)
72
164
  get(singleton_uri(id_or_href, 'users'))
73
165
  end
74
-
75
- def get_discussion(id_or_href, options = {})
166
+
167
+ # Returns a single Tender discussion.
168
+ # @param [URI, String, Integer] id_or_href The discussion ID or HREF. Can be either a URI
169
+ # instance, a string containing a URI, or a discussion ID as a numeric string or integer.
170
+ # @return [Hash] The discussion attributes in a Hash.
171
+ def get_discussion(id_or_href)
76
172
  get(singleton_uri(id_or_href, 'discussions'))
77
173
  end
78
174
 
79
- def get_category(id_or_href, options = {})
175
+ # Returns a single Tender category.
176
+ # @param [URI, String, Integer] id_or_href The category ID or HREF. Can be either a URI
177
+ # instance, a string containing a URI, or a category ID as a numeric string or integer.
178
+ # @return [Hash] The category attributes in a Hash.
179
+ def get_category(id_or_href)
80
180
  get(singleton_uri(id_or_href, 'categories'))
81
181
  end
82
182
 
83
- def get_queue(id_or_href, options = {})
183
+ # Returns a single Tender queue.
184
+ # @param [URI, String, Integer] id_or_href The queue ID or HREF. Can be either a URI
185
+ # instance, a string containing a URI, or a queue ID as a numeric string or integer.
186
+ # @return [Hash] The queue attributes in a Hash.
187
+ def get_queue(id_or_href)
84
188
  get(singleton_uri(id_or_href, 'queues'), options)
85
189
  end
86
-
190
+
191
+ # Iterates over all Tender categories.
192
+ # @yield [Hash] The attributes of each category will be yielded as (nested) Hash.
193
+ # @option (see #paged_each)
194
+ # @return [nil]
87
195
  def each_category(options = {}, &block)
88
- buffered_each(collection_uri('categories'), 'categories', options, &block)
196
+ paged_each(collection_uri('categories'), 'categories', options, &block)
89
197
  end
90
198
 
199
+ # Iterates over all Tender users.
200
+ # @yield [Hash] The attributes of each user will be yielded as (nested) Hash.
201
+ # @option (see #paged_each)
202
+ # @return [nil]
91
203
  def each_queue(options = {}, &block)
92
- buffered_each(collection_uri('queues'), 'named_queues', options, &block)
204
+ paged_each(collection_uri('queues'), 'named_queues', options, &block)
93
205
  end
94
206
 
207
+ # Iterates over all Tender users.
208
+ # @yield [Hash] The attributes of each user will be yielded as (nested) Hash.
209
+ # @option (see #paged_each)
210
+ # @return [nil]
95
211
  def each_user(options = {}, &block)
96
- buffered_each(collection_uri('users'), 'users', options, &block)
212
+ paged_each(collection_uri('users'), 'users', options, &block)
97
213
  end
98
214
 
215
+ # Iterates over all Tender discussions.
216
+ # @yield [Hash] The attributes of each discussion will be yielded as (nested) Hash.
217
+ # @option (see #paged_each)
218
+ # @return [nil]
99
219
  def each_discussion(options = {}, &block)
100
- buffered_each(collection_uri('discussions'), 'discussions', options, &block)
220
+ paged_each(collection_uri('discussions'), 'discussions', options, &block)
221
+ end
222
+
223
+ # Returns a persistent connection to the server, reusing a connection of it was
224
+ # previously established.
225
+ #
226
+ # This method is mainly used for internal use but can be used to do advanced
227
+ # HTTP connectivity with the Tender API server.
228
+ #
229
+ # @return [Net::HTTP] The net/http connection instance.
230
+ def connection
231
+ @connection ||= Net::HTTP.new(TENDER_API_HOST, Net::HTTP.https_default_port).tap do |http|
232
+ http.use_ssl = true
233
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
234
+ http.start
235
+ end
236
+ end
237
+
238
+ # Closes the persistent connectio to the server
239
+ # @return [nil]
240
+ def close_connection
241
+ @connection.finish if connected?
242
+ end
243
+
244
+ # @return [Boolean] <tt>true</tt> iff the client currently has a TCP connection with the Tender API server.
245
+ def connected?
246
+ @connection && @connection.started?
247
+ end
248
+
249
+ # @return [Boolean] <tt>true</tt> iff the client is using persistent connections.
250
+ def persistent?
251
+ @persistent
101
252
  end
102
253
 
103
254
  protected
255
+
256
+ def request_headers
257
+ @request_headers ||= { "Accept" => "application/vnd.tender-v1+json", "X-Tender-Auth" => api_key }
258
+ end
104
259
 
105
260
  def get(uri)
106
- Love.logger.debug "GET #{uri}" if Love.logger
261
+ raise Love::Exception, "This is not a Tender API URI." unless uri.host = TENDER_API_HOST
107
262
 
108
- http = Net::HTTP.new(uri.host, uri.port)
109
- if uri.scheme = 'https'
110
- http.use_ssl = true
111
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
112
- end
263
+ Love.logger.debug "GET #{uri.request_uri}" if Love.logger
113
264
 
114
- req = Net::HTTP::Get.new(uri.request_uri, {
115
- "Accept" => "application/vnd.tender-v1+json",
116
- "X-Tender-Auth" => api_key
117
- })
118
-
119
- response = http.request(req)
120
- case response.code
121
- when /^2\d\d/
265
+ request = Net::HTTP::Get.new(uri.request_uri, request_headers)
266
+ response = connection.request(request)
267
+ case response
268
+ when Net::HTTPSuccess; Yajl::Parser.new.parse(safely_convert_to_utf8(response.body))
269
+ when Net::HTTPUnauthorized; raise Love::Unauthorized, "Invalid credentials used!"
270
+ when Net::HTTPForbidden; raise Love::Unauthorized, "You don't have permission to access this resource!"
271
+ when Net::NotFound; raise Love::NotFound, "The resource #{uri} was not found!"
272
+ else raise Love::Exception, "#{response.code}: #{response.body}"
273
+ end
274
+ ensure
275
+ close_connection unless persistent?
276
+ end
277
+
278
+ # Converts a binary, (alomst) UTF-8 string into an actual UTF-8 string.
279
+ # It will replace any unknown characters or unvalid byte sequences into a UTF-8
280
+ # "unknown character" question mark.
281
+ #
282
+ # @param [String] binary_string The input string, should have binary encoding
283
+ # @return [String] The string using UTF-8 encoding.
284
+ def safely_convert_to_utf8(binary_string)
285
+ if binary_string.respond_to?(:force_encoding)
286
+ # Ruby 1.9
122
287
  converter = Encoding::Converter.new('binary', 'utf-8', :invalid => :replace, :undef => :replace)
123
- Yajl::Parser.new.parse(converter.convert(response.body))
124
- when '401'
125
- raise Love::Unauthorized, "Invalid credentials used!"
288
+ converter.convert(binary_string)
126
289
  else
127
- raise Love::Exception, "#{response.code}: #{response.body}"
290
+ # Ruby 1.8 - currently don't do anything
291
+ binary_string
128
292
  end
129
293
  end
130
294
 
131
- def buffered_each(uri, list_key, options = {}, &block)
295
+ # Iterates over a collection, issuing multiple requests to get all the paged results.
296
+ #
297
+ # @option options [Date] :since Only include records updated since the provided date.
298
+ # Caution: not supported by all resources.
299
+ # @option options [Integer] :start_page The initial page number to request.
300
+ # @option options [Integer] :end_page The final page number to request.
301
+ def paged_each(uri, list_key, options = {}, &block)
132
302
  query_params = {}
133
303
  query_params[:since] = options[:since].to_date.to_s(:db) if options[:since]
134
304
  query_params[:page] = [options[:start_page].to_i, 1].max rescue 1
135
305
 
136
- initial_result = get(request_uri(uri, query_params))
306
+ initial_result = get(append_query(uri, query_params))
137
307
 
138
308
  # Determine the amount of pages that is going to be requested.
139
- max_page = (initial_result['total'].to_f / initial_result['per_page'].to_f).ceil
140
- end_page = options[:end_page].nil? ? max_page : [options[:end_page].to_i, max_page].min
309
+ max_page = (initial_result['total'].to_f / initial_result['per_page'].to_f).ceil
310
+ end_page = options[:end_page].nil? ? max_page : [options[:end_page].to_i, max_page].min
141
311
 
142
312
  # Print out some initial debugging information
143
313
  Love.logger.debug "Paged requests to #{uri}: #{max_page} total pages, importing #{query_params[:page]} upto #{end_page}." if Love.logger
@@ -151,7 +321,7 @@ module Love
151
321
  start_page = query_params[:page].to_i + 1
152
322
  start_page.upto(end_page) do |page|
153
323
  query_params[:page] = page
154
- result = get(request_uri(uri, query_params))
324
+ result = get(append_query(uri, query_params))
155
325
  if result[list_key].kind_of?(Array)
156
326
  result[list_key].each { |record| yield(record) }
157
327
  sleep(sleep_between_requests) if sleep_between_requests
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
 
4
4
  # Do not change the version and date fields by hand. This will be done
5
5
  # automatically by the gem release script.
6
- s.version = "0.0.4"
7
- s.date = "2010-11-29"
6
+ s.version = "0.0.5"
7
+ s.date = "2010-11-30"
8
8
 
9
9
  s.summary = "Ruby library to access the Tender REST API."
10
10
  s.description = <<-EOT
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Love::ResourceURI do
4
4
 
5
5
  include Love::ResourceURI
6
- def account; "mysupport"; end
6
+ def site; "mysupport"; end
7
7
 
8
8
  describe '#collection_uri' do
9
9
 
@@ -23,7 +23,7 @@ describe Love::ResourceURI do
23
23
  collection_uri(uri).should == uri
24
24
  end
25
25
 
26
- it "should not accept a URI for another account" do
26
+ it "should not accept a URI for another site" do
27
27
  lambda { collection_uri('https://api.tenderapp.com/other/bars') }.should raise_error(Love::Exception)
28
28
  end
29
29
 
@@ -52,9 +52,9 @@ describe Love::ResourceURI do
52
52
  uri = URI.parse('https://api.tenderapp.com/mysupport/bars/789')
53
53
  singleton_uri(uri, 'bars').should be_kind_of(::URI)
54
54
  singleton_uri(uri, 'bars').should == uri
55
- end
55
+ end
56
56
 
57
- it "should not accept a URI for another account" do
57
+ it "should not accept a URI for another site" do
58
58
  lambda { singleton_uri('https://api.tenderapp.com/other/bars/123', 'bars') }.should raise_error(Love::Exception)
59
59
  end
60
60
 
@@ -64,7 +64,32 @@ describe Love::ResourceURI do
64
64
 
65
65
  it "should not weird resource IDs" do
66
66
  lambda { singleton_uri('%!&', 'bars') }.should raise_error(Love::Exception)
67
- end
67
+ end
68
+ end
69
+
70
+ describe '#append_query' do
71
+ before { @uri = URI.parse('https://api.tenderapp.com/') }
72
+
73
+ it "should return a URI instance" do
74
+ append_query(@uri, :a => 'b').should be_kind_of(URI)
75
+ end
76
+
77
+ it "should add a correctly quoted query string" do
78
+ adjusted = append_query(@uri, :a => 'some data')
79
+ adjusted.to_s.should == 'https://api.tenderapp.com/?a=some+data'
80
+ end
81
+
82
+ it "should keep existing query parameters intact" do
83
+ @uri.query = 'foo=bar'
84
+ adjusted = append_query(@uri, :a => 'some data')
85
+ adjusted.to_s.should == 'https://api.tenderapp.com/?foo=bar&a=some+data'
86
+ end
87
+
88
+ it "should overwrite existing parameters" do
89
+ @uri.query = 'foo=bar'
90
+ adjusted = append_query(@uri, :foo => 'baz')
91
+ adjusted.to_s.should == 'https://api.tenderapp.com/?foo=baz'
92
+ end
68
93
  end
69
94
  end
70
95
 
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: love
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
5
4
  prerelease: false
6
5
  segments:
7
6
  - 0
8
7
  - 0
9
- - 4
10
- version: 0.0.4
8
+ - 5
9
+ version: 0.0.5
11
10
  platform: ruby
12
11
  authors:
13
12
  - Willem van Bergen
@@ -15,64 +14,60 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2010-11-29 00:00:00 -05:00
17
+ date: 2010-11-30 00:00:00 -05:00
19
18
  default_executable:
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
22
  requirement: &id001 !ruby/object:Gem::Requirement
23
23
  none: false
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- hash: 3
28
27
  segments:
29
28
  - 0
30
29
  version: "0"
31
- name: activesupport
32
- prerelease: false
33
30
  type: :runtime
31
+ prerelease: false
34
32
  version_requirements: *id001
35
33
  - !ruby/object:Gem::Dependency
34
+ name: yajl-ruby
36
35
  requirement: &id002 !ruby/object:Gem::Requirement
37
36
  none: false
38
37
  requirements:
39
38
  - - ">="
40
39
  - !ruby/object:Gem::Version
41
- hash: 3
42
40
  segments:
43
41
  - 0
44
42
  version: "0"
45
- name: yajl-ruby
46
- prerelease: false
47
43
  type: :runtime
44
+ prerelease: false
48
45
  version_requirements: *id002
49
46
  - !ruby/object:Gem::Dependency
47
+ name: rake
50
48
  requirement: &id003 !ruby/object:Gem::Requirement
51
49
  none: false
52
50
  requirements:
53
51
  - - ">="
54
52
  - !ruby/object:Gem::Version
55
- hash: 3
56
53
  segments:
57
54
  - 0
58
55
  version: "0"
59
- name: rake
60
- prerelease: false
61
56
  type: :development
57
+ prerelease: false
62
58
  version_requirements: *id003
63
59
  - !ruby/object:Gem::Dependency
60
+ name: rspec
64
61
  requirement: &id004 !ruby/object:Gem::Requirement
65
62
  none: false
66
63
  requirements:
67
64
  - - ~>
68
65
  - !ruby/object:Gem::Version
69
- hash: 7
70
66
  segments:
71
67
  - 2
72
68
  version: "2"
73
- name: rspec
74
- prerelease: false
75
69
  type: :development
70
+ prerelease: false
76
71
  version_requirements: *id004
77
72
  description: " A simple API wrapper for Tender, that handles paged results, uses yajl-ruby for\n JSON parsing, and manually handles UTF-8 encoding to circumvent the invalid UTF-8\n character problem in Ruby 1.9.\n"
78
73
  email:
@@ -114,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
109
  requirements:
115
110
  - - ">="
116
111
  - !ruby/object:Gem::Version
117
- hash: 3
112
+ hash: 4412278213524451879
118
113
  segments:
119
114
  - 0
120
115
  version: "0"
@@ -123,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
118
  requirements:
124
119
  - - ">="
125
120
  - !ruby/object:Gem::Version
126
- hash: 3
121
+ hash: 4412278213524451879
127
122
  segments:
128
123
  - 0
129
124
  version: "0"