jchris-couchrest 0.16 → 0.17.0

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.
@@ -20,7 +20,7 @@ module CouchRest
20
20
  @uri = @root = "#{host}/#{name}"
21
21
  @streamer = Streamer.new(self)
22
22
  @bulk_save_cache = []
23
- @bulk_save_cache_limit = 50
23
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
24
24
  end
25
25
 
26
26
  # returns the database's uri
@@ -61,12 +61,15 @@ module CouchRest
61
61
  # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
62
62
  def view(name, params = {}, &block)
63
63
  keys = params.delete(:keys)
64
- url = CouchRest.paramify_url "#{@uri}/_view/#{name}", params
64
+ name = name.split('/') # I think this will always be length == 2, but maybe not...
65
+ dname = name.shift
66
+ vname = name.join('/')
67
+ url = CouchRest.paramify_url "#{@uri}/_design/#{dname}/_view/#{vname}", params
65
68
  if keys
66
69
  CouchRest.post(url, {:keys => keys})
67
70
  else
68
71
  if block_given?
69
- @streamer.view(name, params, &block)
72
+ @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
70
73
  else
71
74
  CouchRest.get url
72
75
  end
@@ -74,13 +77,15 @@ module CouchRest
74
77
  end
75
78
 
76
79
  # GET a document from CouchDB, by id. Returns a Ruby Hash.
77
- def get(id)
80
+ def get(id, params = {})
78
81
  slug = escape_docid(id)
79
- hash = CouchRest.get("#{@uri}/#{slug}")
80
- doc = if /^_design/ =~ hash["_id"]
81
- Design.new(hash)
82
+ url = CouchRest.paramify_url("#{@uri}/#{slug}", params)
83
+ result = CouchRest.get(url)
84
+ return result unless result.is_a?(Hash)
85
+ doc = if /^_design/ =~ result["_id"]
86
+ Design.new(result)
82
87
  else
83
- Document.new(hash)
88
+ Document.new(result)
84
89
  end
85
90
  doc.database = self
86
91
  doc
@@ -1,14 +1,15 @@
1
1
  module CouchRest
2
2
  class Response < Hash
3
- def initialize(keys = {})
4
- keys.each do |k,v|
3
+ def initialize(pkeys = {})
4
+ pkeys ||= {}
5
+ pkeys.each do |k,v|
5
6
  self[k.to_s] = v
6
7
  end
7
8
  end
8
- def []= key, value
9
+ def []=(key, value)
9
10
  super(key.to_s, value)
10
11
  end
11
- def [] key
12
+ def [](key)
12
13
  super(key.to_s)
13
14
  end
14
15
  end
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+ class Upgrade
3
+ attr_accessor :olddb, :newdb, :dbname
4
+ def initialize dbname, old_couch, new_couch
5
+ @dbname = dbname
6
+ @olddb = old_couch.database dbname
7
+ @newdb = new_couch.database!(dbname)
8
+ @bulk_docs = []
9
+ end
10
+ def clone!
11
+ puts "#{dbname} - #{olddb.info['doc_count']} docs"
12
+ streamer = CouchRest::Streamer.new(olddb)
13
+ streamer.view("_all_docs_by_seq") do |row|
14
+ load_row_docs(row) if row
15
+ maybe_flush_bulks
16
+ end
17
+ flush_bulks!
18
+ end
19
+
20
+ private
21
+
22
+ def maybe_flush_bulks
23
+ flush_bulks! if (@bulk_docs.length > 99)
24
+ end
25
+
26
+ def flush_bulks!
27
+ url = CouchRest.paramify_url "#{@newdb.uri}/_bulk_docs", {:all_or_nothing => true}
28
+ puts "posting #{@bulk_docs.length} bulk docs to #{url}"
29
+ begin
30
+ CouchRest.post url, {:docs => @bulk_docs}
31
+ @bulk_docs = []
32
+ rescue Exception => e
33
+ puts e.response
34
+ raise e
35
+ end
36
+ end
37
+
38
+ def load_row_docs(row)
39
+ results = @olddb.get(row["id"], {:open_revs => "all", :attachments => true})
40
+ results.select{|r|r["ok"]}.each do |r|
41
+ doc = r["ok"]
42
+ if /^_/.match(doc["_id"]) && !/^_design/.match(doc["_id"])
43
+ puts "invalid docid #{doc["_id"]} -- trimming"
44
+ doc["_id"] = doc["_id"].sub('_','')
45
+ end
46
+ doc.delete('_rev')
47
+ @bulk_docs << doc
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,6 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), '..', 'support', 'class')
2
2
 
3
3
  # Extracted from ActiveSupport::Callbacks written by Yehuda Katz
4
+ # http://github.com/wycats/rails/raw/abstract_controller/activesupport/lib/active_support/new_callbacks.rb
4
5
  # http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
5
6
 
6
7
  module CouchRest
@@ -11,7 +12,7 @@ module CouchRest
11
12
  #
12
13
  # Example:
13
14
  # class Storage
14
- # include CouchRest::Callbacks
15
+ # include ActiveSupport::Callbacks
15
16
  #
16
17
  # define_callbacks :save
17
18
  # end
@@ -45,7 +46,7 @@ module CouchRest
45
46
  #
46
47
  # Example:
47
48
  # class Storage
48
- # include CouchRest::Callbacks
49
+ # include ActiveSupport::Callbacks
49
50
  #
