jchris-couchrest 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
data/bin/couchview CHANGED
@@ -1,111 +1,50 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- commands = %w{push generate}
4
-
5
- command = ARGV[0]
6
-
7
- if !commands.include?(command)
8
- puts <<-USAGE
9
- Couchview has two modes: push and generate. Run couchview push or couchview generate for usage documentation.
10
- USAGE
11
- exit
3
+ require 'optparse'
4
+ require File.dirname(__FILE__) + "/../lib/couch_rest/commands"
5
+
6
+ # Set defaults
7
+ options = {
8
+ :loud => true,
9
+ }
10
+
11
+ opts = OptionParser.new do |opts|
12
+ opts.banner = "Usage: #$0 [options] (push|generate) directory database"
13
+ opts.on('-q', '--quiet', "Omit extra debug info") do
14
+ options[:loud] = false
15
+ end
16
+ opts.on_tail('-h', '--help [push|generate]', "Display detailed help and exit") do |help_command|
17
+ puts opts
18
+ case help_command
19
+ when "push"
20
+ puts CouchRest::Commands::Push.help
21
+ when "generate"
22
+ puts CouchRest::Commands::Generate.help
23
+ end
24
+ exit
25
+ end
12
26
  end
27
+ opts.parse!(ARGV)
13
28
 
14
- if ARGV.length == 1
15
- case command
16
- when "generate"
17
- puts <<-GEN
18
- Usage: couchview generate directory design1 design2 design3 ...
19
-
20
- Couchview will create directories and example views for the design documents you specify.
21
-
22
- GEN
23
- when "push"
24
- puts <<-PUSH
25
- == Pushing views with Couchview ==
26
-
27
- Usage: couchview push directory dbname
28
-
29
- Couchview expects a specific filesystem layout for your CouchDB views (see
30
- example below). It also supports advanced features like inlining of library
31
- code (so you can keep DRY) as well as avoiding unnecessary document
32
- modification.
33
-
34
- Couchview also solves a problem with CouchDB's view API, which only provides
35
- access to the final reduce side of any views which have both a map and a
36
- reduce function defined. The intermediate map results are often useful for
37
- development and production. CouchDB is smart enough to reuse map indexes for
38
- functions duplicated across views within the same design document.
39
-
40
- For views with a reduce function defined, Couchview creates both a reduce view
41
- and a map-only view, so that you can browse and query the map side as well as
42
- the reduction, with no performance penalty.
43
-
44
- == Example ==
45
-
46
- couchview push foo-project/bar-views baz-database
29
+ options[:command] = ARGV.shift
30
+ options[:directory] = ARGV.shift
31
+ options[:trailing_args] = ARGV
47
32
 
48
- This will push the views defined in foo-project/bar-views into a database
49
- called baz-database. Couchview expects the views to be defined in files with
50
- names like:
51
-
52
- foo-project/bar-views/my-design/viewname-map.js
53
- foo-project/bar-views/my-design/viewname-reduce.js
54
- foo-project/bar-views/my-design/noreduce-map.js
55
-
56
- Pushed to => http://localhost:5984/baz-database/_design/my-design
57
-
58
- And the design document:
59
- {
60
- "views" : {
61
- "viewname-map" : {
62
- "map" : "### contents of view-name-map.js ###"
63
- },
64
- "viewname-reduce" : {
65
- "map" : "### contents of view-name-map.js ###",
66
- "reduce" : "### contents of view-name-reduce.js ###"
67
- },
68
- "noreduce-map" : {
69
- "map" : "### contents of noreduce-map.js ###"
70
- }
71
- }
72
- }
73
-
74
- Couchview will create a design document for each subdirectory of the views
75
- directory specified on the command line.
76
-
77
- == Library Inlining ==
78
-
79
- Couchview can optionally inline library code into your views so you only have
80
- to maintain it in one place. It looks for any files named lib.* in your
81
- design-doc directory (for doc specific libs) and in the parent views directory
82
- (for project global libs). These libraries are only inserted into views which
83
- include the text
84
-
85
- //include-lib
86
-
87
- or
88
-
89
- #include-lib
90
-
91
- Couchview is a result of scratching my own itch. I'd be happy to make it more
92
- general, so please contact me at jchris@grabb.it if you'd like to see anything
93
- added or changed.
94
- PUSH
95
- end
33
+ # There must be a better way to check for extra required args
34
+ unless (["push", "generate"].include?(options[:command]) && options[:directory] && options[:trailing_args])
35
+ puts(opts)
96
36
  exit
97
37
  end
98
38
 
99
- require 'rubygems'
100
- require 'couchrest'
39
+ # The options hash now contains the resolved defaults
40
+ # and the overrides from the command line.
41
+
42
+ # Call your class and send it the options here
43
+ # cr = CouchRest::FileManager.new(options[:database_name])
101
44
 
