dpla-couchrest 1.2.1.pre.dpla

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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +176 -0
  6. data/README.md +66 -0
  7. data/Rakefile +23 -0
  8. data/THANKS.md +21 -0
  9. data/VERSION +1 -0
  10. data/couchrest.gemspec +36 -0
  11. data/examples/word_count/markov +38 -0
  12. data/examples/word_count/views/books/chunked-map.js +3 -0
  13. data/examples/word_count/views/books/united-map.js +1 -0
  14. data/examples/word_count/views/markov/chain-map.js +6 -0
  15. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  16. data/examples/word_count/views/word_count/count-map.js +6 -0
  17. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  18. data/examples/word_count/word_count.rb +46 -0
  19. data/examples/word_count/word_count_query.rb +40 -0
  20. data/examples/word_count/word_count_views.rb +26 -0
  21. data/history.txt +214 -0
  22. data/init.rb +1 -0
  23. data/lib/couchrest.rb +146 -0
  24. data/lib/couchrest/attributes.rb +89 -0
  25. data/lib/couchrest/commands/generate.rb +71 -0
  26. data/lib/couchrest/commands/push.rb +103 -0
  27. data/lib/couchrest/database.rb +402 -0
  28. data/lib/couchrest/design.rb +91 -0
  29. data/lib/couchrest/document.rb +105 -0
  30. data/lib/couchrest/helper/attachments.rb +29 -0
  31. data/lib/couchrest/helper/pager.rb +103 -0
  32. data/lib/couchrest/helper/streamer.rb +60 -0
  33. data/lib/couchrest/helper/upgrade.rb +51 -0
  34. data/lib/couchrest/middlewares/logger.rb +263 -0
  35. data/lib/couchrest/monkeypatches.rb +25 -0
  36. data/lib/couchrest/rest_api.rb +166 -0
  37. data/lib/couchrest/server.rb +92 -0
  38. data/lib/couchrest/support/inheritable_attributes.rb +107 -0
  39. data/spec/.gitignore +1 -0
  40. data/spec/couchrest/couchrest_spec.rb +197 -0
  41. data/spec/couchrest/database_spec.rb +914 -0
  42. data/spec/couchrest/design_spec.rb +206 -0
  43. data/spec/couchrest/document_spec.rb +400 -0
  44. data/spec/couchrest/helpers/pager_spec.rb +115 -0
  45. data/spec/couchrest/helpers/streamer_spec.rb +134 -0
  46. data/spec/couchrest/rest_api_spec.rb +241 -0
  47. data/spec/couchrest/server_spec.rb +35 -0
  48. data/spec/fixtures/attachments/README +3 -0
  49. data/spec/fixtures/attachments/couchdb.png +0 -0
  50. data/spec/fixtures/attachments/test.html +11 -0
  51. data/spec/fixtures/views/lib.js +3 -0
  52. data/spec/fixtures/views/test_view/lib.js +3 -0
  53. data/spec/fixtures/views/test_view/only-map.js +4 -0
  54. data/spec/fixtures/views/test_view/test-map.js +3 -0
  55. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  56. data/spec/spec.opts +5 -0
  57. data/spec/spec_helper.rb +46 -0
  58. data/utils/remap.rb +27 -0
  59. data/utils/subset.rb +30 -0
  60. metadata +212 -0
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+
3
+ module CouchRest
4
+ module Commands
5
+ module Generate
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ design_names = options[:trailing_args]
10
+
11
+ FileUtils.mkdir_p(directory)
12
+ filename = File.join(directory, "lib.js")
13
+ self.write(filename, <<-FUNC)
14
+ // Put global functions here.
15
+ // Include in your views with
16
+ //
17
+ // //include-lib
18
+ FUNC
19
+
20
+ design_names.each do |design_name|
21
+ subdirectory = File.join(directory, design_name)
22
+ FileUtils.mkdir_p(subdirectory)
23
+ filename = File.join(subdirectory, "sample-map.js")
24
+ self.write(filename, <<-FUNC)
25
+ function(doc) {
26
+ // Keys is first letter of _id
27
+ emit(doc._id[0], doc);
28
+ }
29
+ FUNC
30
+
31
+ filename = File.join(subdirectory, "sample-reduce.js")
32
+ self.write(filename, <<-FUNC)
33
+ function(keys, values) {
34
+ // Count the number of keys starting with this letter
35
+ return values.length;
36
+ }
37
+ FUNC
38
+
39
+ filename = File.join(subdirectory, "lib.js")
40
+ self.write(filename, <<-FUNC)
41
+ // Put functions specific to '#{design_name}' here.
42
+ // Include in your views with
43
+ //
44
+ // //include-lib
45
+ FUNC
46
+ end
47
+ end
48
+
49
+ def self.help
50
+ helpstring = <<-GEN
51
+
52
+ Usage: couchview generate directory design1 design2 design3 ...
53
+
54
+ Couchview will create directories and example views for the design documents you specify.
55
+
56
+ GEN
57
+ helpstring.gsub(/^ /, '')
58
+ end
59
+
60
+ def self.write(filename, contents)
61
+ puts "Writing #{filename}"
62
+ File.open(filename, "w") do |f|
63
+ # Remove leading spaces
64
+ contents.gsub!(/^ ( )?/, '')
65
+ f.write contents
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+
3
+ module Commands
4
+
5
+ module Push
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ database = options[:trailing_args].first
10
+
11
+ fm = CouchRest::FileManager.new(database)
12
+ fm.loud = options[:loud]
13
+
14
+ if options[:loud]
15
+ puts "Pushing views from directory #{directory} to database #{fm.db}"
16
+ end
17
+
18
+ fm.push_views(directory)
19
+ end
20
+
21
+ def self.help
22
+ helpstring = <<-GEN
23
+
24
+ == Pushing views with Couchview ==
25
+
26
+ Usage: couchview push directory dbname
27
+
28
+ Couchview expects a specific filesystem layout for your CouchDB views (see
29
+ example below). It also supports advanced features like inlining of library
30
+ code (so you can keep DRY) as well as avoiding unnecessary document
31
+ modification.
32
+
33
+ Couchview also solves a problem with CouchDB's view API, which only provides
34
+ access to the final reduce side of any views which have both a map and a
35
+ reduce function defined. The intermediate map results are often useful for
36
+ development and production. CouchDB is smart enough to reuse map indexes for
37
+ functions duplicated across views within the same design document.
38
+
39
+ For views with a reduce function defined, Couchview creates both a reduce view
40
+ and a map-only view, so that you can browse and query the map side as well as
41
+ the reduction, with no performance penalty.
42
+
43
+ == Example ==
44
+
45
+ couchview push foo-project/bar-views baz-database
46
+
47
+ This will push the views defined in foo-project/bar-views into a database
48
+ called baz-database. Couchview expects the views to be defined in files with
49
+ names like:
50
+
51
+ foo-project/bar-views/my-design/viewname-map.js
52
+ foo-project/bar-views/my-design/viewname-reduce.js
53
+ foo-project/bar-views/my-design/noreduce-map.js
54
+
55
+ Pushed to => http://127.0.0.1:5984/baz-database/_design/my-design
56
+
57
+ And the design document:
58
+ {
59
+ "views" : {
60
+ "viewname-map" : {
61
+ "map" : "### contents of view-name-map.js ###"
62
+ },
63
+ "viewname-reduce" : {
64
+ "map" : "### contents of view-name-map.js ###",
65
+ "reduce" : "### contents of view-name-reduce.js ###"
66
+ },
67
+ "noreduce-map" : {
68
+ "map" : "### contents of noreduce-map.js ###"
69
+ }
70
+ }
71
+ }
72
+
73
+ Couchview will create a design document for each subdirectory of the views
74
+ directory specified on the command line.
75
+
76
+ == Library Inlining ==
77
+
78
+ Couchview can optionally inline library code into your views so you only have
79
+ to maintain it in one place. It looks for any files named lib.* in your
80
+ design-doc directory (for doc specific libs) and in the parent views directory
81
+ (for project global libs). These libraries are only inserted into views which
82
+ include the text
83
+
84
+ // !include lib
85
+
86
+ or
87
+
88
+ # !include lib
89
+
90
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
91
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
92
+ added or changed.
93
+
94
+ GEN
95
+ helpstring.gsub(/^ /, '')
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,402 @@
1
+ require 'cgi'
2
+ require "base64"
3
+
4
+ module CouchRest
5
+ class Database
6
+ attr_reader :server, :host, :name, :root, :uri
7
+ attr_accessor :bulk_save_cache_limit
8
+
9
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server
10
+ # and database name.
11
+ #
12
+ # ==== Parameters
13
+ # server<CouchRest::Server>:: database host
14
+ # name<String>:: database name
15
+ #
16
+ 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
23
+ @bulk_save_cache = []
24
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
25
+ end
26
+
27
+ # == Database information and manipulation methods
28
+
29
+ # returns the database's uri
30
+ def to_s
31
+ @root
32
+ end
33
+
34
+ # GET the database info from CouchDB
35
+ def info
36
+ CouchRest.get @root
37
+ end
38
+
39
+ # Compact the database, removing old document revisions and optimizing space use.
40
+ def compact!
41
+ CouchRest.post "#{@root}/_compact"
42
+ end
43
+
44
+ # Create the database
45
+ def create!
46
+ bool = server.create_db(@name) rescue false
47
+ bool && true
48
+ end
49
+
50
+ # Delete and re create the database
51
+ def recreate!
52
+ delete!
53
+ create!
54
+ rescue RestClient::ResourceNotFound
55
+ ensure
56
+ create!
57
+ end
58
+
59
+ # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
60
+ def replicate_from(other_db, continuous = false, create_target = false, doc_ids = nil)
61
+ replicate(other_db, continuous, :target => name, :create_target => create_target, :doc_ids => doc_ids)
62
+ end
63
+
64
+ # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
65
+ def replicate_to(other_db, continuous = false, create_target = false, doc_ids = nil)
66
+ replicate(other_db, continuous, :source => name, :create_target => create_target, :doc_ids => doc_ids)
67
+ end
68
+
69
+ # DELETE the database itself. This is not undoable and could be rather
70
+ # catastrophic. Use with care!
71
+ def delete!
72
+ CouchRest.delete @root
73
+ end
74
+
75
+
76
+ # == Retrieving and saving single documents
77
+
78
+ # GET a document from CouchDB, by id. Returns a Document or Design.
79
+ def get(id, params = {})
80
+ slug = escape_docid(id)
81
+ url = CouchRest.paramify_url("#{@root}/#{slug}", params)
82
+ result = CouchRest.get(url)
83
+ return result unless result.is_a?(Hash)
84
+ doc = if /^_design/ =~ result["_id"]
85
+ Design.new(result)
86
+ else
87
+ Document.new(result)
88
+ end
89
+ doc.database = self
90
+ doc
91
+ end
92
+
93
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
94
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
95
+ # no <tt>_id</tt> is present on the document. IDs are attached to
96
+ # documents on the client side because POST has the curious property of
97
+ # being automatically retried by proxies in the event of network
98
+ # segmentation and lost responses.
99
+ #
100
+ # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
101
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
102
+ #
103
+ # If <tt>batch</tt> is true (false by default) the document is saved in
104
+ # batch mode, "used to achieve higher throughput at the cost of lower
105
+ # guarantees. When [...] sent using this option, it is not immediately
106
+ # written to disk. Instead it is stored in memory on a per-user basis for a
107
+ # second or so (or the number of docs in memory reaches a certain point).
108
+ # After the threshold has passed, the docs are committed to disk. Instead
109
+ # of waiting for the doc to be written to disk before responding, CouchDB
110
+ # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
111
+ # for crucial data, but it ideal for applications like logging which can
112
+ # accept the risk that a small proportion of updates could be lost due to a
113
+ # crash."
114
+ def save_doc(doc, bulk = false, batch = false)
115
+ if doc['_attachments']
116
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
117
+ end
118
+ if bulk
119
+ @bulk_save_cache << doc
120
+ bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
121
+ return {'ok' => true} # Compatibility with Document#save
122
+ elsif !bulk && @bulk_save_cache.length > 0
123
+ bulk_save
124
+ end
125
+ result = if doc['_id']
126
+ slug = escape_docid(doc['_id'])
127
+ begin
128
+ uri = "#{@root}/#{slug}"
129
+ uri << "?batch=ok" if batch
130
+ CouchRest.put uri, doc
131
+ rescue RestClient::ResourceNotFound
132
+ 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
135
+ end
136
+ 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
143
+ end
144
+ if result['ok']
145
+ doc['_id'] = result['id']
146
+ doc['_rev'] = result['rev']
147
+ doc.database = self if doc.respond_to?(:database=)
148
+ end
149
+ result
150
+ end
151
+
152
+ # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
153
+ def bulk_save_doc(doc)
154
+ save_doc(doc, true)
155
+ end
156
+
157
+ # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
158
+ def batch_save_doc(doc)
159
+ save_doc(doc, false, true)
160
+ end
161
+
162
+ # POST an array of documents to CouchDB. If any of the documents are
163
+ # missing ids, supply one from the uuid cache.
164
+ #
165
+ # If called with no arguments, bulk saves the cache of documents to be bulk saved.
166
+ def bulk_save(docs = nil, use_uuids = true, all_or_nothing = false)
167
+ if docs.nil?
168
+ docs = @bulk_save_cache
169
+ @bulk_save_cache = []
170
+ end
171
+ if (use_uuids)
172
+ ids, noids = docs.partition{|d|d['_id']}
173
+ uuid_count = [noids.length, @server.uuid_batch_count].max
174
+ noids.each do |doc|
175
+ nextid = @server.next_uuid(uuid_count) rescue nil
176
+ doc['_id'] = nextid if nextid
177
+ end
178
+ end
179
+ request_body = {:docs => docs}
180
+ if all_or_nothing
181
+ request_body[:all_or_nothing] = true
182
+ end
183
+ CouchRest.post "#{@root}/_bulk_docs", request_body
184
+ end
185
+ alias :bulk_delete :bulk_save
186
+
187
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
188
+ # <tt>_rev</tt>.
189
+ #
190
+ # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
191
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
192
+ def delete_doc(doc, bulk = false)
193
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
194
+ if bulk
195
+ @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], :_deleted => true }
196
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
197
+ return {'ok' => true} # Mimic the non-deferred version
198
+ end
199
+ slug = escape_docid(doc['_id'])
200
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
201
+ end
202
+
203
+ # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
204
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
205
+ # hash with a '_rev' key
206
+ def copy_doc(doc, dest)
207
+ raise ArgumentError, "_id is required for copying" unless doc['_id']
208
+ slug = escape_docid(doc['_id'])
209
+ destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
210
+ "#{dest['_id']}?rev=#{dest['_rev']}"
211
+ else
212
+ dest
213
+ end
214
+ CouchRest.copy "#{@root}/#{slug}", destination
215
+ end
216
+
217
+ # Updates the given doc by yielding the current state of the doc
218
+ # and trying to update update_limit times. Returns the doc
219
+ # if successfully updated without hitting the limit.
220
+ # If the limit is reached, the last execption will be raised.
221
+ def update_doc(doc_id, params = {}, update_limit = 10)
222
+ resp = {'ok' => false}
223
+ last_fail = nil
224
+
225
+ until resp['ok'] or update_limit <= 0
226
+ doc = self.get(doc_id, params)
227
+ yield doc
228
+ begin
229
+ resp = self.save_doc doc
230
+ rescue RestClient::RequestFailed => e
231
+ if e.http_code == 409 # Update collision
232
+ update_limit -= 1
233
+ last_fail = e
234
+ else
235
+ raise e
236
+ end
237
+ end
238
+ end
239
+
240
+ raise last_fail unless resp['ok']
241
+ doc
242
+ end
243
+
244
+
245
+ # == View and multi-document based queries
246
+
247
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
248
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
249
+ def view(name, params = {}, payload = {}, &block)
250
+ params = params.dup
251
+ payload['keys'] = params.delete(:keys) if params[:keys]
252
+ # Try recognising the name, otherwise assume already prepared
253
+ 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
261
+ else
262
+ if !payload.empty?
263
+ CouchRest.post url, payload
264
+ else
265
+ CouchRest.get url
266
+ end
267
+ end
268
+ end
269
+
270
+ # POST a temporary view function to CouchDB for querying. This is not
271
+ # recommended, as you don't get any performance benefit from CouchDB's
272
+ # materialized views. Can be quite slow on large databases.
273
+ def temp_view(payload, params = {}, &block)
274
+ view('_temp_view', params, payload, &block)
275
+ end
276
+ alias :slow_view :temp_view
277
+
278
+
279
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
280
+ def all_docs(params = {}, payload = {}, &block)
281
+ view("_all_docs", params, payload, &block)
282
+ end
283
+ alias :documents :all_docs
284
+
285
+ # Query CouchDB's special <tt>_changes</tt> feed for the latest.
286
+ # All standard CouchDB options can be provided.
287
+ #
288
+ # Warning: sending :feed => 'continuous' will cause your code to block
289
+ # indefinetly while waiting for changes. You might want to look-up an
290
+ # alternative to this.
291
+ def changes(params = {}, payload = {}, &block)
292
+ view("_changes", params, payload, &block)
293
+ end
294
+
295
+ # Query a CouchDB-Lucene search view
296
+ def fti(name, params={})
297
+ # -> http://localhost:5984/yourdb/_fti/YourDesign/by_name?include_docs=true&q=plop*'
298
+ view("_fti/#{name}", params)
299
+ end
300
+ alias :search :fti
301
+
302
+ # load a set of documents by passing an array of ids
303
+ def get_bulk(ids)
304
+ all_docs(:keys => ids, :include_docs => true)
305
+ end
306
+ alias :bulk_load :get_bulk
307
+
308
+
309
+ # == Handling attachments
310
+
311
+ # GET an attachment directly from CouchDB
312
+ def fetch_attachment(doc, name)
313
+ uri = url_for_attachment(doc, name)
314
+ CouchRest.get uri, :raw => true
315
+ end
316
+
317
+ # PUT an attachment directly to CouchDB
318
+ 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))
322
+ end
323
+
324
+ # DELETE an attachment directly from CouchDB
325
+ def delete_attachment(doc, name, force=false)
326
+ uri = url_for_attachment(doc, name)
327
+ # this needs a rev
328
+ begin
329
+ CouchRest.delete(uri)
330
+ rescue Exception => error
331
+ if force
332
+ # get over a 409
333
+ doc = get(doc['_id'])
334
+ uri = url_for_attachment(doc, name)
335
+ CouchRest.delete(uri)
336
+ else
337
+ error
338
+ end
339
+ end
340
+ end
341
+
342
+
343
+
344
+ private
345
+
346
+ def replicate(other_db, continuous, options)
347
+ raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
348
+ raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source))
349
+ doc_ids = options.delete(:doc_ids)
350
+ payload = options
351
+ if options.has_key?(:target)
352
+ payload[:source] = other_db.root
353
+ else
354
+ payload[:target] = other_db.root
355
+ end
356
+ payload[:continuous] = continuous
357
+ 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}"
374
+ end
375
+
376
+ def url_for_attachment(doc, name)
377
+ @root + uri_for_attachment(doc, name)
378
+ end
379
+
380
+ def escape_docid id
381
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
382
+ end
383
+
384
+ def encode_attachments(attachments)
385
+ attachments.each do |k,v|
386
+ next if v['stub']
387
+ v['data'] = base64(v['data'])
388
+ end
389
+ attachments
390
+ end
391
+
392
+ def base64(data)
393
+ Base64.encode64(data).gsub(/\s/,'')
394
+ end
395
+
396
+ # Convert a simplified view name into a complete view path. If
397
+ # the name already starts with a "_" no alterations will be made.
398
+ def name_to_view_path(name)
399
+ name =~ /^([^_].+?)\/(.*)$/ ? "_design/#{$1}/_view/#{$2}" : name
400
+ end
401
+ end
402
+ end