50
51
  # define_callbacks :save
51
52
  #
@@ -85,8 +86,8 @@ module CouchRest
85
86
  klass.extend ClassMethods
86
87
  end
87
88
 
88
- def run_callbacks(kind, options = {})
89
- send("_run_#{kind}_callbacks")
89
+ def run_callbacks(kind, options = {}, &blk)
90
+ send("_run_#{kind}_callbacks", &blk)
90
91
  end
91
92
 
92
93
  class Callback
@@ -166,9 +167,13 @@ module CouchRest
166
167
 
167
168
  # This will supply contents for before and around filters, and no
168
169
  # contents for after filters (for the forward pass).
169
- def start(key = nil, object = nil)
170
+ def start(key = nil, options = {})
171
+ object, terminator = (options || {}).values_at(:object, :terminator)
172
+
170
173
  return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
171
174
 
175
+ terminator ||= false
176
+
172
177
  # options[0] is the compiled form of supplied conditions
173
178
  # options[1] is the "end" for the conditional
174
179
 
@@ -177,8 +182,14 @@ module CouchRest
177
182
  # if condition # before_save :filter_name, :if => :condition
178
183
  # filter_name
179
184
  # end
180
- [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
181
- elsif @compiled_options[0]
185
+ filter = <<-RUBY_EVAL
186
+ unless halted
187
+ result = #{@filter}
188
+ halted ||= (#{terminator})
189
+ end
190
+ RUBY_EVAL
191
+ [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
192
+ else
182
193
  # Compile around filters with conditions into proxy methods
183
194
  # that contain the conditions.
184
195
  #
@@ -196,8 +207,8 @@ module CouchRest
196
207
 
197
208
  name = "_conditional_callback_#{@kind}_#{next_id}"
198
209
  txt = <<-RUBY_EVAL
199
- def #{name}
200
- #{@compiled_options[0]}
210
+ def #{name}(halted)
211
+ #{@compiled_options[0] || "if true"} && !halted
201
212
  #{@filter} do
202
213
  yield self
203
214
  end
@@ -207,16 +218,16 @@ module CouchRest
207
218
  end
208
219
  RUBY_EVAL
209
220
  @klass.class_eval(txt)
210
- "#{name} do"
211
- else
212
- "#{@filter} do"
221
+ "#{name}(halted) do"
213
222
  end
214
223
  end
215
224
  end
216
225
 
217
226
  # This will supply contents for around and after filters, but not
218
227
  # before filters (for the backward pass).
219
- def end(key = nil, object = nil)
228
+ def end(key = nil, options = {})
229
+ object = (options || {})[:object]
230
+
220
231
  return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
221
232
 
222
233
  if @kind == :around || @kind == :after
@@ -299,7 +310,7 @@ module CouchRest
299
310
  # This method_missing is supplied to catch callbacks with keys and create
300
311
  # the appropriate callback for future use.
301
312
  def method_missing(meth, *args, &blk)
302
- if meth.to_s =~ /_run_(\w+)_(\w+)_(\w+)_callbacks/
313
+ if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/
303
314
  return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
304
315
  end
305
316
  super
@@ -307,20 +318,26 @@ module CouchRest
307
318
 
308
319
  # An Array with a compile method
309
320
  class CallbackChain < Array
310
- def compile(key = nil, object = nil)
321
+ def initialize(symbol)
322
+ @symbol = symbol
323
+ end
324
+
325
+ def compile(key = nil, options = {})
311
326
  method = []
327
+ method << "halted = false"
312
328
  each do |callback|
313
- method << callback.start(key, object)
329
+ method << callback.start(key, options)
314
330
  end
315
- method << "yield self"
331
+ method << "yield self if block_given?"
316
332
  reverse_each do |callback|
317
- method << callback.end(key, object)
333
+ method << callback.end(key, options)
318
334
  end
319
335
  method.compact.join("\n")
320
336
  end
321
337
 
322
338
  def clone(klass)
323
- CallbackChain.new(map {|c| c.clone(klass)})
339
+ chain = CallbackChain.new(@symbol)
340
+ chain.push(*map {|c| c.clone(klass)})
324
341
  end
325
342
  end
326
343
 
@@ -338,16 +355,18 @@ module CouchRest
338
355
  # The _run_save_callbacks method can optionally take a key, which
339
356
  # will be used to compile an optimized callback method for each
340
357
  # key. See #define_callbacks for more information.
341
- def _define_runner(symbol, str, options)
342
- self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
358
+ def _define_runner(symbol, str, options)
359
+ str = <<-RUBY_EVAL
343
360
  def _run_#{symbol}_callbacks(key = nil)
344
361
  if key
345
- send("_run_\#{self.class}_#{symbol}_\#{key}_callbacks") { yield }
362
+ send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? }
346
363
  else
347
364
  #{str}
348
365
  end
349
366
  end
350
367
  RUBY_EVAL
368
+
369
+ class_eval str, __FILE__, __LINE__ + 1
351
370
 
352
371
  before_name, around_name, after_name =
353
372
  options.values_at(:before, :after, :around)
@@ -359,15 +378,18 @@ module CouchRest
359
378
  def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
360
379
  @_keyed_callbacks ||= {}
361
380
  @_keyed_callbacks[[kind, key]] ||= begin
362
- str = self.send("_#{kind}_callbacks").compile(key, obj)
381
+ str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator"))
382
+
363
383
  self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
364
- def _run_#{klass}_#{kind}_#{key}_callbacks
384
+ def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks
365
385
  #{str}
366
386
  end
367
387
  RUBY_EVAL
388
+
368
389
  true
369
390
  end
370
- obj.send("_run_#{klass}_#{kind}_#{key}_callbacks", &blk)
391
+
392
+ obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
371
393
  end
372
394
 
373
395
  # Define callbacks.
@@ -402,20 +424,32 @@ module CouchRest
402
424
  # method that took into consideration the per_key conditions. This
403
425
  # is a speed improvement for ActionPack.
404
426
  def define_callbacks(*symbols)
427
+ terminator = symbols.pop if symbols.last.is_a?(String)
405
428
  symbols.each do |symbol|
429
+ self.class_inheritable_accessor("_#{symbol}_terminator")
430
+ self.send("_#{symbol}_terminator=", terminator)
406
431
  self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
407
432
  class_inheritable_accessor :_#{symbol}_callbacks
408
- self._#{symbol}_callbacks = CallbackChain.new
433
+ self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
409
434
 
410
- def self.#{symbol}_callback(type, *filters, &blk)
435
+ def self.#{symbol}_callback(*filters, &blk)
436
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
411
437
  options = filters.last.is_a?(Hash) ? filters.pop : {}
412
438
  filters.unshift(blk) if block_given?
413
- filters.map! {|f| Callback.new(f, type, options.dup, self, :#{symbol})}
439
+
440
+ filters.map! do |filter|
441
+ # overrides parent class
442
+ self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)}
443
+ Callback.new(filter, type, options.dup, self, :#{symbol})
444
+ end
414
445
  self._#{symbol}_callbacks.push(*filters)
415
- _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
446
+ _define_runner(:#{symbol},
447
+ self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
448
+ options)
416
449
  end
417
450
 
418
- def self.skip_#{symbol}_callback(type, *filters, &blk)
451
+ def self.skip_#{symbol}_callback(*filters, &blk)
452
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
419
453
  options = filters.last.is_a?(Hash) ? filters.pop : {}
420
454
  filters.unshift(blk) if block_given?
421
455
  filters.each do |filter|
@@ -428,15 +462,22 @@ module CouchRest
428
462
  else
429
463
  self._#{symbol}_callbacks.delete(filter)
430
464
  end
431
- _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
465
+ _define_runner(:#{symbol},
466
+ self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
467
+ options)
432
468
  end
433
469
 
434
470
  end
435
471
 
472
+ def self.reset_#{symbol}_callbacks
473
+ self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
474
+ _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {})
475
+ end
476
+
436
477
  self.#{symbol}_callback(:before)
437
478
  RUBY_EVAL
438
479
  end
439
480
  end
440
481
  end
441
482
  end
442
- end
483
+ end
@@ -16,6 +16,7 @@ module CouchRest
16
16
  def design_doc_slug
17
17
  return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
18
18
  funcs = []
19
+ design_doc ||= Design.new(default_design_doc)
19
20
  design_doc['views'].each do |name, view|
20
21
  funcs << "#{name}/#{view['map']}#{view['reduce']}"
21
22
  end
@@ -55,9 +55,14 @@ module CouchRest
55
55
  else
56
56
  # Let people use :send as a Time parse arg
57
57
  klass = ::CouchRest.constantize(target)
58
+ # I'm not convince we should or should not create a new instance if we are casting a doc/extended doc without default value and nothing was passed
59
+ # unless (property.casted &&
60
+ # (klass.superclass == CouchRest::ExtendedDocument || klass.superclass == CouchRest::Document) &&
61
+ # (self[key].nil? || property.default.nil?))
58
62
  klass.send(property.init_method, self[key])
63
+ #end
59
64
  end
60
- self[key].casted_by = self if self[key].respond_to?(:casted_by)
65
+ self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
61
66
  end
62
67
  end
63
68
  end
@@ -129,7 +129,7 @@ module CouchRest
129
129
  private
130
130
 
131
131
  def fetch_view_with_docs(name, opts, raw=false, &block)
132
- if raw
132
+ if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
133
133
  fetch_view(name, opts, &block)
134
134
  else
135
135
  begin
@@ -31,13 +31,19 @@ if RUBY_VERSION.to_f < 1.9
31
31
  class Net::BufferedIO #:nodoc:
32
32
  alias :old_rbuf_fill :rbuf_fill
33
33
  def rbuf_fill
34
- begin
35
- @rbuf << @io.read_nonblock(65536)
36
- rescue Errno::EWOULDBLOCK
37
- if IO.select([@io], nil, nil, @read_timeout)
34
+ if @io.respond_to?(:read_nonblock)
35
+ begin
38
36
  @rbuf << @io.read_nonblock(65536)
39
- else
40
- raise Timeout::TimeoutError
37
+ rescue Errno::EWOULDBLOCK
38
+ if IO.select([@io], nil, nil, @read_timeout)
39
+ retry
40
+ else
41
+ raise Timeout::TimeoutError
42
+ end
43
+ end
44
+ else
45
+ timeout(@read_timeout) do
46
+ @rbuf << @io.sysread(65536)
41
47
  end
42
48
  end
43
49
  end
@@ -24,15 +24,17 @@ module CouchRest
24
24
  subklass.send(:include, CouchRest::Mixins::Properties)
25
25
  end
26
26
 
27
+ # Accessors
28
+ attr_accessor :casted_by
29
+
27
30
  # Callbacks
28
31
  define_callbacks :create
29
32
  define_callbacks :save
30
33
  define_callbacks :update
31
34
  define_callbacks :destroy
32
35
 
33
- def initialize(keys={})
36
+ def initialize(passed_keys={})
34
37
  apply_defaults # defined in CouchRest::Mixins::Properties
35
- keys ||= {}
36
38
  super
37
39
  cast_keys # defined in CouchRest::Mixins::Properties
38
40
  unless self['_id'] && self['_rev']
@@ -203,8 +205,8 @@ module CouchRest
203
205
  _run_destroy_callbacks do
204
206
  result = database.delete_doc(self, bulk)
205
207
  if result['ok']
206
- self['_rev'] = nil
207
- self['_id'] = nil
208
+ self.delete('_rev')
209
+ self.delete('_id')
208
210
  end
209
211
  result['ok']
210
212
  end
@@ -25,151 +25,167 @@
25
25
  # example, an array without those additions being shared with either their
26
26
  # parent, siblings, or children, which is unlike the regular class-level
27
27
  # attributes that are shared across the entire hierarchy.
28
- class Class
29
- # Defines class-level and instance-level attribute reader.
30
- #
31
- # @param *syms<Array> Array of attributes to define reader for.
32
- # @return <Array[#to_s]> List of attributes that were made into cattr_readers
33
- #
34
- # @api public
35
- #
36
- # @todo Is this inconsistent in that it does not allow you to prevent
37
- # an instance_reader via :instance_reader => false
38
- def cattr_reader(*syms)
39
- syms.flatten.each do |sym|
40
- next if sym.is_a?(Hash)
41
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
- unless defined? @@#{sym}
43
- @@#{sym} = nil
44
- end
28
+ module CouchRest
29
+ module ClassExtension
30
+ def self.included(base)
31
+ if CouchRest::ClassExtension::InstanceMethods.instance_methods.all? {|m| base.respond_to?(m)}
32
+ # do nothing
33
+ elsif CouchRest::ClassExtension::InstanceMethods.instance_methods.any? {|m| base.respond_to?(m)}
34
+ raise RuntimeError, "Conflicting extentions to Class, work it out"
35
+ else
36
+ base.send(:include, CouchRest::ClassExtension::InstanceMethods)
37
+ end
38
+ end
39
+
40
+ module InstanceMethods
41
+ # Defines class-level and instance-level attribute reader.
42
+ #
43
+ # @param *syms<Array> Array of attributes to define reader for.
44
+ # @return <Array[#to_s]> List of attributes that were made into cattr_readers
45
+ #
46
+ # @api public
47
+ #
48
+ # @todo Is this inconsistent in that it does not allow you to prevent
49
+ # an instance_reader via :instance_reader => false
50
+ def cattr_reader(*syms)
51
+ syms.flatten.each do |sym|
52
+ next if sym.is_a?(Hash)
53
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
54
+ unless defined? @@#{sym}
55
+ @@#{sym} = nil
56
+ end
45
57
 
46
- def self.#{sym}
47
- @@#{sym}
48
- end
58
+ def self.#{sym}
59
+ @@#{sym}
60
+ end
49
61
 
50
- def #{sym}
51
- @@#{sym}
52
- end
53
- RUBY
62
+ def #{sym}
63
+ @@#{sym}
54
64
  end
55
- end
65
+ RUBY
66
+ end
67
+ end
56
68
 
57
- # Defines class-level (and optionally instance-level) attribute writer.
58
- #
59
- # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
60
- # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
61
- # @return <Array[#to_s]> List of attributes that were made into cattr_writers
62
- #
63
- # @api public
64
- def cattr_writer(*syms)
65
- options = syms.last.is_a?(Hash) ? syms.pop : {}
66
- syms.flatten.each do |sym|
67
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
68
- unless defined? @@#{sym}
69
- @@#{sym} = nil
70
- end
69
+ # Defines class-level (and optionally instance-level) attribute writer.
70
+ #
71
+ # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
72
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
73
+ # @return <Array[#to_s]> List of attributes that were made into cattr_writers
74
+ #
75
+ # @api public
76
+ def cattr_writer(*syms)
77
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
78
+ syms.flatten.each do |sym|
79
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
80
+ unless defined? @@#{sym}
81
+ @@#{sym} = nil
82
+ end
71
83
 
72
- def self.#{sym}=(obj)
73
- @@#{sym} = obj
74
- end
75
- RUBY
84
+ def self.#{sym}=(obj)
85
+ @@#{sym} = obj
86
+ end
87
+ RUBY
76
88
 
77
- unless options[:instance_writer] == false
78
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
- def #{sym}=(obj)
80
- @@#{sym} = obj
81
- end
82
- RUBY
83
- end
89
+ unless options[:instance_writer] == false
90
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
91
+ def #{sym}=(obj)
92
+ @@#{sym} = obj
84
93
  end
85
- end
94
+ RUBY
95
+ end
96
+ end
97
+ end
86
98
 
87
- # Defines class-level (and optionally instance-level) attribute accessor.
88
- #
89
- # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
90
- # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
91
- # @return <Array[#to_s]> List of attributes that were made into accessors
92
- #
93
- # @api public
94
- def cattr_accessor(*syms)
95
- cattr_reader(*syms)
96
- cattr_writer(*syms)
97
- end
99
+ # Defines class-level (and optionally instance-level) attribute accessor.
100
+ #
101
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
102
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
103
+ # @return <Array[#to_s]> List of attributes that were made into accessors
104
+ #
105
+ # @api public
106
+ def cattr_accessor(*syms)
107
+ cattr_reader(*syms)
108
+ cattr_writer(*syms)
109
+ end
98
110
 
99
- # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
100
- # each subclass has a copy of parent's attribute.
101
- #
102
- # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
103
- # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
104
- #
105
- # @api public
106
- #
107
- # @todo Do we want to block instance_reader via :instance_reader => false
108
- # @todo It would be preferable that we do something with a Hash passed in
109
- # (error out or do the same as other methods above) instead of silently
110
- # moving on). In particular, this makes the return value of this function
111
- # less useful.
112
- def class_inheritable_reader(*ivars)
113
- instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
111
+ # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
112
+ # each subclass has a copy of parent's attribute.
113
+ #
114
+ # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
115
+ # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
116
+ #
117
+ # @api public
118
+ #
119
+ # @todo Do we want to block instance_reader via :instance_reader => false
120
+ # @todo It would be preferable that we do something with a Hash passed in
121
+ # (error out or do the same as other methods above) instead of silently
122
+ # moving on). In particular, this makes the return value of this function
123
+ # less useful.
124
+ def class_inheritable_reader(*ivars)
125
+ instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
114
126
 
115
- ivars.each do |ivar|
116
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
117
- def self.#{ivar}
118
- return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
119
- ivar = superclass.#{ivar}
120
- return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
121
- @#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) && !ivar.is_a?(Symbol) ? ivar.dup : ivar
122
- end
123
- RUBY
124
- unless instance_reader == false
125
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
126
- def #{ivar}
127
- self.class.#{ivar}
128
- end
129
- RUBY
127
+ ivars.each do |ivar|
128
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
129
+ def self.#{ivar}
130
+ return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
131
+ ivar = superclass.#{ivar}
132
+ return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
133
+ @#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) && !ivar.is_a?(Symbol) ? ivar.dup : ivar
134
+ end
135
+ RUBY
136
+ unless instance_reader == false
137
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
138
+ def #{ivar}
139
+ self.class.#{ivar}
140
+ end
141
+ RUBY
142
+ end
143
+ end
130
144
  end
145
+
146
+ # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
147
+ # each subclass has a copy of parent's attribute.
148
+ #
149
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
150
+ # define inheritable writer for.
151
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
152
+ # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
153
+ #
154
+ # @api public
155
+ #
156
+ # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
157
+ # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
158
+ def class_inheritable_writer(*ivars)
159
+ instance_writer = ivars.pop[:instance_writer] if ivars.last.is_a?(Hash)
160
+ ivars.each do |ivar|
161
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
162
+ def self.#{ivar}=(obj)
163
+ @#{ivar} = obj
131
164
  end
132
- end
165
+ RUBY
166
+ unless instance_writer == false
167
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
168
+ def #{ivar}=(obj) self.class.#{ivar} = obj end
169
+ RUBY
170
+ end
171
+ end
172
+ end
133
173
 
134
- # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
135
- # each subclass has a copy of parent's attribute.
136
- #
137
- # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
138
- # define inheritable writer for.
139
- # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
140
- # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
141
- #
142
- # @api public
143
- #
144
- # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
145
- # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
146
- def class_inheritable_writer(*ivars)
147
- instance_writer = ivars.pop[:instance_writer] if ivars.last.is_a?(Hash)
148
- ivars.each do |ivar|
149
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
150
- def self.#{ivar}=(obj)
151
- @#{ivar} = obj
152
- end
153
- RUBY
154
- unless instance_writer == false
155
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
156
- def #{ivar}=(obj) self.class.#{ivar} = obj end
157
- RUBY
174
+ # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
175
+ # each subclass has a copy of parent's attribute.
176
+ #
177
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
178
+ # define inheritable accessor for.
179
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
180
+ # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
181
+ #
182
+ # @api public
183
+ def class_inheritable_accessor(*syms)
184
+ class_inheritable_reader(*syms)
185
+ class_inheritable_writer(*syms)
158
186
  end
159
187
  end
160
188
  end
161
-
162
- # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
163
- # each subclass has a copy of parent's attribute.
164
- #
165
- # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
166
- # define inheritable accessor for.
167
- # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
168
- # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
169
- #
170
- # @api public
171
- def class_inheritable_accessor(*syms)
172
- class_inheritable_reader(*syms)
173
- class_inheritable_writer(*syms)
174
- end
175
- end
189
+ end
190
+
191
+ Class.send(:include, CouchRest::ClassExtension)
data/lib/couchrest.rb CHANGED
@@ -22,12 +22,13 @@ $:.unshift File.dirname(__FILE__) unless
22
22
  $:.include?(File.dirname(__FILE__)) ||
23
23
  $:.include?(File.expand_path(File.dirname(__FILE__)))
24
24
 
25
+ $COUCHREST_DEBUG ||= false
25
26
 
26
27
  require 'couchrest/monkeypatches'
27
28
 
28
29
  # = CouchDB, close to the metal
29
30
  module CouchRest
30
- VERSION = '0.16' unless self.const_defined?("VERSION")
31
+ VERSION = '0.17.0' unless self.const_defined?("VERSION")
31
32
 
32
33
  autoload :Server, 'couchrest/core/server'
33
34
  autoload :Database, 'couchrest/core/database'
@@ -39,6 +40,7 @@ module CouchRest
39
40
  autoload :Pager, 'couchrest/helper/pager'
40
41
  autoload :FileManager, 'couchrest/helper/file_manager'
41
42
  autoload :Streamer, 'couchrest/helper/streamer'
43
+ autoload :Upgrade, 'couchrest/helper/upgrade'
42
44
 
43
45
  autoload :ExtendedDocument, 'couchrest/more/extended_document'
44
46
  autoload :CastedModel, 'couchrest/more/casted_model'
@@ -124,18 +126,42 @@ module CouchRest
124
126
  cr.database(parsed[:database])
125
127
  end
126
128
 
127
- def put uri, doc = nil
129
+ def put(uri, doc = nil)
128
130
  payload = doc.to_json if doc
129
- JSON.parse(RestClient.put(uri, payload))
131
+ begin
132
+ JSON.parse(RestClient.put(uri, payload))
133
+ rescue Exception => e
134
+ if $COUCHREST_DEBUG == true
135
+ raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}"
136
+ else
137
+ raise e
138
+ end
139
+ end
130
140
  end
131
141
 
132
- def get uri
133
- JSON.parse(RestClient.get(uri), :max_nesting => false)
142
+ def get(uri)
143
+ begin
144
+ JSON.parse(RestClient.get(uri), :max_nesting => false)
145
+ rescue => e
146
+ if $COUCHREST_DEBUG == true
147
+ raise "Error while sending a GET request #{uri}\n: #{e}"
148
+ else
149
+ raise e
150
+ end
151
+ end
134
152
  end
135
153
 
136
154
  def post uri, doc = nil
137
155
  payload = doc.to_json if doc
138
- JSON.parse(RestClient.post(uri, payload))
156
+ begin
157
+ JSON.parse(RestClient.post(uri, payload))
158
+ rescue Exception => e
159
+ if $COUCHREST_DEBUG == true
160
+ raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}"
161
+ else
162
+ raise e
163
+ end
164
+ end
139
165
  end
