couchrest 1.2.1 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|