102
- if command == 'push'
103
- dirname = ARGV[1]
104
- dbname = ARGV[2]
105
- fm = CouchRest::FileManager.new(dbname)
106
- fm.loud = true
107
- puts "Pushing views from directory #{dirname} to database #{fm.db}"
108
- fm.push_views(dirname)
109
- elsif command == 'generate'
110
- puts "Under construction ;)"
45
+ case options[:command]
46
+ when "push"
47
+ CouchRest::Commands::Push.run(options)
48
+ when "generate"
49
+ CouchRest::Commands::Generate.run(options)
111
50
  end
data/lib/couch_rest.rb CHANGED
@@ -35,7 +35,7 @@ class CouchRest
35
35
 
36
36
  # creates the database if it doesn't exist
37
37
  def database! name
38
- create_db(path) rescue nil
38
+ create_db(name) rescue nil
39
39
  database name
40
40
  end
41
41
 
@@ -0,0 +1,5 @@
1
+ require File.join(File.dirname(__FILE__), "..", "couchrest")
2
+
3
+ %w(push generate).each do |filename|
4
+ require File.join(File.dirname(__FILE__), "commands", filename)
5
+ end
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+
3
+ class 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,99 @@
1
+ class 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
+ puts "Pushing views from directory #{directory} to database #{fm.db}"
14
+ fm.push_views(directory)
15
+ end
16
+
17
+ def self.help
18
+ helpstring = <<-GEN
19
+
20
+ == Pushing views with Couchview ==
21
+
22
+ Usage: couchview push directory dbname
23
+
24
+ Couchview expects a specific filesystem layout for your CouchDB views (see
25
+ example below). It also supports advanced features like inlining of library
26
+ code (so you can keep DRY) as well as avoiding unnecessary document
27
+ modification.
28
+
29
+ Couchview also solves a problem with CouchDB's view API, which only provides
30
+ access to the final reduce side of any views which have both a map and a
31
+ reduce function defined. The intermediate map results are often useful for
32
+ development and production. CouchDB is smart enough to reuse map indexes for
33
+ functions duplicated across views within the same design document.
34
+
35
+ For views with a reduce function defined, Couchview creates both a reduce view
36
+ and a map-only view, so that you can browse and query the map side as well as
37
+ the reduction, with no performance penalty.
38
+
39
+ == Example ==
40
+
41
+ couchview push foo-project/bar-views baz-database
42
+
43
+ This will push the views defined in foo-project/bar-views into a database
44
+ called baz-database. Couchview expects the views to be defined in files with
45
+ names like:
46
+
47
+ foo-project/bar-views/my-design/viewname-map.js
48
+ foo-project/bar-views/my-design/viewname-reduce.js
49
+ foo-project/bar-views/my-design/noreduce-map.js
50
+
51
+ Pushed to => http://localhost:5984/baz-database/_design/my-design
52
+
53
+ And the design document:
54
+ {
55
+ "views" : {
56
+ "viewname-map" : {
57
+ "map" : "### contents of view-name-map.js ###"
58
+ },
59
+ "viewname-reduce" : {
60
+ "map" : "### contents of view-name-map.js ###",
61
+ "reduce" : "### contents of view-name-reduce.js ###"
62
+ },
63
+ "noreduce-map" : {
64
+ "map" : "### contents of noreduce-map.js ###"
65
+ }
66
+ }
67
+ }
68
+
69
+ Couchview will create a design document for each subdirectory of the views
70
+ directory specified on the command line.
71
+
72
+ == Library Inlining ==
73
+
74
+ Couchview can optionally inline library code into your views so you only have
75
+ to maintain it in one place. It looks for any files named lib.* in your
76
+ design-doc directory (for doc specific libs) and in the parent views directory
77
+ (for project global libs). These libraries are only inserted into views which
78
+ include the text
79
+
80
+ //include-lib
81
+
82
+ or
83
+
84
+ #include-lib
85
+
86
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
87
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
88
+ added or changed.
89
+
90
+ GEN
91
+ helpstring.gsub(/^ /, '')
92
+ end
93
+
94
+ end
95
+
96
+
97
+ end
98
+
99
+ end
data/lib/couchrest.rb CHANGED
@@ -8,4 +8,22 @@ require File.dirname(__FILE__) + '/pager'
8
8
  require File.dirname(__FILE__) + '/file_manager'
9
9
  require File.dirname(__FILE__) + '/streamer'
10
10
 
11
+ # this has to come after the JSON gem
11
12
 