140
166
 
141
167
  def delete uri
@@ -149,8 +149,8 @@ describe CouchRest::Database do
149
149
  {"mild" => "yet local"},
150
150
  {"another" => ["set","of","keys"]}
151
151
  ])
152
- rs['new_revs'].each do |r|
153
- @db.get(r['id'])
152
+ rs.each do |r|
153
+ @db.get(r['id']).rev.should == r["rev"]
154
154
  end
155
155
  end
156
156
 
@@ -170,26 +170,10 @@ describe CouchRest::Database do
170
170
  {"_id" => "twoB", "mild" => "yet local"},
171
171
  {"another" => ["set","of","keys"]}
172
172
  ])
173
- rs['new_revs'].each do |r|
174
- @db.get(r['id'])
173
+ rs.each do |r|
174
+ @db.get(r['id']).rev.should == r["rev"]
175
175
  end
176
176
  end
177
-
178
- it "in the case of an id conflict should not insert anything" do
179
- @r = @db.save_doc({'lemons' => 'from texas', 'and' => 'how', "_id" => "oneB"})
180
-
181
- lambda do
182
- rs = @db.bulk_save([
183
- {"_id" => "oneB", "wild" => "and random"},
184
- {"_id" => "twoB", "mild" => "yet local"},
185
- {"another" => ["set","of","keys"]}
186
- ])
187
- end.should raise_error(RestClient::RequestFailed)
188
-
189
- lambda do
190
- @db.get('twoB')
191
- end.should raise_error(RestClient::ResourceNotFound)
192
- end
193
177
 
