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.
@@ -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