couch-quilt 0.2.1

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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ coverage
2
+ pkg
3
+ rdoc
data/INSTALL ADDED
@@ -0,0 +1,32 @@
1
+ Installing Quilt
2
+ ================
3
+
4
+ Dependencies
5
+ ------------
6
+
7
+ Quilt is written in Ruby using the Ruby FuseFS library.
8
+ So all you need is Ruby itself, the Rubygems package manager and the ruby fusefs library.
9
+ On Debian derivates you can achieve that using the following command:
10
+
11
+ apt-get install ruby rubygems libfusefs-ruby
12
+
13
+
14
+ Installation
15
+ ------------
16
+
17
+ As soon I released this as a gem, you will be able to install via Rubygems:
18
+
19
+ gem install quilt
20
+
21
+
22
+ Problems
23
+ --------
24
+
25
+ On Ubuntu the fuse install lack adequate permission settings:
26
+
27
+ fusermount: failed to open /etc/fuse.conf: Permission denied
28
+
29
+
30
+ The following line can be used to fix this:
31
+
32
+ chmod a+rwx /etc/fuse.conf
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ Quilt
2
+ =====
3
+
4
+ Read and write CouchDB documents via FUSE userspace filesystem
5
+
6
+
7
+ Install dependencies
8
+ --------------------
9
+
10
+ apt-get install ruby rubygems libfusefs-ruby
11
+
12
+
13
+ Install Quilt
14
+ -------------
15
+
16
+ gem install couch-quilt
17
+
18
+
19
+ Getting started
20
+ ---------------
21
+
22
+ start quilt by typing
23
+
24
+ couchquilt mountpoint [server] [--debug]
25
+
26
+
27
+ Your mapped CouchDB will now be available at *mountpoint*.
28
+
29
+ You can create databases and documents with mkdir, add properties via echo >> and so on.
30
+ Databases and documents can be deleted via a touch /database_id/_delete or /database_id/document_id/_delete.
31
+
32
+
33
+ Read more on the [projectpage](http://jo.github.com/quilt/).
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/rdoctask'
4
+ require File.join(File.dirname(__FILE__), 'lib', 'couchquilt', 'version')
5
+
6
+ desc 'Default: run specs.'
7
+ task :default => :spec
8
+
9
+ desc "Run all examples"
10
+ Spec::Rake::SpecTask.new(:spec) do |t|
11
+ t.spec_files = FileList['spec/*_spec.rb']
12
+ t.spec_opts = ["--color"]
13
+ end
14
+
15
+ desc "Run all examples with RCov"
16
+ Spec::Rake::SpecTask.new(:coverage) do |t|
17
+ t.spec_files = FileList['spec/*_spec.rb']
18
+ t.rcov = true
19
+ t.rcov_opts = ['--exclude', '/var/lib/gems', '--exclude', "spec"]
20
+ end
21
+
22
+ begin
23
+ require 'jeweler'
24
+ Jeweler::Tasks.new do |s|
25
+ s.name = "couch-quilt"
26
+ s.version = Couchquilt::VERSION
27
+ s.summary = "Access CouchDB from filesystem."
28
+ s.email = "schmidt@netzmerk.com"
29
+ s.homepage = "http://jo.github.com/quilt"
30
+ s.description = "Access CouchDB JSON documents from filesystem."
31
+ s.authors = ['Johannes Jörg Schmidt']
32
+ s.rubyforge_project = "couch-quilt"
33
+ #s.files = FileList["[A-Z]*(.rdoc)", "{bin,lib,spec}/**/*", "README.md", "INSTALL", "Rakefile"]
34
+ end
35
+
36
+ Jeweler::GemcutterTasks.new
37
+ rescue LoadError
38
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
39
+ end
data/bin/couchquilt ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright 2010 Johannes J. Schmidt, TF
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # usage:
17
+ # couchquilt ~/quilt http://localhost:5984
18
+
19
+ QUILT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
20
+ require File.join(QUILT_ROOT, "lib/couchquilt")
21
+
22
+ DEBUG = ARGV.delete("--debug")
23
+ require File.join(File.dirname(__FILE__), '../lib/couchquilt/debugged_fs') if DEBUG
24
+
25
+ if ARGV[0].nil?
26
+ puts "usage:"
27
+ puts "couchquilt mountpoint [server] [--debug]"
28
+ exit
29
+ end
30
+
31
+ # database server url defaults to http://127.0.0.1:5984
32
+ # couch fs will be mounted on ./app/127.0.0.1:5984 per default
33
+ mountpoint = ARGV[0]
34
+ server = ARGV[1] || "http://127.0.0.1:5984"
35
+
36
+ # create mount point if needed
37
+ Dir.mkdir(mountpoint) unless File.directory?(mountpoint)
38
+
39
+ # init quilt fs
40
+ quilt_fs_class = DEBUG ? Couchquilt::DebuggedFS : Couchquilt::FS
41
+ FuseFS.set_root quilt_fs_class.new(server)
42
+ FuseFS.mount_under mountpoint
43
+
44
+ # listen for exit signals and unmount fuse
45
+ trap("INT") do
46
+ puts "ancelling..."
47
+ FuseFS.unmount
48
+ FuseFS.exit
49
+ puts "Bye."
50
+ end
51
+
52
+ # actually do the Fuse mount
53
+ puts "Quilt maps #{server} to #{mountpoint}"
54
+ puts "Debug mode" if DEBUG
55
+ FuseFS.run
@@ -0,0 +1,56 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{couch-quilt}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Johannes J\303\266rg Schmidt"]
12
+ s.date = %q{2010-02-25}
13
+ s.default_executable = %q{couchquilt}
14
+ s.description = %q{Access CouchDB JSON documents from filesystem.}
15
+ s.email = %q{schmidt@netzmerk.com}
16
+ s.executables = ["couchquilt"]
17
+ s.extra_rdoc_files = [
18
+ "README.md"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "INSTALL",
23
+ "README.md",
24
+ "Rakefile",
25
+ "bin/couchquilt",
26
+ "couch-quilt.gemspec",
27
+ "lib/couchquilt.rb",
28
+ "lib/couchquilt/couch_client.rb",
29
+ "lib/couchquilt/debugged_fs.rb",
30
+ "lib/couchquilt/fs.rb",
31
+ "lib/couchquilt/version.rb",
32
+ "spec/couchquilt_spec.rb",
33
+ "spec/spec_helper.rb"
34
+ ]
35
+ s.homepage = %q{http://jo.github.com/quilt}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubyforge_project = %q{couch-quilt}
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{Access CouchDB from filesystem.}
41
+ s.test_files = [
42
+ "spec/couchquilt_spec.rb",
43
+ "spec/spec_helper.rb"
44
+ ]
45
+
46
+ if s.respond_to? :specification_version then
47
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
+ else
52
+ end
53
+ else
54
+ end
55
+ end
56
+
data/lib/couchquilt.rb ADDED
@@ -0,0 +1,37 @@
1
+ # Copyright 2010 Johannes J. Schmidt, TF
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # Quilt mixes CouchDB Design Documents into FUSE
16
+
17
+
18
+ require "rubygems"
19
+ begin
20
+ require 'json'
21
+ rescue LoadError
22
+ raise "You need install and require your own json compatible library since Quilt couldn't load the json/json_pure gem" unless Kernel.const_defined?("JSON")
23
+ end
24
+ require "rest_client"
25
+ require 'fusefs'
26
+
27
+ $:.unshift File.dirname(__FILE__) unless
28
+ $:.include?(File.dirname(__FILE__)) ||
29
+ $:.include?(File.expand_path(File.dirname(__FILE__)))
30
+
31
+ require 'couchquilt/couch_client'
32
+ require 'couchquilt/fs'
33
+
34
+
35
+ # Set out your Quilt
36
+ module Couchquilt
37
+ end
@@ -0,0 +1,44 @@
1
+ # speaking to CouchDB server
2
+ module Couchquilt
3
+ class CouchClient
4
+ def initialize(server_name)
5
+ @server_name = server_name
6
+ end
7
+
8
+ # initiates a GET request and returns the JSON parsed response
9
+ def get(path)
10
+ response = RestClient.get(url_for(path))
11
+ JSON.parse(response) rescue response
12
+ rescue RestClient::ResourceNotFound
13
+ nil
14
+ end
15
+
16
+ # initiates a PUT request and returns true if it was successful
17
+ def put(path, payload = {})
18
+ RestClient.put url_for(path), payload.to_json
19
+ true
20
+ end
21
+
22
+ # initiates a DELETE request and returns true if it was successful
23
+ def delete(path)
24
+ RestClient.delete url_for(path)
25
+ true
26
+ rescue
27
+ false
28
+ end
29
+
30
+ # initiates a HEAD request to +url+ and returns true if the resource exists
31
+ def head(path)
32
+ RestClient.head url_for(path)
33
+ true
34
+ rescue RestClient::ResourceNotFound
35
+ false
36
+ end
37
+
38
+ private
39
+
40
+ def url_for(path)
41
+ File.join(@server_name, path)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ module Couchquilt
2
+ # proxy sends all requests to QuiltFS
3
+ # used for printing debug information
4
+ class DebuggedFS
5
+ def initialize(server_name)
6
+ @quilt = FS.new(server_name)
7
+ rescue => e
8
+ STDERR.puts e.message, e.backtrace
9
+ end
10
+
11
+ (FS.public_instance_methods - public_instance_methods).each do |method|
12
+ class_eval <<-STR
13
+ def #{method}(*args)
14
+ @quilt.#{method}(*args)
15
+ rescue => e
16
+ STDOUT.puts "#{method}", args.inspect, e.class, e.message, e.backtrace
17
+ end
18
+ STR
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,414 @@
1
+ require 'cgi'
2
+ require 'uri'
3
+
4
+ module Couchquilt
5
+ class FS
6
+ # initializes Quilt FS with the database server name
7
+ def initialize(server_name)
8
+ @couch = CouchClient.new(server_name)
9
+ end
10
+
11
+ # list contents of path
12
+ def contents(path)
13
+ database, id, *parts = extract_parts(path)
14
+
15
+ list case named_path(path)
16
+ when :root
17
+ @couch.get("_all_dbs")
18
+ when :database
19
+ ["_design"] +
20
+ # database meta data
21
+ map_json(@couch.get(database)) +
22
+ # all documents but design documents
23
+ # Note: we can not use ?startkey="_design/"&endkey="_design0" here,
24
+ # because that would return no results for databases without design documents
25
+ @couch.get("#{database}/_all_docs")["rows"].map { |r| r["id"] }.select { |id| id !~ /^_design\// }
26
+ when :_design
27
+ query = URI.encode('startkey="_design"&endkey="_design0"')
28
+ # all design documents
29
+ @couch.get("#{database}/_all_docs?#{query}")["rows"].map { |r| r["id"].sub("_design/", "") }
30
+ when :_show
31
+ (@couch.get("#{database}/#{id}")["shows"] || {}).keys
32
+ when :_list
33
+ (@couch.get("#{database}/#{id}")["lists"] || {}).keys
34
+ when :_view
35
+ (@couch.get("#{database}/#{id}")["views"] || {}).keys
36
+ when :list_function
37
+ (@couch.get("#{database}/#{id}")["views"] || {}).keys.map { |name| "#{name}.html" }
38
+ when :show_function
39
+ query = URI.encode('startkey="_design0"')
40
+ @couch.get("#{database}/_all_docs?#{query}")["rows"].map { |r| "#{r["id"]}.html" }
41
+ when :view_function, :view_function_result
42
+ map_json(get_view_result_part(database, id, parts))
43
+ when :design_document
44
+ ["_list", "_show", "_view"] +
45
+ map_json(get_document_part(database, id))
46
+ else
47
+ map_json(get_document_part(database, id, parts))
48
+ end
49
+ end
50
+
51
+ # is path a directory?
52
+ def directory?(path)
53
+ database, id, *parts = extract_parts(path)
54
+
55
+ case named_path(path)
56
+ when :database, :document, :design_document
57
+ @couch.head(path)
58
+ when :view_function_result
59
+ doc = get_view_result_part(database, id, parts)
60
+ # arrays and hashes are mapped into directories
61
+ doc.is_a?(Hash) || doc.is_a?(Array)
62
+ when :database_info, :show_function_result, :list_function_result
63
+ false
64
+ when nil
65
+ # look into document
66
+ doc = get_document_part(database, id, parts)
67
+ # arrays and hashes are mapped into directories
68
+ doc.is_a?(Hash) || doc.is_a?(Array)
69
+ else
70
+ # all other special paths are directories by now
71
+ # TODO: thats not so good.
72
+ true
73
+ end
74
+ end
75
+
76
+ # is path a file?
77
+ def file?(path)
78
+ database, id, *parts = extract_parts(path)
79
+
80
+ case named_path(path)
81
+ when :database_info, :show_function_result, :list_function_result
82
+ true
83
+ when :view_function_result
84
+ # Every javascript or HTML is file, based on extension
85
+ [".js", ".html"].include?(File.extname(path))
86
+ when nil
87
+ # look into document
88
+ doc = get_document_part(database, id, parts)
89
+ # only arrays and hashes are mapped into directories
90
+ !doc.nil? && !(doc.is_a?(Hash) || doc.is_a?(Array))
91
+ else
92
+ false
93
+ end
94
+ end
95
+
96
+ # reading file contents of path
97
+ def read_file(path)
98
+ database, id, *parts = extract_parts(path)
99
+
100
+ content case named_path(path)
101
+ when :database_info
102
+ @couch.get(database)[remove_extname(id)]
103
+ when :show_function_result
104
+ parts.shift
105
+ @couch.get(remove_extname(File.join(database, id, "_show", *parts)))
106
+ when :list_function_result
107
+ parts.shift
108
+ @couch.get(remove_extname(File.join(database, id, "_list", *parts)))
109
+ when :view_function_result
110
+ get_view_result_part(database, id, parts)
111
+ else
112
+ get_document_part(database, id, parts)
113
+ end
114
+ end
115
+
116
+ # is path writable?
117
+ # every javascript file is writable, except ones starting with an underscore
118
+ def can_write?(path)
119
+ database, id, *parts = extract_parts(path)
120
+
121
+ case named_path(path)
122
+ when :switch_delete_database, :switch_delete_document
123
+ true
124
+ when :database_info, :show_function_result, :list_function_result, :view_function_result
125
+ false
126
+ else
127
+ File.basename(path) !~ /\A_/ && File.extname(path) == ".js"
128
+ end
129
+ end
130
+
131
+ # writes content str to path
132
+ def write_to(path, str)
133
+ database, id, *parts = extract_parts(path)
134
+ # fetch document
135
+ doc = @couch.get("#{database}/#{id}")
136
+ # update the value that the file at path holds
137
+ update_value(doc, parts, str)
138
+ # save document
139
+ @couch.put("#{database}/#{id}", doc)
140
+ end
141
+
142
+ # can I delete path?
143
+ def can_delete?(path)
144
+ return false if File.basename(path) =~ /\A_/
145
+
146
+ case named_path(path)
147
+ when :database_info, :show_function_result, :list_function_result, :view_function_result
148
+ false
149
+ else
150
+ true
151
+ end
152
+ end
153
+
154
+ # deletes path
155
+ # either deletes a database, document or removes a part of a document
156
+ def delete(path)
157
+ database, id, *parts = extract_parts(path)
158
+
159
+ # fetch document
160
+ doc = @couch.get("#{database}/#{id}")
161
+ # remove object
162
+ remove_object(doc, parts)
163
+ # save document
164
+ @couch.put("#{database}/#{id}", doc)
165
+ end
166
+
167
+ # can I make a directory at path?
168
+ def can_mkdir?(path)
169
+ database, id, *parts = extract_parts(path)
170
+
171
+ case named_path(path)
172
+ when :root, :_design, :_list, :list_function, :_show, :show_function, :_view, :view_function, :view_function_result
173
+ false
174
+ when :database, :document, :design_document
175
+ # can create database or document unless exists
176
+ !@couch.head(path)
177
+ else
178
+ !get_document_part(database, id, parts)
179
+ end
180
+ end
181
+
182
+ # makes a directory
183
+ # this creates either a database, a document or inserts an object into a document
184
+ def mkdir(path)
185
+ database, id, *parts = extract_parts(path)
186
+
187
+ case named_path(path)
188
+ when :database
189
+ @couch.put(database)
190
+ when :document, :design_document
191
+ @couch.put("#{database}/#{id}")
192
+ else
193
+ # fetch document
194
+ doc = @couch.get("#{database}/#{id}")
195
+ # insert empty object
196
+ update_value(doc, parts, {})
197
+ # save document
198
+ @couch.put("#{database}/#{id}", doc)
199
+ end
200
+ end
201
+
202
+ # can I remove a directory at path?
203
+ def can_rmdir?(path)
204
+ database, id, *parts = extract_parts(path)
205
+
206
+ case named_path(path)
207
+ when :root, :_design, :_list, :list_function, :_show, :show_function, :_view, :view_function, :view_function_result, :database, :document, :design_document
208
+ false
209
+ else
210
+ get_document_part(database, id, parts).empty? rescue nil
211
+ end
212
+ end
213
+
214
+ # deletes a directory
215
+ # that is a part of a document
216
+ def rmdir(path)
217
+ database, id, *parts = extract_parts(path)
218
+
219
+ # fetch document
220
+ doc = @couch.get("#{database}/#{id}")
221
+ # remove object
222
+ update_value(doc, parts, nil)
223
+ # save document
224
+ @couch.put("#{database}/#{id}", doc)
225
+ end
226
+
227
+ # switch to delete a database or document
228
+ def touch(path)
229
+ database, id, *parts = extract_parts(path)
230
+
231
+ case named_path(path)
232
+ when :switch_delete_database
233
+ @couch.delete(database)
234
+ when :switch_delete_document
235
+ doc = @couch.get("#{database}/#{id}")
236
+ @couch.delete("#{database}/#{id}?rev=#{doc["_rev"]}")
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ # gets the database, id and parts from path
243
+ def extract_parts(path)
244
+ database, id, *parts = path.scan(/[^\/]+/)
245
+ if id == "_design" && !parts.empty?
246
+ id << "/#{parts.shift}"
247
+ end
248
+ [database, id] + parts
249
+ end
250
+
251
+ # returns a path identifier for special paths
252
+ def named_path(path)
253
+ database, id, *parts = extract_parts(path)
254
+
255
+ if database.nil?
256
+ # /
257
+ :root
258
+ elsif id == "_delete"
259
+ :switch_delete_database
260
+ elsif parts.size == 1 && parts.first == "_delete"
261
+ :switch_delete_document
262
+ elsif id.nil?
263
+ # /database_id
264
+ :database
265
+ elsif ["compact_running.b.js", "db_name.js", "disk_format_version.i.js", "disk_size.i.js", "doc_count.i.js", "doc_del_count.i.js", "instance_start_time.js", "purge_seq.i.js", "update_seq.i.js"].include?(id) && parts.empty?
266
+ :database_info
267
+ elsif id == "_design" && parts.empty?
268
+ # /database_id/_design
269
+ :_design
270
+ elsif id =~ /_design\// && parts.empty?
271
+ # /database_id/_design/design_document_id
272
+ :design_document
273
+ elsif id =~ /_design\// && parts == ["_show"]
274
+ # /database_id/_design/design_document_id/_show
275
+ :_show
276
+ elsif id =~ /_design\// && parts == ["_list"]
277
+ # /database_id/_design/design_document_id/_list
278
+ :_list
279
+ elsif id =~ /_design\// && parts == ["_view"]
280
+ # /database_id/_design/design_document_id/_view
281
+ :_view
282
+ elsif id =~ /_design\// && parts.size == 2 && parts.first == "_list"
283
+ # /database_id/_design/design_document_id/_list/list_function_name
284
+ :list_function
285
+ elsif id =~ /_design\// && parts.size == 3 && parts.first == "_list"
286
+ # /database_id/_design/design_document_id/_list/list_function_name/view_function_name
287
+ :list_function_result
288
+ elsif id =~ /_design\// && parts.size == 2 && parts.first == "_show"
289
+ # /database_id/_design/design_document_id/_show/show_function_name
290
+ :show_function
291
+ elsif id =~ /_design\// && parts.size == 3 && parts.first == "_show"
292
+ # /database_id/_design/design_document_id/_show/show_function_name/document_id
293
+ :show_function_result
294
+ elsif id =~ /_design\// && parts.size == 2 && parts.first == "_view"
295
+ # /database_id/_design/design_document_id/_view/view_function_name
296
+ :view_function
297
+ elsif id =~ /_design\// && parts.size >= 3 && parts.first == "_view"
298
+ # /database_id/_design/design_document_id/_view/view_function_name/document_id
299
+ :view_function_result
300
+ elsif parts.empty?
301
+ # /database_id/document_id
302
+ :document
303
+ else
304
+ # /database_id/document_id/object
305
+ # /database_id/_design/design_document_id
306
+ # /database_id/_design/design_document_id/object
307
+ nil
308
+ end
309
+ end
310
+
311
+ # maps json contents into contents array
312
+ def map_json(doc)
313
+ case doc
314
+ when Hash
315
+ # Hash is mapped to directory
316
+ doc.keys.sort.map { |k| append_extname(k, doc[k]) }
317
+ when Array
318
+ # Array is mapped to directory
319
+ doc.map { |k| append_extname(doc.index(k), k) }
320
+ end
321
+ end
322
+
323
+ # fetch part of document
324
+ def get_document_part(database, id, parts = [])
325
+ doc = @couch.get("#{database}/#{id}")
326
+ get_part(doc, parts)
327
+ end
328
+
329
+ # get view result, or a part of that document.
330
+ def get_view_result_part(database, id, parts = [])
331
+ a, view_function_name, *rest = parts
332
+ view = [id.sub("_design/", ""), "_view", view_function_name].join("/")
333
+ doc = @couch.get("#{database}/_design/#{view}")
334
+ get_part(doc, rest)
335
+ end
336
+
337
+ # get a part of the document
338
+ # eg: get_part({ :a => { :b => :c}}, [:a, :b]) #=> :c
339
+ # also removes the file extension and gets index of arrays
340
+ def get_part(doc, parts)
341
+ return if doc.nil?
342
+ doc = doc.dup
343
+ parts.each do |part|
344
+ case doc
345
+ when Hash
346
+ doc = doc[remove_extname(part)]
347
+ when Array
348
+ doc = doc[part.to_i]
349
+ end
350
+ end
351
+ doc
352
+ end
353
+
354
+ # updates a part of a hash
355
+ # Example:
356
+ # update_value({:a => { :b => 'c'}}, [:a, :b], 'cis') #=> {:a => { :b => 'cis'}}
357
+ def update_value(hash, keys, value)
358
+ key = remove_extname(keys.shift)
359
+ if keys.empty?
360
+ hash[key] = value
361
+ else
362
+ hash[key] = update_value(hash[key], keys, value)
363
+ end
364
+ hash
365
+ end
366
+
367
+ # removes an object from a hash
368
+ # Example:
369
+ # remove_object({:a => { :b => 'c'}}, [:a, :b]) #=> {:a => { }}
370
+ def remove_object(hash, keys)
371
+ key = remove_extname(keys.shift)
372
+ if keys.empty?
373
+ hash.delete(key)
374
+ else
375
+ hash[key] = remove_object(hash[key], keys)
376
+ end
377
+ hash
378
+ end
379
+
380
+ # remove extname to get the id
381
+ def remove_extname(filename)
382
+ filename.sub(/((\.(f|i|b))?\.js|\.html)\z/, "")
383
+ end
384
+
385
+ # Appends extname, that is: builds a filename from key and value.
386
+ # Note: values are casted by extension.
387
+ def append_extname(key, value)
388
+ basename = key.is_a?(Integer) ? "%.3d" % key : key
389
+
390
+ case value
391
+ when Float
392
+ "#{basename}.f.js"
393
+ when Integer
394
+ "#{basename}.i.js"
395
+ when nil, String
396
+ "#{basename}.js"
397
+ when true, false
398
+ "#{basename}.b.js"
399
+ else
400
+ basename
401
+ end
402
+ end
403
+
404
+ # escapes the value for using as filename
405
+ def list(array)
406
+ return [] if array.nil?
407
+ array.compact.map { |v| CGI.escape(v) }.sort
408
+ end
409
+
410
+ def content(value)
411
+ value.to_s
412
+ end
413
+ end
414
+ end