13
+ # this date format sorts lexicographically
14
+ # and is compatible with Javascript's new Date(time_string) constructor
15
+ # note that sorting will break if you store times from multiple timezones
16
+ # I like to add a ENV['TZ'] = 'UTC' to my apps
17
+ class Time
18
+ def to_json(options = nil)
19
+ %("#{strftime("%Y/%m/%d %H:%M:%S %z")}")
20
+ end
21
+ # this works to decode the outputted time format
22
+ # from ActiveSupport
23
+ # def self.parse string, fallback=nil
24
+ # d = DateTime.parse(string).new_offset
25
+ # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
26
+ # rescue
27
+ # fallback
28
+ # end
29
+ end
data/lib/file_manager.rb CHANGED
@@ -84,7 +84,7 @@ class CouchRest
84
84
 
85
85
  doc["_attachments"][path] = {
86
86
  "data" => content,
87
- "content_type" => @content_types[path.split('.').last]
87
+ "content_type" => MIMES[path.split('.').last]
88
88
  }
89
89
  end
90
90
 
@@ -151,6 +151,40 @@ class CouchRest
151
151
  designs
152
152
  end
153
153
 
154
+ def pull_views(view_dir)
155
+ prefix = "_design"
156
+ ds = db.documents(:startkey => '#{prefix}/', :endkey => '#{prefix}/ZZZZZZZZZ')
157
+ ds['rows'].collect{|r|r['id']}.each do |id|
158
+ puts directory = id.split('/').last
159
+ FileUtils.mkdir_p(File.join(view_dir,directory))
160
+ views = db.get(id)['views']
161
+
162
+ vgroups = views.keys.group_by{|k|k.sub(/\-(map|reduce)$/,'')}
163
+ vgroups.each do|g,vs|
164
+ mapname = vs.find {|v|views[v]["map"]}
165
+ if mapname
166
+ # save map
167
+ mapfunc = views[mapname]["map"]
168
+ mapfile = File.join(view_dir, directory, "#{g}-map.js") # todo support non-js views
169
+ File.open(mapfile,'w') do |f|
170
+ f.write mapfunc
171
+ end
172
+ end
173
+
174
+ reducename = vs.find {|v|views[v]["reduce"]}
175
+ if reducename
176
+ # save reduce
177
+ reducefunc = views[reducename]["reduce"]
178
+ reducefile = File.join(view_dir, directory, "#{g}-reduce.js") # todo support non-js views
179
+ File.open(reducefile,'w') do |f|
180
+ f.write reducefunc
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ end
187
+
154
188
 
155
189
  private
156
190
 
@@ -187,210 +221,3 @@ class CouchRest
187
221
  end
188
222
  end
189
223
  end
