couch-quilt 0.2.1

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