194
178
  it "should empty the bulk save cache if no documents are given" do
195
179
  @db.save_doc({"_id" => "bulk_cache_1", "val" => "test"}, true)
@@ -593,7 +577,8 @@ describe CouchRest::Database do
593
577
  @db.save_doc({'_id' => @docid, 'will-exist' => 'here'})
594
578
  end
595
579
  it "should fail without a rev" do
596
- lambda{@db.move_doc @doc, @docid}.should raise_error(RestClient::RequestFailed)
580
+ @doc.delete("_rev")
581
+ lambda{@db.move_doc @doc, @docid}.should raise_error(ArgumentError)
597
582
  lambda{@db.get(@r['id'])}.should_not raise_error
598
583
  end
599
584
  it "should succeed with a rev" do
@@ -224,7 +224,8 @@ describe CouchRest::Document do
224
224
  @db.save_doc({'_id' => @docid, 'will-exist' => 'here'})
225
225
  end
226
226
  it "should fail without a rev" do
227
- lambda{@doc.move @docid}.should raise_error(RestClient::RequestFailed)
227
+ @doc.delete("_rev")
228
+ lambda{@doc.move @docid}.should raise_error(ArgumentError)
228
229
  lambda{@db.get(@resp['id'])}.should_not raise_error
229
230
  end
