couchbase 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,6 +21,7 @@ require 'ext/multi_json_fix'
21
21
  require 'yaji'
22
22
  require 'uri'
23
23
  require 'couchbase_ext'
24
+ require 'couchbase/constants'
24
25
  require 'couchbase/utils'
25
26
  require 'couchbase/bucket'
26
27
  require 'couchbase/view_row'
@@ -109,7 +109,7 @@ module Couchbase
109
109
  if obj['doc']
110
110
  obj['doc']['value'] = obj['doc'].delete('json')
111
111
  end
112
- doc = ViewRow.wrap(self, obj)
112
+ doc = DesignDoc.wrap(self, obj)
113
113
  key = doc.id.sub(/^_design\//, '')
114
114
  next if self.environment == :production && key =~ /dev_/
115
115
  docmap[key] = doc
@@ -0,0 +1,12 @@
1
+ module Couchbase
2
+ module Constants # :nodoc:
3
+ S_ID = 'id'.freeze
4
+ S_DOC = 'doc'.freeze
5
+ S_VALUE = 'value'.freeze
6
+ S_META = 'meta'.freeze
7
+ S_FLAGS = 'flags'.freeze
8
+ S_CAS = 'cas'.freeze
9
+ S_KEY = 'key'.freeze
10
+ S_IS_LAST = Object.new.freeze
11
+ end
12
+ end
@@ -17,5 +17,5 @@
17
17
 
18
18
  # Couchbase ruby client
19
19
  module Couchbase
20
- VERSION = "1.2.1"
20
+ VERSION = "1.2.2"
21
21
  end
@@ -47,8 +47,9 @@ module Couchbase
47
47
  str = super
48
48
  if @type || @reason
49
49
  str.sub(/ \(/, ": #{[@type, @reason].compact.join(": ")} (")
50
+ else
51
+ str
50
52
  end
51
- str
52
53
  end
53
54
  end
54
55
  end
@@ -58,6 +59,88 @@ module Couchbase
58
59
  # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html
59
60
  class View
60
61
  include Enumerable
62
+ include Constants
63
+
64
+ class ArrayWithTotalRows < Array # :nodoc:
65
+ attr_accessor :total_rows
66
+ alias total_entries total_rows
67
+ end
68
+
69
+ class AsyncHelper # :nodoc:
70
+ include Constants
71
+ EMPTY = []
72
+
73
+ def initialize(wrapper_class, bucket, include_docs, quiet, block)
74
+ @wrapper_class = wrapper_class
75
+ @bucket = bucket
76
+ @block = block
77
+ @quiet = quiet
78
+ @include_docs = include_docs
79
+ @queue = []
80
+ @first = @shift = 0
81
+ @completed = false
82
+ end
83
+
84
+ # Register object in the emitter.
85
+ def push(obj)
86
+ if @include_docs
87
+ @queue << obj
88
+ @bucket.get(obj[S_ID], :extended => true, :quiet => @quiet) do |res|
89
+ obj[S_DOC] = {
90
+ S_VALUE => res.value,
91
+ S_META => {
92
+ S_ID => obj[S_ID],
93
+ S_FLAGS => res.flags,
94
+ S_CAS => res.cas
95
+ }
96
+ }
97
+ check_for_ready_documents
98
+ end
99
+ else
100
+ old_obj = @queue.shift
101
+ @queue << obj
102
+ block_call(old_obj) if old_obj
103
+ end
104
+ end
105
+
106
+ def complete!
107
+ if @include_docs
108
+ @completed = true
109
+ check_for_ready_documents
110
+ elsif !@queue.empty?
111
+ obj = @queue.shift
112
+ obj[S_IS_LAST] = true
113
+ block_call obj
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def block_call(obj)
120
+ @block.call @wrapper_class.wrap(@bucket, obj)
121
+ end
122
+
123
+ def check_for_ready_documents
124
+ shift = @shift
125
+ queue = @queue
126
+ save_last = @completed ? 0 : 1
127
+ while @first < queue.size + shift - save_last
128
+ obj = queue[@first - shift]
129
+ break unless obj[S_DOC]
130
+ queue[@first - shift] = nil
131
+ @first += 1
132
+ if @completed && @first == queue.size + shift
133
+ obj[S_IS_LAST] = true
134
+ end
135
+ block_call obj
136
+ end
137
+ if @first - shift > queue.size / 2
138
+ queue[0, @first - shift] = EMPTY
139
+ @shift = @first
140
+ end
141
+ end
142
+
143
+ end
61
144
 
62
145
  attr_reader :params
63
146
 
@@ -112,6 +195,16 @@ module Couchbase
112
195
  fetch(params) {|doc| yield(doc)}
113
196
  end
114
197
 
198
+ def first(params = {})
199
+ params = params.merge(:limit => 1)
200
+ fetch(params).first
201
+ end
202
+
203
+ def take(n, params = {})
204
+ params = params.merge(:limit => n)
205
+ fetch(params)
206
+ end
207
+
115
208
  # Registers callback function for handling error objects in view
116
209
  # results stream.
117
210
  #
@@ -259,93 +352,150 @@ module Couchbase
259
352
  # doc.recent_posts_with_comments(:start_key => [post_id, 0],
260
353
  # :end_key => [post_id, 1],
261
354
  # :include_docs => true)
262
- def fetch(params = {})
355
+ def fetch(params = {}, &block)
263
356
  params = @params.merge(params)
357
+ include_docs = params.delete(:include_docs)
358
+ quiet = params.delete(:quiet){ true }
359
+
264
360
  options = {:chunked => true, :extended => true, :type => :view}
265
361
  if body = params.delete(:body)
266
362
  body = MultiJson.dump(body) unless body.is_a?(String)
267
363
  options.update(:body => body, :method => params.delete(:method) || :post)
268
364
  end
269
- include_docs = params.delete(:include_docs)
270
- quiet = true
271
- if params.has_key?(:quiet)
272
- quiet = params.delete(:quiet)
273
- end
274
365
  path = Utils.build_query(@endpoint, params)
275
366
  request = @bucket.make_http_request(path, options)
276
- res = []
367
+
368
+ if @bucket.async?
369
+ if block
370
+ fetch_async(request, include_docs, quiet, block)
371
+ end
372
+ else
373
+ fetch_sync(request, include_docs, quiet, block)
374
+ end
375
+ end
376
+
377
+ # Method for fetching asynchronously all rows and passing array to callback
378
+ #
379
+ # Parameters are same as for {View#fetch} method, but callback is called for whole set for
380
+ # rows instead of one by each.
381
+ #
382
+ # @example
383
+ # con.run do
384
+ # doc.recent_posts.fetch_all do |posts|
385
+ # do_something_with_all_posts(posts)
386
+ # end
387
+ # end
388
+ def fetch_all(params = {}, &block)
389
+ return fetch(params) unless @bucket.async?
390
+ raise ArgumentError, "Block needed for fetch_all in async mode" unless block
391
+
392
+ all = []
393
+ fetch(params) do |row|
394
+ all << row
395
+ if row.last?
396
+ @bucket.create_timer(0) { block.call(all) }
397
+ end
398
+ end
399
+ end
400
+
401
+
402
+ # Returns a string containing a human-readable representation of the {View}
403
+ #
404
+ # @return [String]
405
+ def inspect
406
+ %(#<#{self.class.name}:#{self.object_id} @endpoint=#{@endpoint.inspect} @params=#{@params.inspect}>)
407
+ end
408
+
409
+ private
410
+
411
+ def send_error(*args)
412
+ if @on_error
413
+ @on_error.call(*args.take(2))
414
+ else
415
+ raise Error::View.new(*args)
416
+ end
417
+ end
418
+
419
+ def fetch_async(request, include_docs, quiet, block)
420
+ filter = ["/rows/", "/errors/"]
421
+ parser = YAJI::Parser.new(:filter => filter, :with_path => true)
422
+ helper = AsyncHelper.new(@wrapper_class, @bucket, include_docs, quiet, block)
423
+
277
424
  request.on_body do |chunk|
278
- res << chunk
279
- request.pause if chunk.value.nil? || chunk.error
425
+ if chunk.success?
426
+ parser << chunk.value if chunk.value
427
+ helper.complete! if chunk.completed?
428
+ else
429
+ send_error("http_error", chunk.error)
430
+ end
431
+ end
432
+
433
+ parser.on_object do |path, obj|
434
+ case path
435
+ when "/errors/"
436
+ from, reason = obj["from"], obj["reason"]
437
+ send_error(from, reason)
438
+ else
439
+ helper.push(obj)
440
+ end
280
441
  end
442
+
443
+ request.perform
444
+ nil
445
+ end
446
+
447
+ def fetch_sync(request, include_docs, quiet, block)
448
+ res = []
281
449
  filter = ["/rows/", "/errors/"]
282
- filter << "/total_rows" unless block_given?
450
+ unless block
451
+ filter << "/total_rows"
452
+ docs = ArrayWithTotalRows.new
453
+ end
283
454
  parser = YAJI::Parser.new(:filter => filter, :with_path => true)
284
- docs = []
455
+ last_chunk = nil
456
+
457
+ request.on_body do |chunk|
458
+ last_chunk = chunk
459
+ res << chunk.value if chunk.success?
460
+ end
461
+
285
462
  parser.on_object do |path, obj|
286
463
  case path
287
464
  when "/total_rows"
288
465
  # if total_rows key present, save it and take next object
289
- docs.instance_eval("def total_rows; #{obj}; end")
466
+ docs.total_rows = obj
290
467
  when "/errors/"
291
468
  from, reason = obj["from"], obj["reason"]
292
- if @on_error
293
- @on_error.call(from, reason)
294
- else
295
- raise Error::View.new(from, reason)
296
- end
469
+ send_error(from, reason)
297
470
  else
298
471
  if include_docs
299
- val, flags, cas = @bucket.get(obj['id'], :extended => true, :quiet => quiet)
300
- obj['doc'] = {
301
- 'value' => val,
302
- 'meta' => {
303
- 'id' => obj['id'],
304
- 'flags' => flags,
305
- 'cas' => cas
472
+ val, flags, cas = @bucket.get(obj[S_ID], :extended => true, :quiet => quiet)
473
+ obj[S_DOC] = {
474
+ S_VALUE => val,
475
+ S_META => {
476
+ S_ID => obj[S_ID],
477
+ S_FLAGS => flags,
478
+ S_CAS => cas
306
479
  }
307
480
  }
308
481
  end
309
- if block_given?
310
- yield @wrapper_class.wrap(@bucket, obj)
311
- else
312
- docs << @wrapper_class.wrap(@bucket, obj)
313
- end
482
+ doc = @wrapper_class.wrap(@bucket, obj)
483
+ block ? block.call(doc) : docs << doc
314
484
  end
315
485
  end
316
- # run event loop until the terminating chunk will be found
317
- # last_res variable keeps latest known chunk of the result
318
- last_res = nil
319
- while true
320
- # feed response received chunks to the parser
321
- while r = res.shift
322
- if r.error
323
- if @on_error
324
- @on_error.call("http_error", r.error)
325
- break
326
- else
327
- raise Error::View.new("http_error", r.error, nil)
328
- end
329
- end
330
- last_res = r
331
- parser << r.value
332
- end
333
- if last_res.nil? || !last_res.completed? # shall we run the event loop?
334
- request.continue
335
- else
336
- break
486
+
487
+ request.continue
488
+
489
+ if last_chunk.success?
490
+ while value = res.shift
491
+ parser << value
337
492
  end
493
+ else
494
+ send_error("http_error", last_chunk.error, nil)
338
495
  end
339
- # return nil for call with block
340
- block_given? ? nil : docs
341
- end
342
-
343
496
 
344
- # Returns a string containing a human-readable representation of the {View}
345
- #
346
- # @return [String]
347
- def inspect
348
- %(#<#{self.class.name}:#{self.object_id} @endpoint=#{@endpoint.inspect} @params=#{@params.inspect}>)
497
+ # return nil for call with block
498
+ docs
349
499
  end
350
500
  end
351
501
  end
@@ -20,11 +20,11 @@ module Couchbase
20
20
  #
21
21
  # @since 1.2.0
22
22
  #
23
- # It behaves like Hash, but also defines special methods for each view if
24
- # the documnent considered as Design document.
23
+ # It behaves like Hash for document included into row, and has access methods to row data as well.
25
24
  #
26
25
  # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-datastore.html
27
26
  class ViewRow
27
+ include Constants
28
28
 
29
29
  # Undefine as much methods as we can to free names for views
30
30
  instance_methods.each do |m|
@@ -84,26 +84,11 @@ module Couchbase
84
84
  # @return [Hash]
85
85
  attr_accessor :meta
86
86
 
87
- # The list of views defined or empty array
88
- #
89
- # @since 1.2.0
90
- #
91
- # @return [Array<View>]
92
- attr_accessor :views
93
-
94
- # The list of spatial views defined or empty array
95
- #
96
- # @since 1.2.0
97
- #
98
- # @return [Array<View>]
99
- attr_accessor :spatial
100
-
101
87
  # Initialize the document instance
102
88
  #
103
89
  # @since 1.2.0
104
90
  #
105
- # It takes reference to the bucket, data hash. It will define view
106
- # methods if the data object looks like design document.
91
+ # It takes reference to the bucket, data hash.
107
92
  #
108
93
  # @param [Couchbase::Bucket] bucket the reference to connection
109
94
  # @param [Hash] data the data hash, which was built from JSON document
@@ -111,37 +96,14 @@ module Couchbase
111
96
  def initialize(bucket, data)
112
97
  @bucket = bucket
113
98
  @data = data
114
- @key = data['key']
115
- @value = data['value']
116
- if data['doc']
117
- @meta = data['doc']['meta']
118
- @doc = data['doc']['value']
119
- end
120
- @id = data['id'] || @meta && @meta['id']
121
- @views = []
122
- @spatial = []
123
- if design_doc?
124
- if @doc.has_key?('views')
125
- @doc['views'].each do |name, _|
126
- @views << name
127
- self.instance_eval <<-EOV, __FILE__, __LINE__ + 1
128
- def #{name}(params = {})
129
- View.new(@bucket, "\#{@id}/_view/#{name}", params)
130
- end
131
- EOV
132
- end
133
- end
134
- if @doc.has_key?('spatial')
135
- @doc['spatial'].each do |name, _|
136
- @spatial << name
137
- self.instance_eval <<-EOV, __FILE__, __LINE__ + 1
138
- def #{name}(params = {})
139
- View.new(@bucket, "\#{@id}/_spatial/#{name}", params)
140
- end
141
- EOV
142
- end
143
- end
99
+ @key = data[S_KEY]
100
+ @value = data[S_VALUE]
101
+ if data[S_DOC]
102
+ @meta = data[S_DOC][S_META]
103
+ @doc = data[S_DOC][S_VALUE]
144
104
  end
105
+ @id = data[S_ID] || @meta && @meta[S_ID]
106
+ @last = data.delete(S_IS_LAST) || false
145
107
  end
146
108
 
147
109
  # Wraps data hash into ViewRow instance
@@ -156,7 +118,7 @@ module Couchbase
156
118
  #
157
119
  # @return [ViewRow]
158
120
  def self.wrap(bucket, data)
159
- ViewRow.new(bucket, data)
121
+ self.new(bucket, data)
160
122
  end
161
123
 
162
124
  # Get attribute of the document
@@ -198,33 +160,113 @@ module Couchbase
198
160
  @doc[key] = value
199
161
  end
200
162
 
201
- # Check if the document is design
163
+ # Signals if this row is last in a stream
202
164
  #
203
- # @since 1.2.0
165
+ # @since 1.2.1
204
166
  #
205
- # @return [true, false]
206
- def design_doc?
207
- !!(@doc && @id =~ %r(_design/))
167
+ # @return [true, false] +true+ if this row is last in a stream
168
+ def last?
169
+ @last
170
+ end
171
+
172
+ def inspect
173
+ desc = "#<#{self.class.name}:#{self.object_id}"
174
+ [:@id, :@key, :@value, :@doc, :@meta].each do |iv|
175
+ desc << " #{iv}=#{instance_variable_get(iv).inspect}"
176
+ end
177
+ desc << ">"
178
+ desc
208
179
  end
180
+ end
181
+
182
+ # This class encapsulates information about design docs
183
+ #
184
+ # @since 1.2.1
185
+ #
186
+ # It is subclass of ViewRow, but also gives access to view creation through method_missing
187
+ #
188
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-datastore.html
189
+ class DesignDoc < ViewRow
190
+ # It isn't allowed to change design document ID after
191
+ # initialization
192
+ undef id=
193
+
194
+ # Initialize the design doc instance
195
+ #
196
+ # @since 1.2.1
197
+ #
198
+ # It takes reference to the bucket, data hash. It will define view
199
+ # methods if the data object looks like design document.
200
+ #
201
+ # @param [Couchbase::Bucket] bucket the reference to connection
202
+ # @param [Hash] data the data hash, which was built from JSON document
203
+ # representation
204
+ def initialize(bucket, data)
205
+ super
206
+ @all_views = {}
207
+ @views = @doc.has_key?('views') ? @doc['views'].keys : []
208
+ @spatial = @doc.has_key?('spatial') ? @doc['spatial'].keys : []
209
+ @views.each{|name| @all_views[name] = "#{@id}/_view/#{name}"}
210
+ @spatial.each{|name| @all_views[name] = "#{@id}/_spatial/#{name}"}
211
+ end
212
+
213
+ def method_missing(meth, *args)
214
+ if path = @all_views[meth.to_s]
215
+ View.new(@bucket, path, *args)
216
+ else
217
+ super
218
+ end
219
+ end
220
+
221
+ def respond_to?(meth, *args)
222
+ if @all_views[meth.to_s]
223
+ true
224
+ else
225
+ super
226
+ end
227
+ end
228
+
229
+ def method(meth, *args)
230
+ if path = @all_views[meth.to_s]
231
+ lambda{|*p| View.new(@bucket, path, *p)}
232
+ else
233
+ super
234
+ end
235
+ end
236
+
237
+ # The list of views defined or empty array
238
+ #
239
+ # @since 1.2.1
240
+ #
241
+ # @return [Array<View>]
242
+ attr_accessor :views
243
+
244
+ # The list of spatial views defined or empty array
245
+ #
246
+ # @since 1.2.1
247
+ #
248
+ # @return [Array<View>]
249
+ attr_accessor :spatial
209
250
 
210
251
  # Check if the document has views defines
211
252
  #
212
- # @since 1.2.0
253
+ # @since 1.2.1
213
254
  #
214
- # @see ViewRow#views
255
+ # @see DesignDoc#views
215
256
  #
216
257
  # @return [true, false] +true+ if the document have views
217
258
  def has_views?
218
- !!(design_doc? && !@views.empty?)
259
+ !@views.empty?
219
260
  end
220
261
 
221
262
  def inspect
222
- desc = "#<#{self.class.name}:#{self.object_id} "
223
- desc << [:@id, :@key, :@value, :@doc, :@meta, :@views].map do |iv|
224
- "#{iv}=#{instance_variable_get(iv).inspect}"
225
- end.join(' ')
263
+ desc = "#<#{self.class.name}:#{self.object_id}"
264
+ [:@id, :@views, :@spatial].each do |iv|
265
+ desc << " #{iv}=#{instance_variable_get(iv).inspect}"
266
+ end
226
267
  desc << ">"
227
268
  desc
228
269
  end
270
+
229
271
  end
230
272
  end