dpla-couchrest 1.2.1.pre.dpla

Sign up to get free protection for your applications and to get access to all the features.
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