230
231
  it "should succeed with a rev" do
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+ require File.join(FIXTURE_PATH, 'more', 'card')
3
+
4
+ class Car < CouchRest::ExtendedDocument
5
+ use_database TEST_SERVER.default_database
6
+
7
+ property :name
8
+ property :driver, :cast_as => 'Driver'
9
+ end
10
+
11
+ class Driver < CouchRest::ExtendedDocument
12
+ use_database TEST_SERVER.default_database
13
+ # You have to add a casted_by accessor if you want to reach a casted extended doc parent
14
+ attr_accessor :casted_by
15
+
16
+ property :name
17
+ end
18
+
19
+ describe "casting an extended document" do
20
+
21
+ before(:each) do
22
+ @car = Car.new(:name => 'Renault 306')
23
+ @driver = Driver.new(:name => 'Matt')
24
+ end
25
+
26
+ # it "should not create an empty casted object" do
27
+ # @car.driver.should be_nil
28
+ # end
29
+
30
+ it "should let you assign the casted attribute after instantializing an object" do
31
+ @car.driver = @driver
32
+ @car.driver.name.should == 'Matt'
33
+ end
34
+
35
+ it "should let the casted document who casted it" do
36
+ Car.new(:name => 'Renault 306', :driver => @driver)
37
+ @car.driver.casted_by.should == @car
38
+ end
39
+
40
+ end
@@ -70,6 +70,7 @@ describe CouchRest::CastedModel do
70
70
 