190
-
191
- __END__
192
-
193
-
194
-
195
-
196
- # parse the file structure to load the public files, controllers, and views into a hash with the right shape for coucdb
197
- couch = {}
198
-
199
- couch["public"] = Dir["#{File.expand_path(File.dirname("."))}/public/**/*.*"].collect do |f|
200
- {f.split("public/").last => open(f).read}
201
- end
202
-
203
- couch["controllers"] = {}
204
- Dir["#{File.expand_path(File.dirname("."))}/app/controllers/**/*.*"].collect do |c|
205
- path_parts = c.split("/")
206
-
207
- controller_name = path_parts[path_parts.length - 2]
208
- action_name = path_parts[path_parts.length - 1].split(".").first
209
-
210
- couch["controllers"][controller_name] ||= {"actions" => {}}
211
- couch["controllers"][controller_name]["actions"][action_name] = open(c).read
212
-
213
- end
214
-
215
- couch["designs"] = {}
216
- Dir["#{File.expand_path(File.dirname("."))}/app/views/**/*.*"].collect do |design_doc|
217
- design_doc_parts = design_doc.split('/')
218
- pre_normalized_view_name = design_doc_parts.last.split("-")
219
- view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-")
220
-
221
- folder = design_doc.split("app/views").last.split("/")[1]
222
-
223
- couch["designs"][folder] ||= {}
224
- couch["designs"][folder]["views"] ||= {}
225
- couch["designs"][folder]["language"] ||= LANGS[design_doc_parts.last.split(".").last]
226
-
227
- if design_doc_parts.last =~ /-map/
228
- couch["designs"][folder]["views"]["#{view_name}-map"] ||= {}
229
-
230
- couch["designs"][folder]["views"]["#{view_name}-map"]["map"] = open(design_doc).read
231
-
232
- couch["designs"][folder]["views"]["#{view_name}-reduce"] ||= {}
233
- couch["designs"][folder]["views"]["#{view_name}-reduce"]["map"] = open(design_doc).read
234
- end
235
-
236
- if design_doc_parts.last =~ /-reduce/
237
- couch["designs"][folder]["views"]["#{view_name}-reduce"] ||= {}
238
-
239
- couch["designs"][folder]["views"]["#{view_name}-reduce"]["reduce"] = open(design_doc).read
240
- end
241
- end
242
-
243
- # cleanup empty maps and reduces
244
- couch["designs"].each do |name, props|
245
- props["views"].delete("#{name}-reduce") unless props["views"]["#{name}-reduce"].keys.include?("reduce")
246
- end
247
-
248
- # parsing done, begin posting
249
-
250
- # connect to couchdb
251
- cr = CouchRest.new("http://localhost:5984")
252
- @db = cr.database(DBNAME)
253
-
254
- def create_or_update(id, fields)
255
- existing = get(id)
256
-
257
- if existing
258
- updated = fields.merge({"_id" => id, "_rev" => existing["_rev"]})
259
- else
260
- puts "saving #{id}"
261
- save(fields.merge({"_id" => id}))
262
- end
263
-
264
- if existing == updated
265
- puts "no change to #{id}. skipping..."
266
- else
267
- puts "replacing #{id}"
268
- save(updated)
269
- end
270
-
271
- end
272
-
273
- def get(id)
274
- doc = handle_errors do
275
- @db.get(id)
276
- end
277
- end
278
-
279
- def save(doc)
280
- handle_errors do
281
- @db.save(doc)
282
- end
283
- end
284
-
285
- def handle_errors(&block)
286
- begin
287
- yield
288
- rescue Exception => e
289
- # puts e.message
290
- nil
291
- end
292
- end
293
-
294
-
295
- if todo.include? "views"
296
- puts "posting views into CouchDB"
297
- couch["designs"].each do |k,v|
298
- create_or_update("_design/#{k}", v)
299
- end
300
- puts
301
- end
302
-
303
- if todo.include? "controllers"
304
- puts "posting controllers into CouchDB"
305
- couch["controllers"].each do |k,v|
306
- create_or_update("controller/#{k}", v)
307
- end
308
- puts
309
- end
310
-
311
-
312
- if todo.include? "public"
313
- puts "posting public docs into CouchDB"
314
-
315
- if couch["public"].empty?
316
- puts "no docs in public"; exit
317
- end
318
-
319
- @content_types = {
320
- "html" => "text/html",
321
- "htm" => "text/html",
322
- "png" => "image/png",
323
- "css" => "text/css",
324
- "js" => "test/javascript"
325
- }
326
-
327
- def md5 string
328
- Digest::MD5.hexdigest(string)
329
- end
330
-
331
- @attachments = {}
332
- @signatures = {}
333
- couch["public"].each do |doc|
334
- @signatures[doc.keys.first] = md5(doc.values.first)
335
-
336
- @attachments[doc.keys.first] = {
337
- "data" => doc.values.first,
338
- "content_type" => @content_types[doc.keys.first.split('.').last]
339
- }
340
- end
341
-
342
- doc = get("public")
343
-
344
- unless doc
345
- puts "creating public"
346
- @db.save({"_id" => "public", "_attachments" => @attachments, "signatures" => @signatures})
347
- exit
348
- end
349
-
350
- # remove deleted docs
351
- to_be_removed = doc["signatures"].keys.select{|d| !couch["public"].collect{|p| p.keys.first}.include?(d) }
352
-
353
- to_be_removed.each do |p|
354
- puts "deleting #{p}"
355
- doc["signatures"].delete(p)
356
- doc["_attachments"].delete(p)
357
- end
358
-
359
- # update existing docs:
360
- doc["signatures"].each do |path, sig|
361
- if (@signatures[path] == sig)
362
- puts "no change to #{path}. skipping..."
363
- else
364
- puts "replacing #{path}"
365
- doc["signatures"][path] = md5(@attachments[path]["data"])
366
- doc["_attachments"][path].delete("stub")
367
- doc["_attachments"][path].delete("length")
368
- doc["_attachments"][path]["data"] = @attachments[path]["data"]
369
- doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} )
370
-
371
- end
372
- end
373
-
374
- # add in new files
375
- new_files = couch["public"].select{|d| !doc["signatures"].keys.include?( d.keys.first) }
376
-
377
- new_files.each do |f|
378
- puts "creating #{f}"
379
- path = f.keys.first
380
- content = f.values.first
381
- doc["signatures"][path] = md5(content)
382
-
383
- doc["_attachments"][path] = {
384
- "data" => content,
385
- "content_type" => @content_types[path.split('.').last]
386
- }
387
- end
388
-
389
- begin
390
- @db.save(doc)
391
- rescue Exception => e
392
- puts e.message
393
- end
394
-
395
- puts
396
- 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.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2008-08-03 00:00:00 -07:00
13
+ date: 2008-09-10 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -47,6 +47,9 @@ files:
47
47
  - lib/pager.rb
48
48
  - lib/file_manager.rb
49
49
  - lib/streamer.rb
50
+ - lib/couch_rest/commands.rb
51
+ - lib/couch_rest/commands/generate.rb
52
+ - lib/couch_rest/commands/push.rb
50
53
  - Rakefile
51
54
  - README.markdown
52
55
  - bin/couchdir