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.
@@ -3,7 +3,17 @@ require "base64"
3
3
 
4
4
  module CouchRest
5
5
  class Database
6
- attr_reader :server, :host, :name, :root, :uri
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 = name
18
- @server = server
19
- @host = server.uri
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
- # == Database information and manipulation methods
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
- # returns the database's uri
44
+ # String of #root
30
45
  def to_s
31
- @root
46
+ uri.to_s
32
47
  end
33
48
 
34
49
  # GET the database info from CouchDB
35
50
  def info
36
- CouchRest.get @root
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
- CouchRest.post "#{@root}/_compact"
56
+ connection.post "#{path}/_compact"
42
57
  end
43
58
 
44
59
  # Create the database
45
60
  def create!
46
- bool = server.create_db(@name) rescue false
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 RestClient::ResourceNotFound
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
- CouchRest.delete @root
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("#{@root}/#{slug}", params)
82
- result = CouchRest.get(url)
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
- uri = "#{@root}/#{slug}"
129
- uri << "?batch=ok" if batch
130
- CouchRest.put uri, doc
131
- rescue RestClient::ResourceNotFound
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'] = @server.next_uuid
134
- CouchRest.put "#{@root}/#{slug}", doc
149
+ slug = doc['_id'] = server.next_uuid
150
+ connection.put "#{path}/#{slug}", doc
135
151
  end
136
152
  else
137
- begin
138
- slug = doc['_id'] = @server.next_uuid
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 = @server.next_uuid(uuid_count) rescue nil
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
- CouchRest.post "#{@root}/_bulk_docs", request_body
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
- CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
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
- CouchRest.copy "#{@root}/#{slug}", destination
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 RestClient::RequestFailed => e
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
- url = CouchRest.paramify_url "#{@root}/#{view_path}", params
255
- if block_given?
256
- if !payload.empty?
257
- @streamer.post url, payload, &block
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
- if !payload.empty?
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
- uri = url_for_attachment(doc, name)
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
- docid = escape_docid(doc['_id'])
320
- uri = url_for_attachment(doc, name)
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
- uri = url_for_attachment(doc, name)
327
- # this needs a rev
335
+ attach_path = path_for_attachment(doc, name)
328
336
  begin
329
- CouchRest.delete(uri)
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
- uri = url_for_attachment(doc, name)
335
- CouchRest.delete(uri)
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
- CouchRest.post "#{@host}/_replicate", payload
359
- end
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 url_for_attachment(doc, name)
377
- @root + uri_for_attachment(doc, name)
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
@@ -62,7 +62,7 @@ JAVASCRIPT
62
62
 
63
63
  # Provide information about the status of the design document.
64
64
  def info
65
- CouchRest.get "#{database.root}/#{id}/_info"
65
+ database.connection.get "#{database.uri}/#{id}/_info"
66
66
  end
67
67
 
68
68
  def save
@@ -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
@@ -1,6 +1,8 @@
1
1
  module CouchRest
2
2
  class Pager
3
+
3
4
  attr_accessor :db
5
+
4
6
  def initialize db
5
7
  @db = db
6
8
  end
@@ -100,4 +102,4 @@ module CouchRest
100
102
  end
101
103
 
102
104
  end
103
- end
105
+ 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