71
71
  describe "saved document with casted models" do
72
72
  before(:each) do
73
+ reset_test_db!
73
74
  @obj = DummyModel.new(:casted_attribute => {:name => 'whatever'})
74
75
  @obj.save.should be_true
75
76
  @obj = DummyModel.get(@obj.id)
@@ -94,4 +95,4 @@ describe CouchRest::CastedModel do
94
95
 
95
96
  end
96
97
 
97
- end
98
+ end
@@ -4,6 +4,7 @@ describe "ExtendedDocument attachments" do
4
4
 
5
5
  describe "#has_attachment?" do
6
6
  before(:each) do
7
+ reset_test_db!
7
8
  @obj = Basic.new
8
9
  @obj.save.should == true
9
10
  @file = File.open(FIXTURE_PATH + '/attachments/test.html')
@@ -126,4 +127,4 @@ describe "ExtendedDocument attachments" do
126
127
  @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}"
127
128
  end
128
129
  end
129
- end
130
+ end
@@ -442,7 +442,7 @@ describe "ExtendedDocument" do
442
442
  @dobj.destroy
443
443
  @dobj.rev.should be_nil
444
444
  @dobj.id.should be_nil
445
- @dobj.save.should == true
445
+ @dobj.save.should == true
446
446
  end
447
447
  it "should make it go away" do
