jchris-couchrest 0.9.2 → 0.9.3

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/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