analysand 1.0.1
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/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +41 -0
- data/Rakefile +8 -0
- data/analysand.gemspec +30 -0
- data/bin/analysand +28 -0
- data/lib/analysand.rb +5 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +276 -0
- data/lib/analysand/connection_testing.rb +46 -0
- data/lib/analysand/database.rb +492 -0
- data/lib/analysand/errors.rb +26 -0
- data/lib/analysand/instance.rb +156 -0
- data/lib/analysand/response.rb +45 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_session_grantor.rb +40 -0
- data/spec/analysand/change_watcher_spec.rb +84 -0
- data/spec/analysand/database_spec.rb +228 -0
- data/spec/analysand/database_writing_spec.rb +478 -0
- data/spec/analysand/instance_spec.rb +86 -0
- data/spec/analysand/response_spec.rb +22 -0
- data/spec/analysand/view_response_spec.rb +33 -0
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +73 -0
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +75 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- metadata +276 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'celluloid/logger'
|
2
|
+
|
3
|
+
module Analysand
|
4
|
+
module ConnectionTesting
|
5
|
+
include Celluloid::Logger
|
6
|
+
|
7
|
+
##
|
8
|
+
# Issues a HEAD request to the given URI. If it responds with a success or
|
9
|
+
# redirection code, returns true; otherwise, returns false.
|
10
|
+
def test_http_connection(uri)
|
11
|
+
begin
|
12
|
+
resp = Net::HTTP.start(uri.host, uri.port) { |h| h.head(uri.request_uri) }
|
13
|
+
|
14
|
+
case resp
|
15
|
+
when Net::HTTPSuccess then true
|
16
|
+
when Net::HTTPRedirection then true
|
17
|
+
else
|
18
|
+
error "Expected HEAD #{uri.to_s} to return 200, got #{resp.code} (#{resp.body}) instead"
|
19
|
+
false
|
20
|
+
end
|
21
|
+
rescue => e
|
22
|
+
error "#{e.class} (#{e.message}) caught while attempting connection to #{uri.to_s}"
|
23
|
+
error e.backtrace.join("\n")
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Periodically checks a URI for success using test_http_connection, and
|
30
|
+
# raises an error if test_http_connection does not return success before
|
31
|
+
# the timeout is reached.
|
32
|
+
def wait_for_http_service(uri, timeout = 30)
|
33
|
+
state = 1.upto(timeout) do
|
34
|
+
if test_http_connection(Catalog::Settings.solr_uri)
|
35
|
+
break :started
|
36
|
+
else
|
37
|
+
sleep 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
unless state == :started
|
42
|
+
raise "#{uri.to_s} took longer than #{timeout} seconds to return a success response"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,492 @@
|
|
1
|
+
require 'analysand/bulk_response'
|
2
|
+
require 'analysand/errors'
|
3
|
+
require 'analysand/response'
|
4
|
+
require 'analysand/view_response'
|
5
|
+
require 'net/http/persistent'
|
6
|
+
require 'rack/utils'
|
7
|
+
|
8
|
+
module Analysand
|
9
|
+
##
|
10
|
+
# A wrapper around a CouchDB database in a CouchDB instance.
|
11
|
+
#
|
12
|
+
# Databases MUST be identified by an absolute URI; instantiating this class
|
13
|
+
# with a relative URI will raise an exception.
|
14
|
+
#
|
15
|
+
#
|
16
|
+
# Common tasks
|
17
|
+
# ============
|
18
|
+
#
|
19
|
+
# Creating a database
|
20
|
+
# -------------------
|
21
|
+
#
|
22
|
+
# vdb = Analysand::Database.create!('http://localhost:5984/videos/',
|
23
|
+
# credentials)
|
24
|
+
#
|
25
|
+
# If the database was successfully created, you'll get back a
|
26
|
+
# Analysand::Database instance. If database creation failed, a
|
27
|
+
# DatabaseError containing a CouchDB response will be raised.
|
28
|
+
#
|
29
|
+
# You can also instantiate a database and then create it:
|
30
|
+
#
|
31
|
+
# vdb = Analysand::Database.new('http://localhost:5984/videos')
|
32
|
+
# vdb.create(credentials) # => #<Response ...>
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# Dropping a database
|
36
|
+
# -------------------
|
37
|
+
#
|
38
|
+
# Analysand::Database.drop('http://localhost:5984/videos',
|
39
|
+
# credentials)
|
40
|
+
#
|
41
|
+
# # => #<Response code=200 ...>
|
42
|
+
# # => #<Response code=401 ...>
|
43
|
+
# # => #<Response code=404 ...>
|
44
|
+
#
|
45
|
+
# You can also instantiate a database and then drop it:
|
46
|
+
#
|
47
|
+
# db = Analysand::Database.new('http://localhost:5984/videos')
|
48
|
+
# db.drop # => #<Response ...>
|
49
|
+
#
|
50
|
+
# You can also use #drop!, which will raise Analysand::CannotDropDatabase
|
51
|
+
# on a non-success response.
|
52
|
+
#
|
53
|
+
#
|
54
|
+
# Opening a database
|
55
|
+
# ------------------
|
56
|
+
#
|
57
|
+
# vdb = Analysand::Database.new('http://localhost:5984/videos/')
|
58
|
+
#
|
59
|
+
#
|
60
|
+
# Closing connections
|
61
|
+
# -------------------
|
62
|
+
#
|
63
|
+
# vdb.close
|
64
|
+
#
|
65
|
+
# Note that this only closes the connection used for the current thread. If
|
66
|
+
# the database object is being used from several threads, there will still
|
67
|
+
# be other connections active. To close all connections, you must call
|
68
|
+
# #close from all threads that are using the database object.
|
69
|
+
#
|
70
|
+
# It is safe to call #close without additional synchronization.
|
71
|
+
#
|
72
|
+
# After close returns, you can re-open a connection by calling #get, #put,
|
73
|
+
# etc.
|
74
|
+
#
|
75
|
+
#
|
76
|
+
# Creating a document
|
77
|
+
# -------------------
|
78
|
+
#
|
79
|
+
# doc = { ... }
|
80
|
+
# vdb.put(doc_id, doc, credentials) # => #<Response code=201 ...>
|
81
|
+
# # => #<Response code=403 ...>
|
82
|
+
# # => #<Response code=409 ...>
|
83
|
+
#
|
84
|
+
# Any object that responds to #to_json with a JSON representation of itself
|
85
|
+
# may be used as the document.
|
86
|
+
#
|
87
|
+
# Updating a document
|
88
|
+
# -------------------
|
89
|
+
#
|
90
|
+
# doc = { '_rev' => rev, ... }
|
91
|
+
# vdb.put(doc_id, doc, credentials) # => #<Response code=201 ...>
|
92
|
+
# # => #<Response code=401 ...>
|
93
|
+
# # => #<Response code=409 ...>
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# You can also use #put!, which will raise Analysand::DocumentNotSaved if the
|
97
|
+
# response code is non-success.
|
98
|
+
#
|
99
|
+
# begin
|
100
|
+
# vdb.put!(doc_id, doc, credentials)
|
101
|
+
# rescue Analysand::DocumentNotSaved => e
|
102
|
+
# puts "Unable to save #{doc_id}, reason: #{e.response.body}"
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# #put!, if it returns, returns the response.
|
106
|
+
#
|
107
|
+
#
|
108
|
+
# Deleting a document
|
109
|
+
# -------------------
|
110
|
+
#
|
111
|
+
# vdb.delete(doc_id, rev, credentials) # => #<Response code=200 ...>
|
112
|
+
# # => #<Response code=401 ...>
|
113
|
+
# # => #<Response code=409 ...>
|
114
|
+
#
|
115
|
+
# You can also use #delete!, which will raise Analysand::DocumentNotDeleted if
|
116
|
+
# the response code is non-success.
|
117
|
+
#
|
118
|
+
#
|
119
|
+
# Retrieving a document
|
120
|
+
# ---------------------
|
121
|
+
#
|
122
|
+
# vdb.get(doc_id, credentials) # => #<Response code=200 ...>
|
123
|
+
# # => #<Response code=401 ...>
|
124
|
+
# # => #<Response code=404 ...>
|
125
|
+
#
|
126
|
+
# Note: CouchDB treats forward slashes (/) specially. For document IDs, /
|
127
|
+
# denotes a separator between document ID and the name of an attachment.
|
128
|
+
# This library makes use of that to implement attachment storage and
|
129
|
+
# retrieval (see below).
|
130
|
+
#
|
131
|
+
# If you are using forward slashes in document IDs, you MUST encode them
|
132
|
+
# (i.e. replace / with %2F).
|
133
|
+
#
|
134
|
+
# You can also use #get!, which will raise Analysand::CannotAccessDocument if
|
135
|
+
# the response code is non-success.
|
136
|
+
#
|
137
|
+
#
|
138
|
+
# Reading a view
|
139
|
+
# --------------
|
140
|
+
#
|
141
|
+
# vdb.view('video/recent', :key => ['member1'])
|
142
|
+
# vdb.view('video/by_artist', :startkey => 'a', :endkey => 'b')
|
143
|
+
#
|
144
|
+
# Keys are automatically JSON-encoded. The view method returns a
|
145
|
+
# ViewResponse, which may be accessed like this:
|
146
|
+
#
|
147
|
+
# resp = vdb.view('video/recent', :limit => 10)
|
148
|
+
# resp.total_rows # => 16
|
149
|
+
# resp.offset # => 0
|
150
|
+
# resp.rows # => [ { 'id' => ... }, ... } ]
|
151
|
+
#
|
152
|
+
# See ViewResponse for more details.
|
153
|
+
#
|
154
|
+
# You can also use view!, which will raise Analysand::CannotAccessView on a
|
155
|
+
# non-success response.
|
156
|
+
#
|
157
|
+
#
|
158
|
+
# Uploading an attachment
|
159
|
+
# -----------------------
|
160
|
+
#
|
161
|
+
# vdb.put_attachment('doc1/attachment', io, {}, credentials)
|
162
|
+
# # => #<Response>
|
163
|
+
#
|
164
|
+
# The second argument MUST be an IO-like object. The third argument MAY
|
165
|
+
# contain any of the following options:
|
166
|
+
#
|
167
|
+
# * :rev: When specified, this will be used as the rev of the document that
|
168
|
+
# will own the attachment. When not specified, no rev will be passed in
|
169
|
+
# the request. In order to add attachments to existing documents, then,
|
170
|
+
# you MUST pass this option.
|
171
|
+
# * :content_type: The MIME type of the attachment.
|
172
|
+
#
|
173
|
+
#
|
174
|
+
# Retrieving an attachment
|
175
|
+
# ------------------------
|
176
|
+
#
|
177
|
+
# vdb.get_attachment('doc1/attachment', credentials) do |resp|
|
178
|
+
# # resp is a Net::HTTPResponse
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# or, if you don't need that level of control when reading the response
|
182
|
+
# body:
|
183
|
+
#
|
184
|
+
# vdb.get_attachment('doc1/attachment', credentials)
|
185
|
+
# # => Net::HTTPResponse
|
186
|
+
#
|
187
|
+
# When a block is passed, #get_attachment does not read the response body,
|
188
|
+
# leaving that up to the programmer. When a block is _not_ passed,
|
189
|
+
# #get_attachment reads the body in full.
|
190
|
+
#
|
191
|
+
#
|
192
|
+
# Pinging a database
|
193
|
+
# ------------------
|
194
|
+
#
|
195
|
+
# Useful for connection testing:
|
196
|
+
#
|
197
|
+
# vdb.ping # => #<Response code=200 ...>
|
198
|
+
#
|
199
|
+
#
|
200
|
+
# Getting database status
|
201
|
+
# -----------------------
|
202
|
+
#
|
203
|
+
# vdb.status # => { "db_name" => "videos", ... }
|
204
|
+
#
|
205
|
+
# The returned hash is a parsed form of the JSON received from a GET on the
|
206
|
+
# database.
|
207
|
+
#
|
208
|
+
#
|
209
|
+
# Copying a document
|
210
|
+
# ------------------
|
211
|
+
#
|
212
|
+
# vdb.copy('source', 'destination', credentials)
|
213
|
+
# # => #<Response code=201 ...>
|
214
|
+
# # => #<Response code=401 ...>
|
215
|
+
# # => #<Response code=409 ...>
|
216
|
+
#
|
217
|
+
# To overwrite, you'll need to provide a rev of the destination document:
|
218
|
+
#
|
219
|
+
# vdb.copy('source', "destination?rev=#{rev}", credentials)
|
220
|
+
#
|
221
|
+
#
|
222
|
+
# Acceptable credentials
|
223
|
+
# ======================
|
224
|
+
#
|
225
|
+
# Every method that interacts with CouchDB has an optional credentials
|
226
|
+
# parameter. Two forms of credential are recognized by this class.
|
227
|
+
#
|
228
|
+
# 1. HTTP Basic authentication: When credentials is a hash of the form
|
229
|
+
#
|
230
|
+
# { :username => "...", :password => "... }
|
231
|
+
#
|
232
|
+
# then it will be transformed into an Authorization header for HTTP Basic
|
233
|
+
# authentication.
|
234
|
+
#
|
235
|
+
# 2. Token authentication: When credentials is a string, it is interpreted
|
236
|
+
# as a cookie from CouchDB's Session API. The string is used as the
|
237
|
+
# value of a Cookie header.
|
238
|
+
#
|
239
|
+
# To get a token, use a CouchDB::Instance (ahem) instance.
|
240
|
+
#
|
241
|
+
# Omitting the credentials argument, or providing a form of credentials not
|
242
|
+
# listed here, will result in no credentials being passed in the request.
|
243
|
+
#
|
244
|
+
#
|
245
|
+
# Thread safety
|
246
|
+
# =============
|
247
|
+
#
|
248
|
+
# Database objects may be shared across multiple threads. The HTTP client
|
249
|
+
# used by this object (Net::HTTP::Persistent) creates one persistent
|
250
|
+
# connection per (uri.host, uri.port, thread) tuple, so connection pooling
|
251
|
+
# is also done.
|
252
|
+
class Database
|
253
|
+
include Rack::Utils
|
254
|
+
|
255
|
+
JSON_VALUE_PARAMETERS = %w(key keys startkey endkey).map(&:to_sym)
|
256
|
+
|
257
|
+
attr_reader :http
|
258
|
+
attr_reader :uri
|
259
|
+
|
260
|
+
def self.create!(uri, credentials = nil)
|
261
|
+
new(uri).tap { |db| db.create!(credentials) }
|
262
|
+
end
|
263
|
+
|
264
|
+
def self.drop(uri, credentials = nil)
|
265
|
+
new(uri).drop(credentials)
|
266
|
+
end
|
267
|
+
|
268
|
+
def initialize(uri)
|
269
|
+
raise InvalidURIError, 'You must supply an absolute URI' unless uri.absolute?
|
270
|
+
|
271
|
+
@http = Net::HTTP::Persistent.new('catalog_database')
|
272
|
+
@uri = uri
|
273
|
+
|
274
|
+
# URI.join (used to calculate a document URI) will replace the database
|
275
|
+
# name unless we make it clear that the database is part of the path
|
276
|
+
unless uri.path.end_with?('/')
|
277
|
+
uri.path += '/'
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def ping(credentials = nil)
|
282
|
+
req = Net::HTTP::Get.new(uri.to_s)
|
283
|
+
set_credentials(req, credentials)
|
284
|
+
|
285
|
+
Response.new(http.request(uri, req))
|
286
|
+
end
|
287
|
+
|
288
|
+
def status(credentials = nil)
|
289
|
+
ping(credentials).body
|
290
|
+
end
|
291
|
+
|
292
|
+
def close
|
293
|
+
http.shutdown
|
294
|
+
end
|
295
|
+
|
296
|
+
def put(doc_id, doc, credentials = nil, options = {})
|
297
|
+
query = options
|
298
|
+
headers = { 'Content-Type' => 'application/json' }
|
299
|
+
|
300
|
+
Response.new _put(doc_id, credentials, options, headers, doc.to_json)
|
301
|
+
end
|
302
|
+
|
303
|
+
def put!(doc_id, doc, credentials = nil, options = {})
|
304
|
+
put(doc_id, doc, credentials, options).tap do |resp|
|
305
|
+
raise ex(DocumentNotSaved, resp) unless resp.success?
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def ensure_full_commit(credentials = nil, options = {})
|
310
|
+
headers = { 'Content-Type' => 'application/json' }
|
311
|
+
|
312
|
+
Response.new _post('_ensure_full_commit', credentials, options, headers, {}.to_json)
|
313
|
+
end
|
314
|
+
|
315
|
+
def bulk_docs(docs, credentials = nil, options = {})
|
316
|
+
headers = { 'Content-Type' => 'application/json' }
|
317
|
+
body = { 'docs' => docs }
|
318
|
+
body['all_or_nothing'] = true if options[:all_or_nothing]
|
319
|
+
|
320
|
+
BulkResponse.new _post('_bulk_docs', credentials, {}, headers, body.to_json)
|
321
|
+
end
|
322
|
+
|
323
|
+
def bulk_docs!(docs, credentials = nil, options = {})
|
324
|
+
bulk_docs(docs, credentials, options).tap do |resp|
|
325
|
+
raise ex(BulkOperationFailed, resp) unless resp.success?
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def copy(source, destination, credentials = nil)
|
330
|
+
headers = { 'Destination' => destination }
|
331
|
+
|
332
|
+
Response.new _copy(source, credentials, {}, headers, nil)
|
333
|
+
end
|
334
|
+
|
335
|
+
def put_attachment(loc, io, credentials = nil, options = {})
|
336
|
+
query = {}
|
337
|
+
headers = {}
|
338
|
+
|
339
|
+
if options[:rev]
|
340
|
+
query['rev'] = options[:rev]
|
341
|
+
end
|
342
|
+
|
343
|
+
if options[:content_type]
|
344
|
+
headers['Content-Type'] = options[:content_type]
|
345
|
+
end
|
346
|
+
|
347
|
+
Response.new _put(loc, credentials, query, headers, io.read)
|
348
|
+
end
|
349
|
+
|
350
|
+
def delete(doc_id, rev, credentials = nil)
|
351
|
+
headers = { 'If-Match' => rev }
|
352
|
+
|
353
|
+
Response.new _delete(doc_id, credentials, {}, headers, nil)
|
354
|
+
end
|
355
|
+
|
356
|
+
def delete!(doc_id, rev, credentials = nil)
|
357
|
+
delete(doc_id, rev, credentials).tap do |resp|
|
358
|
+
raise ex(DocumentNotDeleted, resp) unless resp.success?
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def get(doc_id, credentials = nil)
|
363
|
+
Response.new(_get(doc_id, credentials))
|
364
|
+
end
|
365
|
+
|
366
|
+
def get!(doc_id, credentials = nil)
|
367
|
+
get(doc_id, credentials).tap do |resp|
|
368
|
+
raise ex(CannotAccessDocument, resp) unless resp.success?
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def head(doc_id, credentials = nil)
|
373
|
+
Response.new(_head(doc_id, credentials))
|
374
|
+
end
|
375
|
+
|
376
|
+
def get_attachment(loc, credentials = nil)
|
377
|
+
_get(loc, credentials)
|
378
|
+
end
|
379
|
+
|
380
|
+
def all_docs(parameters = {}, credentials = nil)
|
381
|
+
view('_all_docs', parameters, credentials)
|
382
|
+
end
|
383
|
+
|
384
|
+
def all_docs!(parameters = {}, credentials = nil)
|
385
|
+
view!('_all_docs', parameters, credentials)
|
386
|
+
end
|
387
|
+
|
388
|
+
def view(view_name, parameters = {}, credentials = nil)
|
389
|
+
view_path = expand_view_path(view_name)
|
390
|
+
|
391
|
+
JSON_VALUE_PARAMETERS.each do |p|
|
392
|
+
if parameters.has_key?(p)
|
393
|
+
parameters[p] = parameters[p].to_json
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
ViewResponse.new _get(view_path, credentials, parameters, {})
|
398
|
+
end
|
399
|
+
|
400
|
+
def expand_view_path(view_name)
|
401
|
+
if view_name.include?('/')
|
402
|
+
design_doc, view_name = view_name.split('/', 2)
|
403
|
+
"_design/#{design_doc}/_view/#{view_name}"
|
404
|
+
else
|
405
|
+
view_name
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def view!(view_name, parameters = {}, credentials = nil)
|
410
|
+
view(view_name, parameters, credentials).tap do |resp|
|
411
|
+
raise ex(CannotAccessView, resp) unless resp.success?
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def create(credentials = nil)
|
416
|
+
req = Net::HTTP::Put.new(uri.to_s)
|
417
|
+
set_credentials(req, credentials)
|
418
|
+
|
419
|
+
Response.new(http.request(uri, req))
|
420
|
+
end
|
421
|
+
|
422
|
+
def create!(credentials = nil)
|
423
|
+
create(credentials).tap do |resp|
|
424
|
+
raise ex(DatabaseError, resp) unless resp.success?
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def drop(credentials = nil)
|
429
|
+
req = Net::HTTP::Delete.new(uri.to_s)
|
430
|
+
set_credentials(req, credentials)
|
431
|
+
|
432
|
+
Response.new(http.request(uri, req))
|
433
|
+
end
|
434
|
+
|
435
|
+
def drop!(credentials = nil)
|
436
|
+
drop(credentials).tap do |resp|
|
437
|
+
raise ex(CannotDropDatabase, resp) unless resp.success?
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
%w(Head Get Put Post Delete Copy).each do |m|
|
442
|
+
str = <<-END
|
443
|
+
def _#{m.downcase}(doc_id, credentials, query = {}, headers = {}, body = nil)
|
444
|
+
_req(Net::HTTP::#{m}, doc_id, credentials, query, headers, body)
|
445
|
+
end
|
446
|
+
END
|
447
|
+
|
448
|
+
class_eval str, __FILE__, __LINE__
|
449
|
+
end
|
450
|
+
|
451
|
+
##
|
452
|
+
# @private
|
453
|
+
def _req(klass, doc_id, credentials, query, headers, body)
|
454
|
+
uri = URI(self.uri.to_s + URI.escape(doc_id))
|
455
|
+
uri.query = build_query(query) unless query.empty?
|
456
|
+
|
457
|
+
req = klass.new(uri.request_uri)
|
458
|
+
|
459
|
+
headers.each { |k, v| req.add_field(k, v) }
|
460
|
+
req.body = body if body && req.request_body_permitted?
|
461
|
+
set_credentials(req, credentials)
|
462
|
+
|
463
|
+
http.request(uri, req)
|
464
|
+
end
|
465
|
+
|
466
|
+
##
|
467
|
+
# Sets credentials on a request object.
|
468
|
+
#
|
469
|
+
# If creds is a hash containing :username and :password keys, HTTP basic
|
470
|
+
# authorization is used. If creds is a string, the string is added as a
|
471
|
+
# cookie.
|
472
|
+
def set_credentials(req, creds)
|
473
|
+
return unless creds
|
474
|
+
|
475
|
+
if String === creds
|
476
|
+
req.add_field('Cookie', creds)
|
477
|
+
elsif creds[:username] && creds[:password]
|
478
|
+
req.basic_auth(creds[:username], creds[:password])
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
##
|
483
|
+
# @private
|
484
|
+
def ex(klass, response)
|
485
|
+
klass.new("Expected response to have code 2xx, got #{response.code} instead").tap do |ex|
|
486
|
+
ex.response = response
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# vim:ts=2:sw=2:et:tw=78
|