448
448
  @dobj.destroy
@@ -105,6 +105,9 @@ describe "ExtendedDocument views" do
105
105
 
106
106
  describe "a model with a compound key view" do
107
107
  before(:all) do
108
+ Article.design_doc_fresh = false
109
+ Article.by_user_id_and_date.each{|a| a.destroy(true)}
110
+ Article.database.bulk_delete
108
111
  written_at = Time.now - 24 * 3600 * 7
109
112
  @titles = ["uniq one", "even more interesting", "less fun", "not junk"]
110
113
  @user_ids = ["quentin", "aaron"]
@@ -164,41 +167,41 @@ describe "ExtendedDocument views" do
164
167
  # TODO: moved to Design, delete
165
168
  describe "adding a view" do
166
169
  before(:each) do
170
+ reset_test_db!
167
171
  Article.by_date
168
- @design_docs = Article.database.documents :startkey => "_design/",
169
- :endkey => "_design/\u9999"
172
+ @design_docs = Article.database.documents :startkey => "_design/", :endkey => "_design/\u9999"
170
173
  end
171
174
  it "should not create a design doc on view definition" do
172
175
  Article.view_by :created_at
173
- newdocs = Article.database.documents :startkey => "_design/",
174
- :endkey => "_design/\u9999"
176
+ newdocs = Article.database.documents :startkey => "_design/", :endkey => "_design/\u9999"
175
177
  newdocs["rows"].length.should == @design_docs["rows"].length
176
178
  end
177
- it "should create a new design document on view access" do
179
+ it "should create a new version of the design document on view access" do
180
+ old_design_doc = Article.database.documents(:key => @design_docs["rows"].first["key"], :include_docs => true)["rows"][0]["doc"]
178
181
  Article.view_by :updated_at
179
182
  Article.by_updated_at
180
- newdocs = Article.database.documents :startkey => "_design/",
181
- :endkey => "_design/\u9999"
182
- # puts @design_docs.inspect
183
- # puts newdocs.inspect
184
- newdocs["rows"].length.should == @design_docs["rows"].length + 1
183
+ newdocs = Article.database.documents({:startkey => "_design/", :endkey => "_design/\u9999"})
184
+
185
+ doc = Article.database.documents(:key => @design_docs["rows"].first["key"], :include_docs => true)["rows"][0]["doc"]
186
+ doc["_rev"].should_not == old_design_doc["_rev"]
187
+ doc["views"].keys.should include("by_updated_at")
185
188
  end
186
189
  end
187
190
 
188
191
  describe "with a lot of designs left around" do
189
192
  before(:each) do
193
+ reset_test_db!
190
194
  Article.by_date
191
195
  Article.view_by :field
192
196
  Article.by_field
193
197
  end
194
198
  it "should clean them up" do
199
+ ddocs = Article.all_design_doc_versions
195
200
  Article.view_by :stream
196
201
  Article.by_stream
197
- ddocs = Article.all_design_doc_versions
198
- ddocs["rows"].length.should > 1
199
202
  Article.cleanup_design_docs!
