couchrest 1.2.1 → 2.0.0.beta1
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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.travis.yml +1 -0
- data/README.md +11 -7
- data/VERSION +1 -1
- data/couchrest.gemspec +7 -6
- data/history.txt +8 -0
- data/lib/couchrest.rb +26 -31
- data/lib/couchrest/connection.rb +251 -0
- data/lib/couchrest/database.rb +75 -79
- data/lib/couchrest/design.rb +1 -1
- data/lib/couchrest/exceptions.rb +108 -0
- data/lib/couchrest/helper/pager.rb +3 -1
- data/lib/couchrest/helper/stream_row_parser.rb +93 -0
- data/lib/couchrest/rest_api.rb +33 -134
- data/lib/couchrest/server.rb +34 -47
- data/spec/couchrest/connection_spec.rb +415 -0
- data/spec/couchrest/couchrest_spec.rb +61 -67
- data/spec/couchrest/database_spec.rb +151 -147
- data/spec/couchrest/design_spec.rb +28 -28
- data/spec/couchrest/document_spec.rb +72 -70
- data/spec/couchrest/exceptions_spec.rb +74 -0
- data/spec/couchrest/helpers/pager_spec.rb +22 -22
- data/spec/couchrest/helpers/stream_row_parser_spec.rb +154 -0
- data/spec/couchrest/rest_api_spec.rb +44 -208
- data/spec/couchrest/server_spec.rb +0 -29
- data/spec/spec_helper.rb +11 -6
- metadata +31 -17
- data/lib/couchrest/helper/streamer.rb +0 -63
- data/lib/couchrest/monkeypatches.rb +0 -25
- data/spec/couchrest/helpers/streamer_spec.rb +0 -134
data/lib/couchrest/database.rb
CHANGED
@@ -3,7 +3,17 @@ require "base64"
|
|
3
3
|
|
4
4
|
module CouchRest
|
5
5
|
class Database
|
6
|
-
|
6
|
+
|
7
|
+
# Server object we'll use to communicate with.
|
8
|
+
attr_reader :server
|
9
|
+
|
10
|
+
# Name of the database of we're using.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Name of the database we can use in requests.
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
# How many documents should be cached before peforming the bulk save operation
|
7
17
|
attr_accessor :bulk_save_cache_limit
|
8
18
|
|
9
19
|
# Create a CouchRest::Database adapter for the supplied CouchRest::Server
|
@@ -14,36 +24,41 @@ module CouchRest
|
|
14
24
|
# name<String>:: database name
|
15
25
|
#
|
16
26
|
def initialize(server, name)
|
17
|
-
@name
|
18
|
-
@server
|
19
|
-
@
|
20
|
-
@uri = "/#{name.gsub('/','%2F')}"
|
21
|
-
@root = host + uri
|
22
|
-
@streamer = Streamer.new
|
27
|
+
@name = name
|
28
|
+
@server = server
|
29
|
+
@path = "/#{name.gsub('/','%2F')}"
|
23
30
|
@bulk_save_cache = []
|
24
31
|
@bulk_save_cache_limit = 500 # must be smaller than the uuid count
|
25
32
|
end
|
26
33
|
|
27
|
-
|
34
|
+
def connection
|
35
|
+
server.connection
|
36
|
+
end
|
37
|
+
|
38
|
+
# A URI object for the exact location of this database
|
39
|
+
def uri
|
40
|
+
server.uri + path
|
41
|
+
end
|
42
|
+
alias root uri
|
28
43
|
|
29
|
-
#
|
44
|
+
# String of #root
|
30
45
|
def to_s
|
31
|
-
|
46
|
+
uri.to_s
|
32
47
|
end
|
33
48
|
|
34
49
|
# GET the database info from CouchDB
|
35
50
|
def info
|
36
|
-
|
51
|
+
connection.get path
|
37
52
|
end
|
38
53
|
|
39
54
|
# Compact the database, removing old document revisions and optimizing space use.
|
40
55
|
def compact!
|
41
|
-
|
56
|
+
connection.post "#{path}/_compact"
|
42
57
|
end
|
43
58
|
|
44
59
|
# Create the database
|
45
60
|
def create!
|
46
|
-
bool = server.create_db(
|
61
|
+
bool = server.create_db(path) rescue false
|
47
62
|
bool && true
|
48
63
|
end
|
49
64
|
|
@@ -51,7 +66,7 @@ module CouchRest
|
|
51
66
|
def recreate!
|
52
67
|
delete!
|
53
68
|
create!
|
54
|
-
rescue
|
69
|
+
rescue CouchRest::NotFound
|
55
70
|
ensure
|
56
71
|
create!
|
57
72
|
end
|
@@ -69,7 +84,7 @@ module CouchRest
|
|
69
84
|
# DELETE the database itself. This is not undoable and could be rather
|
70
85
|
# catastrophic. Use with care!
|
71
86
|
def delete!
|
72
|
-
|
87
|
+
connection.delete path
|
73
88
|
end
|
74
89
|
|
75
90
|
|
@@ -78,8 +93,8 @@ module CouchRest
|
|
78
93
|
# GET a document from CouchDB, by id. Returns a Document or Design.
|
79
94
|
def get(id, params = {})
|
80
95
|
slug = escape_docid(id)
|
81
|
-
url = CouchRest.paramify_url("#{
|
82
|
-
result =
|
96
|
+
url = CouchRest.paramify_url("#{path}/#{slug}", params)
|
97
|
+
result = connection.get(url)
|
83
98
|
return result unless result.is_a?(Hash)
|
84
99
|
doc = if /^_design/ =~ result["_id"]
|
85
100
|
Design.new(result)
|
@@ -115,6 +130,7 @@ module CouchRest
|
|
115
130
|
if doc['_attachments']
|
116
131
|
doc['_attachments'] = encode_attachments(doc['_attachments'])
|
117
132
|
end
|
133
|
+
|
118
134
|
if bulk
|
119
135
|
@bulk_save_cache << doc
|
120
136
|
bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
|
@@ -125,21 +141,17 @@ module CouchRest
|
|
125
141
|
result = if doc['_id']
|
126
142
|
slug = escape_docid(doc['_id'])
|
127
143
|
begin
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
rescue
|
144
|
+
doc_path = "#{path}/#{slug}"
|
145
|
+
doc_path << "?batch=ok" if batch
|
146
|
+
connection.put doc_path, doc
|
147
|
+
rescue CouchRest::NotFound
|
132
148
|
puts "resource not found when saving even though an id was passed"
|
133
|
-
slug = doc['_id'] =
|
134
|
-
|
149
|
+
slug = doc['_id'] = server.next_uuid
|
150
|
+
connection.put "#{path}/#{slug}", doc
|
135
151
|
end
|
136
152
|
else
|
137
|
-
|
138
|
-
|
139
|
-
CouchRest.put "#{@root}/#{slug}", doc
|
140
|
-
rescue #old version of couchdb
|
141
|
-
CouchRest.post @root, doc
|
142
|
-
end
|
153
|
+
slug = doc['_id'] = @server.next_uuid
|
154
|
+
connection.put "#{path}/#{slug}", doc
|
143
155
|
end
|
144
156
|
if result['ok']
|
145
157
|
doc['_id'] = result['id']
|
@@ -172,7 +184,7 @@ module CouchRest
|
|
172
184
|
ids, noids = docs.partition{|d|d['_id']}
|
173
185
|
uuid_count = [noids.length, @server.uuid_batch_count].max
|
174
186
|
noids.each do |doc|
|
175
|
-
nextid =
|
187
|
+
nextid = server.next_uuid(uuid_count) rescue nil
|
176
188
|
doc['_id'] = nextid if nextid
|
177
189
|
end
|
178
190
|
end
|
@@ -180,7 +192,7 @@ module CouchRest
|
|
180
192
|
if all_or_nothing
|
181
193
|
request_body[:all_or_nothing] = true
|
182
194
|
end
|
183
|
-
|
195
|
+
connection.post "#{path}/_bulk_docs", request_body
|
184
196
|
end
|
185
197
|
alias :bulk_delete :bulk_save
|
186
198
|
|
@@ -197,7 +209,7 @@ module CouchRest
|
|
197
209
|
return {'ok' => true} # Mimic the non-deferred version
|
198
210
|
end
|
199
211
|
slug = escape_docid(doc['_id'])
|
200
|
-
|
212
|
+
connection.delete "#{path}/#{slug}?rev=#{doc['_rev']}"
|
201
213
|
end
|
202
214
|
|
203
215
|
# COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
|
@@ -211,7 +223,7 @@ module CouchRest
|
|
211
223
|
else
|
212
224
|
dest
|
213
225
|
end
|
214
|
-
|
226
|
+
connection.copy "#{path}/#{slug}", destination
|
215
227
|
end
|
216
228
|
|
217
229
|
# Updates the given doc by yielding the current state of the doc
|
@@ -227,7 +239,7 @@ module CouchRest
|
|
227
239
|
yield doc
|
228
240
|
begin
|
229
241
|
resp = self.save_doc doc
|
230
|
-
rescue
|
242
|
+
rescue CouchRest::RequestFailed => e
|
231
243
|
if e.http_code == 409 # Update collision
|
232
244
|
update_limit -= 1
|
233
245
|
last_fail = e
|
@@ -247,23 +259,21 @@ module CouchRest
|
|
247
259
|
# Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
|
248
260
|
# paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
|
249
261
|
def view(name, params = {}, payload = {}, &block)
|
262
|
+
opts = {}
|
250
263
|
params = params.dup
|
251
264
|
payload['keys'] = params.delete(:keys) if params[:keys]
|
265
|
+
|
266
|
+
# Continuous feeds need to be parsed differently
|
267
|
+
opts[:continuous] = true if params['feed'] == 'continuous'
|
268
|
+
|
252
269
|
# Try recognising the name, otherwise assume already prepared
|
253
270
|
view_path = name_to_view_path(name)
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
else
|
259
|
-
@streamer.get url, &block
|
260
|
-
end
|
271
|
+
req_path = CouchRest.paramify_url("#{path}/#{view_path}", params)
|
272
|
+
|
273
|
+
if payload.empty?
|
274
|
+
connection.get req_path, opts, &block
|
261
275
|
else
|
262
|
-
|
263
|
-
CouchRest.post url, payload
|
264
|
-
else
|
265
|
-
CouchRest.get url
|
266
|
-
end
|
276
|
+
connection.post req_path, payload, opts, &block
|
267
277
|
end
|
268
278
|
end
|
269
279
|
|
@@ -310,37 +320,33 @@ module CouchRest
|
|
310
320
|
|
311
321
|
# GET an attachment directly from CouchDB
|
312
322
|
def fetch_attachment(doc, name)
|
313
|
-
|
314
|
-
CouchRest.get uri, :raw => true
|
323
|
+
connection.get path_for_attachment(doc, name), :raw => true
|
315
324
|
end
|
316
325
|
|
317
|
-
# PUT an attachment directly to CouchDB
|
326
|
+
# PUT an attachment directly to CouchDB, expects an IO object, or a string
|
327
|
+
# that will be converted to a StringIO in the 'file' parameter.
|
318
328
|
def put_attachment(doc, name, file, options = {})
|
319
|
-
|
320
|
-
|
321
|
-
CouchRest.put(uri, file, options.merge(:raw => true))
|
329
|
+
file = StringIO.new(file) if file.is_a?(String)
|
330
|
+
connection.put path_for_attachment(doc, name), file, options
|
322
331
|
end
|
323
332
|
|
324
333
|
# DELETE an attachment directly from CouchDB
|
325
334
|
def delete_attachment(doc, name, force=false)
|
326
|
-
|
327
|
-
# this needs a rev
|
335
|
+
attach_path = path_for_attachment(doc, name)
|
328
336
|
begin
|
329
|
-
|
337
|
+
connection.delete(attach_path)
|
330
338
|
rescue Exception => error
|
331
339
|
if force
|
332
340
|
# get over a 409
|
333
341
|
doc = get(doc['_id'])
|
334
|
-
|
335
|
-
|
342
|
+
attach_path = path_for_attachment(doc, name)
|
343
|
+
connection.delete(attach_path)
|
336
344
|
else
|
337
345
|
error
|
338
346
|
end
|
339
347
|
end
|
340
348
|
end
|
341
349
|
|
342
|
-
|
343
|
-
|
344
350
|
private
|
345
351
|
|
346
352
|
def replicate(other_db, continuous, options)
|
@@ -349,32 +355,22 @@ module CouchRest
|
|
349
355
|
doc_ids = options.delete(:doc_ids)
|
350
356
|
payload = options
|
351
357
|
if options.has_key?(:target)
|
352
|
-
payload[:source] = other_db.root
|
358
|
+
payload[:source] = other_db.root.to_s
|
353
359
|
else
|
354
|
-
payload[:target] = other_db.root
|
360
|
+
payload[:target] = other_db.root.to_s
|
355
361
|
end
|
356
362
|
payload[:continuous] = continuous
|
357
363
|
payload[:doc_ids] = doc_ids if doc_ids
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
def uri_for_attachment(doc, name)
|
362
|
-
if doc.is_a?(String)
|
363
|
-
puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
|
364
|
-
docid = doc
|
365
|
-
rev = nil
|
366
|
-
else
|
367
|
-
docid = doc['_id']
|
368
|
-
rev = doc['_rev']
|
369
|
-
end
|
370
|
-
docid = escape_docid(docid)
|
371
|
-
name = CGI.escape(name)
|
372
|
-
rev = "?rev=#{doc['_rev']}" if rev
|
373
|
-
"/#{docid}/#{name}#{rev}"
|
364
|
+
|
365
|
+
# Use a short lived request here
|
366
|
+
connection.post "_replicate", payload
|
374
367
|
end
|
375
368
|
|
376
|
-
def
|
377
|
-
|
369
|
+
def path_for_attachment(doc, name)
|
370
|
+
docid = escape_docid(doc['_id'])
|
371
|
+
name = CGI.escape(name)
|
372
|
+
rev = doc['_rev'] ? "?rev=#{doc['_rev']}" : ''
|
373
|
+
"#{path}/#{docid}/#{name}#{rev}"
|
378
374
|
end
|
379
375
|
|
380
376
|
def escape_docid id
|
data/lib/couchrest/design.rb
CHANGED
@@ -0,0 +1,108 @@
|
|
1
|
+
#
|
2
|
+
# CouchRest Exception Handling
|
3
|
+
#
|
4
|
+
# Restricted set of HTTP error response we'd expect from a CouchDB server. If we don't have a specific error handler,
|
5
|
+
# a generic Exception will be returned with the #http_code attribute set.
|
6
|
+
#
|
7
|
+
# Implementation based on [rest-client exception handling](https://github.com/rest-client/rest-client/blob/master/lib/restclient/exceptions.rb).
|
8
|
+
#
|
9
|
+
# In general, exceptions in the `CouchRest` scope are only generated by the couchrest library,
|
10
|
+
# exceptions generated by other libraries will not be re-mapped.
|
11
|
+
#
|
12
|
+
module CouchRest
|
13
|
+
|
14
|
+
STATUSES = {
|
15
|
+
200 => 'OK',
|
16
|
+
201 => 'Created',
|
17
|
+
202 => 'Accepted',
|
18
|
+
|
19
|
+
304 => 'Not Modified',
|
20
|
+
|
21
|
+
400 => 'Bad Request',
|
22
|
+
401 => 'Unauthorized',
|
23
|
+
403 => 'Forbidden',
|
24
|
+
404 => 'Not Found',
|
25
|
+
405 => 'Method Not Allowed',
|
26
|
+
406 => 'Not Acceptable',
|
27
|
+
409 => 'Conflict',
|
28
|
+
412 => 'Precondition Failed',
|
29
|
+
415 => 'Unsupported Media Type',
|
30
|
+
416 => 'Requested Range Not Satisfiable',
|
31
|
+
417 => 'Expectation Failed',
|
32
|
+
|
33
|
+
500 => 'Internal Server Error',
|
34
|
+
}
|
35
|
+
|
36
|
+
# This is the base CouchRest exception class. Rescue it if you want to
|
37
|
+
# catch any exception that your request might raise.
|
38
|
+
# You can get the status code by e.http_code, or see anything about the
|
39
|
+
# response via e.response.
|
40
|
+
# For example, the entire result body (which is
|
41
|
+
# probably an HTML error page) is e.response.
|
42
|
+
class Exception < RuntimeError
|
43
|
+
attr_accessor :response
|
44
|
+
attr_writer :message
|
45
|
+
|
46
|
+
def initialize response = nil
|
47
|
+
@response = response
|
48
|
+
@message = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def http_code
|
52
|
+
# return integer for compatibility
|
53
|
+
@response.status if @response
|
54
|
+
end
|
55
|
+
|
56
|
+
def http_headers
|
57
|
+
@response.headers if @response
|
58
|
+
end
|
59
|
+
|
60
|
+
def http_body
|
61
|
+
@response.body if @response
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#{message}: #{http_body}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
inspect
|
70
|
+
end
|
71
|
+
|
72
|
+
def message
|
73
|
+
@message || self.class.default_message
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.default_message
|
77
|
+
self.name
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The request failed with an error code not managed by the code
|
82
|
+
class RequestFailed < Exception
|
83
|
+
def message
|
84
|
+
"HTTP status code #{http_code}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
message
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module Exceptions
|
93
|
+
EXCEPTIONS_MAP = {}
|
94
|
+
end
|
95
|
+
|
96
|
+
STATUSES.each_pair do |code, message|
|
97
|
+
klass = Class.new(RequestFailed) do
|
98
|
+
send(:define_method, :message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
|
99
|
+
end
|
100
|
+
klass_constant = const_set message.delete(' \-\''), klass
|
101
|
+
Exceptions::EXCEPTIONS_MAP[code] = klass_constant
|
102
|
+
end
|
103
|
+
|
104
|
+
# Error handler for broken connections, mainly used by streamer
|
105
|
+
class ServerBrokeConnection < ::Exception
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module CouchRest
|
2
|
+
|
3
|
+
# CouchRest Stream Row Parser
|
4
|
+
#
|
5
|
+
# Will parse a stream containing a standard CouchDB response including rows.
|
6
|
+
# Allows each row to be parsed individually, and provided in a block for
|
7
|
+
# efficient memory usage.
|
8
|
+
#
|
9
|
+
# The StreamRowParser#parse method expects to be called multiple times with segments
|
10
|
+
# of data, typcially provided by the Net::HTTPResponse#read_body method.
|
11
|
+
#
|
12
|
+
# Data will be cached until usable objects can be extracted in rows and provied in the block.
|
13
|
+
#
|
14
|
+
class StreamRowParser
|
15
|
+
|
16
|
+
# String containing the fields provided before and after the rows.
|
17
|
+
attr_accessor :header
|
18
|
+
|
19
|
+
# The row level at which we expect to receive "rows" of data.
|
20
|
+
# Typically this will be 0 for contious feeds, and 1 for most other users.
|
21
|
+
attr_reader :row_level
|
22
|
+
|
23
|
+
# Instantiate a new StreamRowParser with the mode set according to the type of data.
|
24
|
+
# The supported modes are:
|
25
|
+
#
|
26
|
+
# * `:array` - objects are contianed in a data array, the default.
|
27
|
+
# * `:feed` - each row of the stream is an object, like in continuous changes feeds.
|
28
|
+
#
|
29
|
+
def initialize(mode = :array)
|
30
|
+
@header = ""
|
31
|
+
@data = ""
|
32
|
+
@string = false
|
33
|
+
@escape = false
|
34
|
+
|
35
|
+
@row_level = mode == :array ? 1 : 0
|
36
|
+
@in_rows = false
|
37
|
+
@obj_level = 0
|
38
|
+
@obj_close = false
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(segment, &block)
|
42
|
+
@in_rows = true if @row_level == 0
|
43
|
+
segment.each_char do |c|
|
44
|
+
if @string
|
45
|
+
# Inside a string, handling escaping and closure
|
46
|
+
if @escape
|
47
|
+
@escape = false
|
48
|
+
else
|
49
|
+
if c == '"'
|
50
|
+
@string = false
|
51
|
+
elsif c == '\\'
|
52
|
+
@escape = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
else
|
56
|
+
# Inside an object
|
57
|
+
@obj_close = false
|
58
|
+
if @obj_level == @row_level && c == "[" # start of rows
|
59
|
+
@in_rows = true
|
60
|
+
elsif @obj_level == @row_level && c == "]" # end of rows
|
61
|
+
@in_rows = false
|
62
|
+
elsif c == "{" # object
|
63
|
+
@obj_level += 1
|
64
|
+
elsif c == "}" # object end
|
65
|
+
@obj_level -= 1
|
66
|
+
@obj_close = true
|
67
|
+
elsif c == '"'
|
68
|
+
@string = true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Append data
|
73
|
+
if @row_level > 0
|
74
|
+
if @obj_level == 0 || (@obj_level == @row_level && !@obj_close)
|
75
|
+
@header << c unless @in_rows && (c == ',' || c == ' ' || c == "\n") # skip row whitespace
|
76
|
+
else
|
77
|
+
@data << c
|
78
|
+
end
|
79
|
+
else
|
80
|
+
@data << c
|
81
|
+
end
|
82
|
+
|
83
|
+
# Determine if we need to trigger an event
|
84
|
+
if @obj_close && @obj_level == @row_level
|
85
|
+
block.call(@data)
|
86
|
+
@data = ""
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|