200
203
  ddocs = Article.all_design_doc_versions
201
204
  ddocs["rows"].length.should == 1
202
205
  end
203
206
  end
204
- end
207
+ end
@@ -8,6 +8,7 @@ require File.join(FIXTURE_PATH, 'more', 'event')
8
8
  describe "ExtendedDocument properties" do
9
9
 
10
10
  before(:each) do
11
+ reset_test_db!
11
12
  @card = Card.new(:first_name => "matt")
12
13
  end
13
14
 
@@ -126,4 +127,4 @@ describe "ExtendedDocument properties" do
126
127
  end
127
128
  end
128
129
 
129
- end
130
+ end
@@ -0,0 +1,59 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+ require File.join(File.dirname(__FILE__), '..', '..', '..', 'lib', 'couchrest', 'support', 'class')
3
+
4
+ describe CouchRest::ClassExtension do
5
+
6
+ before :all do
7
+ class FullyDefinedClassExtensions
8
+ def self.respond_to?(method)
9
+ if CouchRest::ClassExtension::InstanceMethods.instance_methods.include?(method)
10
+ true
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+
17
+ class PartDefinedClassExtensions
18
+ def self.respond_to?(method)
19
+ methods = CouchRest::ClassExtension::InstanceMethods.instance_methods
20
+ methods.delete('cattr_reader')
21
+
22
+ if methods.include?(method)
23
+ false
24
+ else
25
+ super
26
+ end
27
+ end
28
+ end
29
+
30
+ class NoClassExtensions
31
+ def self.respond_to?(method)
32
+ if CouchRest::ClassExtension::InstanceMethods.instance_methods.include?(method)
33
+ false
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+ end
42
+
43
+ it "should not include InstanceMethods if the class extensions are already defined" do
44
+ FullyDefinedClassExtensions.send(:include, CouchRest::ClassExtension)
45
+ FullyDefinedClassExtensions.ancestors.should_not include(CouchRest::ClassExtension::InstanceMethods)
46
+ end
47
+
48
+ it "should raise RuntimeError if the class extensions are only partially defined" do
49
+ lambda {
50
+ PartDefinedClassExtensions.send(:include, CouchRest::ClassExtension)
51
+ }.should raise_error(RuntimeError)
52
+ end
53
+
54
+ it "should include class extensions if they are not already defined" do
55
+ NoClassExtensions.send(:include, CouchRest::ClassExtension)
56
+ NoClassExtensions.ancestors.should include(CouchRest::ClassExtension::InstanceMethods)
57
+ end
58
+
59
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jchris-couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.16"
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
@@ -89,6 +89,7 @@ files:
89
89
  - lib/couchrest/helper
90
90
  - lib/couchrest/helper/pager.rb
91
91
  - lib/couchrest/helper/streamer.rb
92
+ - lib/couchrest/helper/upgrade.rb
92
93
  - lib/couchrest/mixins
93
94
  - lib/couchrest/mixins/attachments.rb
94
95
  - lib/couchrest/mixins/callbacks.rb
@@ -136,36 +137,19 @@ files:
136
137
  - spec/couchrest/helpers/pager_spec.rb
137
138
  - spec/couchrest/helpers/streamer_spec.rb
138
139
  - spec/couchrest/more
140
+ - spec/couchrest/more/casted_extended_doc_spec.rb
139
141
  - spec/couchrest/more/casted_model_spec.rb
140
142
  - spec/couchrest/more/extended_doc_attachment_spec.rb
141
143
  - spec/couchrest/more/extended_doc_spec.rb
142
144
  - spec/couchrest/more/extended_doc_view_spec.rb
143
145
  - spec/couchrest/more/property_spec.rb
146
+ - spec/couchrest/support
147
+ - spec/couchrest/support/class_spec.rb
144
148
  - spec/fixtures
145
149
  - spec/fixtures/attachments
146
150
  - spec/fixtures/attachments/couchdb.png
147
151
  - spec/fixtures/attachments/README
148
152
  - spec/fixtures/attachments/test.html
149
- - spec/fixtures/couchapp
150
- - spec/fixtures/couchapp/_attachments
151
- - spec/fixtures/couchapp/_attachments/index.html
152
- - spec/fixtures/couchapp/doc.json
153
- - spec/fixtures/couchapp/foo
154
- - spec/fixtures/couchapp/foo/bar.txt
155
- - spec/fixtures/couchapp/foo/test.json
156
- - spec/fixtures/couchapp/test.json
157
- - spec/fixtures/couchapp/views
158
- - spec/fixtures/couchapp/views/example-map.js
159
- - spec/fixtures/couchapp/views/example-reduce.js
160
- - spec/fixtures/couchapp-test
161
- - spec/fixtures/couchapp-test/my-app
162
- - spec/fixtures/couchapp-test/my-app/_attachments
163
- - spec/fixtures/couchapp-test/my-app/_attachments/index.html
164
- - spec/fixtures/couchapp-test/my-app/foo
165
- - spec/fixtures/couchapp-test/my-app/foo/bar.txt
166
- - spec/fixtures/couchapp-test/my-app/views
167
- - spec/fixtures/couchapp-test/my-app/views/example-map.js
168
- - spec/fixtures/couchapp-test/my-app/views/example-reduce.js
169
153
  - spec/fixtures/more
170
154
  - spec/fixtures/more/article.rb
171
155
  - spec/fixtures/more/card.rb