From 40f22ffadc674fe18b69abc6798c42aae5cd622b Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 20:48:17 -0700 Subject: [PATCH 01/14] fix broken CouchRest.database! method --- lib/couch_rest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couch_rest.rb b/lib/couch_rest.rb index dd31e67..fa5e8ab 100644 --- a/lib/couch_rest.rb +++ b/lib/couch_rest.rb @@ -35,7 +35,7 @@ class CouchRest # creates the database if it doesn't exist def database! name - create_db(path) rescue nil + create_db(name) rescue nil database name end From fa1ef4b4f933e4dc9cb9f5b4df90597ae23d0b43 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 20:53:38 -0700 Subject: [PATCH 02/14] slash and burn reorg --- lib/{couch_rest => }/commands.rb | 0 lib/{couch_rest => }/commands/generate.rb | 0 lib/{couch_rest => }/commands/push.rb | 0 lib/{ => couchrest}/couchrest.rb | 0 lib/{ => couchrest}/database.rb | 0 lib/{ => helpers}/file_manager.rb | 0 lib/{ => helpers}/pager.rb | 0 lib/{ => helpers}/streamer.rb | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename lib/{couch_rest => }/commands.rb (100%) rename lib/{couch_rest => }/commands/generate.rb (100%) rename lib/{couch_rest => }/commands/push.rb (100%) rename lib/{ => couchrest}/couchrest.rb (100%) rename lib/{ => couchrest}/database.rb (100%) rename lib/{ => helpers}/file_manager.rb (100%) rename lib/{ => helpers}/pager.rb (100%) rename lib/{ => helpers}/streamer.rb (100%) diff --git a/lib/couch_rest/commands.rb b/lib/commands.rb similarity index 100% rename from lib/couch_rest/commands.rb rename to lib/commands.rb diff --git a/lib/couch_rest/commands/generate.rb b/lib/commands/generate.rb similarity index 100% rename from lib/couch_rest/commands/generate.rb rename to lib/commands/generate.rb diff --git a/lib/couch_rest/commands/push.rb b/lib/commands/push.rb similarity index 100% rename from lib/couch_rest/commands/push.rb rename to lib/commands/push.rb diff --git a/lib/couchrest.rb b/lib/couchrest/couchrest.rb similarity index 100% rename from lib/couchrest.rb rename to lib/couchrest/couchrest.rb diff --git a/lib/database.rb b/lib/couchrest/database.rb similarity index 100% rename from lib/database.rb rename to lib/couchrest/database.rb diff --git a/lib/file_manager.rb b/lib/helpers/file_manager.rb similarity index 100% rename from lib/file_manager.rb rename to lib/helpers/file_manager.rb diff --git a/lib/pager.rb b/lib/helpers/pager.rb similarity index 100% rename from lib/pager.rb rename to lib/helpers/pager.rb diff --git a/lib/streamer.rb b/lib/helpers/streamer.rb similarity index 100% rename from lib/streamer.rb rename to lib/helpers/streamer.rb From e411207b790249fd69e4fd4184f3b3e01d377034 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:00:44 -0700 Subject: [PATCH 03/14] more sweeping changes --- lib/couchrest.rb | 17 +++++++++++++ lib/couchrest/couchrest.rb | 29 ---------------------- lib/{couch_rest.rb => couchrest/server.rb} | 0 lib/monkeypatches.rb | 22 ++++++++++++++++ 4 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 lib/couchrest.rb delete mode 100644 lib/couchrest/couchrest.rb rename lib/{couch_rest.rb => couchrest/server.rb} (100%) create mode 100644 lib/monkeypatches.rb diff --git a/lib/couchrest.rb b/lib/couchrest.rb new file mode 100644 index 0000000..d3a5366 --- /dev/null +++ b/lib/couchrest.rb @@ -0,0 +1,17 @@ +require "rubygems" +require 'json' +require 'rest_client' + +$:.unshift File.expand_path(File.dirname(__FILE__)) + + +require 'monkeypatches' +require 'lib/server' +require 'lib/database' + + +module CouchRest + def self.new(*opts) + Server.new(*opts) + end +end \ No newline at end of file diff --git a/lib/couchrest/couchrest.rb b/lib/couchrest/couchrest.rb deleted file mode 100644 index 3cb4b03..0000000 --- a/lib/couchrest/couchrest.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "rubygems" -require 'json' -require 'rest_client' - -require File.dirname(__FILE__) + '/couch_rest' -require File.dirname(__FILE__) + '/database' -require File.dirname(__FILE__) + '/pager' -require File.dirname(__FILE__) + '/file_manager' -require File.dirname(__FILE__) + '/streamer' - -# this has to come after the JSON gem - -# this date format sorts lexicographically -# and is compatible with Javascript's new Date(time_string) constructor -# note that sorting will break if you store times from multiple timezones -# I like to add a ENV['TZ'] = 'UTC' to my apps -class Time - def to_json(options = nil) - %("#{strftime("%Y/%m/%d %H:%M:%S %z")}") - end - # this works to decode the outputted time format - # from ActiveSupport - # def self.parse string, fallback=nil - # d = DateTime.parse(string).new_offset - # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec) - # rescue - # fallback - # end -end \ No newline at end of file diff --git a/lib/couch_rest.rb b/lib/couchrest/server.rb similarity index 100% rename from lib/couch_rest.rb rename to lib/couchrest/server.rb diff --git a/lib/monkeypatches.rb b/lib/monkeypatches.rb new file mode 100644 index 0000000..b35dce2 --- /dev/null +++ b/lib/monkeypatches.rb @@ -0,0 +1,22 @@ + +# this file must be loaded after the JSON gem + +class Time + # this date format sorts lexicographically + # and is compatible with Javascript's new Date(time_string) constructor + # note that sorting will break if you store times from multiple timezones + # I like to add a ENV['TZ'] = 'UTC' to my apps + + def to_json(options = nil) + %("#{strftime("%Y/%m/%d %H:%M:%S %z")}") + end + + # this works to decode the outputted time format + # copied from ActiveSupport + # def self.parse string, fallback=nil + # d = DateTime.parse(string).new_offset + # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec) + # rescue + # fallback + # end +end \ No newline at end of file From ac07c15c2840a0c3d6aef1c49d0b7dfe556a45e6 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:09:24 -0700 Subject: [PATCH 04/14] move files around alot today --- lib/commands.rb | 5 - lib/commands/generate.rb | 71 ------------ lib/commands/push.rb | 99 ---------------- lib/couchrest.rb | 4 +- lib/helpers/file_manager.rb | 223 ------------------------------------ lib/helpers/pager.rb | 103 ----------------- lib/helpers/streamer.rb | 29 ----- lib/monkeypatches.rb | 22 ---- 8 files changed, 2 insertions(+), 554 deletions(-) delete mode 100644 lib/commands.rb delete mode 100644 lib/commands/generate.rb delete mode 100644 lib/commands/push.rb delete mode 100644 lib/helpers/file_manager.rb delete mode 100644 lib/helpers/pager.rb delete mode 100644 lib/helpers/streamer.rb delete mode 100644 lib/monkeypatches.rb diff --git a/lib/commands.rb b/lib/commands.rb deleted file mode 100644 index 88369eb..0000000 --- a/lib/commands.rb +++ /dev/null @@ -1,5 +0,0 @@ -require File.join(File.dirname(__FILE__), "..", "couchrest") - -%w(push generate).each do |filename| - require File.join(File.dirname(__FILE__), "commands", filename) -end diff --git a/lib/commands/generate.rb b/lib/commands/generate.rb deleted file mode 100644 index bc46bcb..0000000 --- a/lib/commands/generate.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'fileutils' - -class CouchRest - module Commands - module Generate - - def self.run(options) - directory = options[:directory] - design_names = options[:trailing_args] - - FileUtils.mkdir_p(directory) - filename = File.join(directory, "lib.js") - self.write(filename, <<-FUNC) - // Put global functions here. - // Include in your views with - // - // //include-lib - FUNC - - design_names.each do |design_name| - subdirectory = File.join(directory, design_name) - FileUtils.mkdir_p(subdirectory) - filename = File.join(subdirectory, "sample-map.js") - self.write(filename, <<-FUNC) - function(doc) { - // Keys is first letter of _id - emit(doc._id[0], doc); - } - FUNC - - filename = File.join(subdirectory, "sample-reduce.js") - self.write(filename, <<-FUNC) - function(keys, values) { - // Count the number of keys starting with this letter - return values.length; - } - FUNC - - filename = File.join(subdirectory, "lib.js") - self.write(filename, <<-FUNC) - // Put functions specific to '#{design_name}' here. - // Include in your views with - // - // //include-lib - FUNC - end - end - - def self.help - helpstring = <<-GEN - - Usage: couchview generate directory design1 design2 design3 ... - - Couchview will create directories and example views for the design documents you specify. - - GEN - helpstring.gsub(/^ /, '') - end - - def self.write(filename, contents) - puts "Writing #{filename}" - File.open(filename, "w") do |f| - # Remove leading spaces - contents.gsub!(/^ ( )?/, '') - f.write contents - end - end - - end - end -end diff --git a/lib/commands/push.rb b/lib/commands/push.rb deleted file mode 100644 index a9a2e95..0000000 --- a/lib/commands/push.rb +++ /dev/null @@ -1,99 +0,0 @@ -class CouchRest - - module Commands - - module Push - - def self.run(options) - directory = options[:directory] - database = options[:trailing_args].first - - fm = CouchRest::FileManager.new(database) - fm.loud = options[:loud] - puts "Pushing views from directory #{directory} to database #{fm.db}" - fm.push_views(directory) - end - - def self.help - helpstring = <<-GEN - - == Pushing views with Couchview == - - Usage: couchview push directory dbname - - Couchview expects a specific filesystem layout for your CouchDB views (see - example below). It also supports advanced features like inlining of library - code (so you can keep DRY) as well as avoiding unnecessary document - modification. - - Couchview also solves a problem with CouchDB's view API, which only provides - access to the final reduce side of any views which have both a map and a - reduce function defined. The intermediate map results are often useful for - development and production. CouchDB is smart enough to reuse map indexes for - functions duplicated across views within the same design document. - - For views with a reduce function defined, Couchview creates both a reduce view - and a map-only view, so that you can browse and query the map side as well as - the reduction, with no performance penalty. - - == Example == - - couchview push foo-project/bar-views baz-database - - This will push the views defined in foo-project/bar-views into a database - called baz-database. Couchview expects the views to be defined in files with - names like: - - foo-project/bar-views/my-design/viewname-map.js - foo-project/bar-views/my-design/viewname-reduce.js - foo-project/bar-views/my-design/noreduce-map.js - - Pushed to => http://localhost:5984/baz-database/_design/my-design - - And the design document: - { - "views" : { - "viewname-map" : { - "map" : "### contents of view-name-map.js ###" - }, - "viewname-reduce" : { - "map" : "### contents of view-name-map.js ###", - "reduce" : "### contents of view-name-reduce.js ###" - }, - "noreduce-map" : { - "map" : "### contents of noreduce-map.js ###" - } - } - } - - Couchview will create a design document for each subdirectory of the views - directory specified on the command line. - - == Library Inlining == - - Couchview can optionally inline library code into your views so you only have - to maintain it in one place. It looks for any files named lib.* in your - design-doc directory (for doc specific libs) and in the parent views directory - (for project global libs). These libraries are only inserted into views which - include the text - - //include-lib - - or - - #include-lib - - Couchview is a result of scratching my own itch. I'd be happy to make it more - general, so please contact me at jchris@grabb.it if you'd like to see anything - added or changed. - - GEN - helpstring.gsub(/^ /, '') - end - - end - - - end - -end diff --git a/lib/couchrest.rb b/lib/couchrest.rb index d3a5366..e63b520 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -6,8 +6,8 @@ $:.unshift File.expand_path(File.dirname(__FILE__)) require 'monkeypatches' -require 'lib/server' -require 'lib/database' +require 'couchrest/server' +require 'couchrest/database' module CouchRest diff --git a/lib/helpers/file_manager.rb b/lib/helpers/file_manager.rb deleted file mode 100644 index 60ee7a4..0000000 --- a/lib/helpers/file_manager.rb +++ /dev/null @@ -1,223 +0,0 @@ -require 'digest/md5' - -class CouchRest - class FileManager - attr_reader :db - attr_accessor :loud - - LANGS = {"rb" => "ruby", "js" => "javascript"} - MIMES = { - "html" => "text/html", - "htm" => "text/html", - "png" => "image/png", - "css" => "text/css", - "js" => "test/javascript" - } - def initialize(dbname, host="http://localhost:5984") - @db = CouchRest.new(host).database(dbname) - end - - def push_directory(push_dir, docid=nil) - docid ||= push_dir.split('/').reverse.find{|part|!part.empty?} - - pushfiles = Dir["#{push_dir}/**/*.*"].collect do |f| - {f.split("#{push_dir}/").last => open(f).read} - end - - return if pushfiles.empty? - - @attachments = {} - @signatures = {} - pushfiles.each do |file| - name = file.keys.first - value = file.values.first - @signatures[name] = md5(value) - - @attachments[name] = { - "data" => value, - "content_type" => MIMES[name.split('.').last] - } - end - - doc = @db.get(docid) rescue nil - - unless doc - say "creating #{docid}" - @db.save({"_id" => docid, "_attachments" => @attachments, "signatures" => @signatures}) - return - end - - # remove deleted docs - to_be_removed = doc["signatures"].keys.select do |d| - !pushfiles.collect{|p| p.keys.first}.include?(d) - end - - to_be_removed.each do |p| - say "deleting #{p}" - doc["signatures"].delete(p) - doc["_attachments"].delete(p) - end - - # update existing docs: - doc["signatures"].each do |path, sig| - if (@signatures[path] == sig) - say "no change to #{path}. skipping..." - else - say "replacing #{path}" - doc["signatures"][path] = md5(@attachments[path]["data"]) - doc["_attachments"][path].delete("stub") - doc["_attachments"][path].delete("length") - doc["_attachments"][path]["data"] = @attachments[path]["data"] - doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} ) - - end - end - - # add in new files - new_files = pushfiles.select{|d| !doc["signatures"].keys.include?( d.keys.first) } - - new_files.each do |f| - say "creating #{f}" - path = f.keys.first - content = f.values.first - doc["signatures"][path] = md5(content) - - doc["_attachments"][path] = { - "data" => content, - "content_type" => MIMES[path.split('.').last] - } - end - - begin - @db.save(doc) - rescue Exception => e - say e.message - end - end - - def push_views(view_dir) - designs = {} - - Dir["#{view_dir}/**/*.*"].each do |design_doc| - design_doc_parts = design_doc.split('/') - next if /^lib\..*$/.match design_doc_parts.last - pre_normalized_view_name = design_doc_parts.last.split("-") - view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-") - - folder = design_doc_parts[-2] - - designs[folder] ||= {} - designs[folder]["views"] ||= {} - design_lang = design_doc_parts.last.split(".").last - designs[folder]["language"] ||= LANGS[design_lang] - - libs = "" - Dir["#{view_dir}/lib.#{design_lang}"].collect do |global_lib| - libs << open(global_lib).read - libs << "\n" - end - Dir["#{view_dir}/#{folder}/lib.#{design_lang}"].collect do |global_lib| - libs << open(global_lib).read - libs << "\n" - end - if design_doc_parts.last =~ /-map/ - designs[folder]["views"]["#{view_name}-map"] ||= {} - - designs[folder]["views"]["#{view_name}-map"]["map"] = read(design_doc, libs) - - designs[folder]["views"]["#{view_name}-reduce"] ||= {} - designs[folder]["views"]["#{view_name}-reduce"]["map"] = read(design_doc, libs) - end - - if design_doc_parts.last =~ /-reduce/ - designs[folder]["views"]["#{view_name}-reduce"] ||= {} - - designs[folder]["views"]["#{view_name}-reduce"]["reduce"] = read(design_doc, libs) - end - end - - # cleanup empty maps and reduces - designs.each do |name, props| - props["views"].each do |view, funcs| - next unless view.include?("reduce") - props["views"].delete(view) unless funcs.keys.include?("reduce") - end - end - - designs.each do |k,v| - create_or_update("_design/#{k}", v) - end - - designs - end - - def pull_views(view_dir) - prefix = "_design" - ds = db.documents(:startkey => '#{prefix}/', :endkey => '#{prefix}/ZZZZZZZZZ') - ds['rows'].collect{|r|r['id']}.each do |id| - puts directory = id.split('/').last - FileUtils.mkdir_p(File.join(view_dir,directory)) - views = db.get(id)['views'] - - vgroups = views.keys.group_by{|k|k.sub(/\-(map|reduce)$/,'')} - vgroups.each do|g,vs| - mapname = vs.find {|v|views[v]["map"]} - if mapname - # save map - mapfunc = views[mapname]["map"] - mapfile = File.join(view_dir, directory, "#{g}-map.js") # todo support non-js views - File.open(mapfile,'w') do |f| - f.write mapfunc - end - end - - reducename = vs.find {|v|views[v]["reduce"]} - if reducename - # save reduce - reducefunc = views[reducename]["reduce"] - reducefile = File.join(view_dir, directory, "#{g}-reduce.js") # todo support non-js views - File.open(reducefile,'w') do |f| - f.write reducefunc - end - end - end - end - - end - - - private - - def say words - puts words if @loud - end - - def md5 string - Digest::MD5.hexdigest(string) - end - - def read(file, libs=nil) - st = open(file).read - st.sub!(/(\/\/|#)include-lib/,libs) if libs - st - end - - def create_or_update(id, fields) - existing = @db.get(id) rescue nil - - if existing - updated = fields.merge({"_id" => id, "_rev" => existing["_rev"]}) - if existing != updated - say "replacing #{id}" - db.save(updated) - else - say "skipping #{id}" - end - else - say "creating #{id}" - db.save(fields.merge({"_id" => id})) - end - - end - end -end diff --git a/lib/helpers/pager.rb b/lib/helpers/pager.rb deleted file mode 100644 index 229ef8a..0000000 --- a/lib/helpers/pager.rb +++ /dev/null @@ -1,103 +0,0 @@ -class CouchRest - class Pager - attr_accessor :db - def initialize db - @db = db - end - - def all_docs(count=100, &block) - startkey = nil - oldend = nil - - while docrows = request_all_docs(count+1, startkey) - startkey = docrows.last['key'] - docrows.pop if docrows.length > count - if oldend == startkey - break - end - yield(docrows) - oldend = startkey - end - end - - def key_reduce(view, count, firstkey = nil, lastkey = nil, &block) - # start with no keys - startkey = firstkey - # lastprocessedkey = nil - keepgoing = true - - while keepgoing && viewrows = request_view(view, count, startkey) - startkey = viewrows.first['key'] - endkey = viewrows.last['key'] - - if (startkey == endkey) - # we need to rerequest to get a bigger page - # so we know we have all the rows for that key - viewrows = @db.view(view, :key => startkey)['rows'] - # we need to do an offset thing to find the next startkey - # otherwise we just get stuck - lastdocid = viewrows.last['id'] - fornextloop = @db.view(view, :startkey => startkey, :startkey_docid => lastdocid, :count => 2)['rows'] - - newendkey = fornextloop.last['key'] - if (newendkey == endkey) - keepgoing = false - else - startkey = newendkey - end - rows = viewrows - else - rows = [] - for r in viewrows - if (lastkey && r['key'] == lastkey) - keepgoing = false - break - end - break if (r['key'] == endkey) - rows << r - end - startkey = endkey - end - - key = :begin - values = [] - - rows.each do |r| - if key != r['key'] - # we're on a new key, yield the old first and then reset - yield(key, values) if key != :begin - key = r['key'] - values = [] - end - # keep accumulating - values << r['value'] - end - yield(key, values) - - end - end - - private - - def request_all_docs count, startkey = nil - opts = {} - opts[:count] = count if count - opts[:startkey] = startkey if startkey - results = @db.documents(opts) - rows = results['rows'] - rows unless rows.length == 0 - end - - def request_view view, count = nil, startkey = nil, endkey = nil - opts = {} - opts[:count] = count if count - opts[:startkey] = startkey if startkey - opts[:endkey] = endkey if endkey - - results = @db.view(view, opts) - rows = results['rows'] - rows unless rows.length == 0 - end - - end -end \ No newline at end of file diff --git a/lib/helpers/streamer.rb b/lib/helpers/streamer.rb deleted file mode 100644 index c9133ee..0000000 --- a/lib/helpers/streamer.rb +++ /dev/null @@ -1,29 +0,0 @@ -class CouchRest - class Streamer - attr_accessor :db - def initialize db - @db = db - end - - def view name, params = nil - urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}" - url = CouchRest.paramify_url urlst, params - IO.popen("curl --silent #{url}") do |view| - view.gets # discard header - while row = parse_line(view.gets) - yield row - end - end - end - - private - - def parse_line line - return nil unless line - if /(\{.*\}),?/.match(line.chomp) - JSON.parse($1) - end - end - - end -end \ No newline at end of file diff --git a/lib/monkeypatches.rb b/lib/monkeypatches.rb deleted file mode 100644 index b35dce2..0000000 --- a/lib/monkeypatches.rb +++ /dev/null @@ -1,22 +0,0 @@ - -# this file must be loaded after the JSON gem - -class Time - # this date format sorts lexicographically - # and is compatible with Javascript's new Date(time_string) constructor - # note that sorting will break if you store times from multiple timezones - # I like to add a ENV['TZ'] = 'UTC' to my apps - - def to_json(options = nil) - %("#{strftime("%Y/%m/%d %H:%M:%S %z")}") - end - - # this works to decode the outputted time format - # copied from ActiveSupport - # def self.parse string, fallback=nil - # d = DateTime.parse(string).new_offset - # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec) - # rescue - # fallback - # end -end \ No newline at end of file From 26de4acc5a24e0f005fb0b3920ad713e7defbea5 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:09:39 -0700 Subject: [PATCH 05/14] moge thng aournd --- lib/couchrest/commands.rb | 5 + lib/couchrest/commands/generate.rb | 71 ++++++++ lib/couchrest/commands/push.rb | 99 ++++++++++++ lib/couchrest/helpers/file_manager.rb | 223 ++++++++++++++++++++++++++ lib/couchrest/helpers/pager.rb | 103 ++++++++++++ lib/couchrest/helpers/streamer.rb | 29 ++++ lib/couchrest/monkeypatches.rb | 22 +++ 7 files changed, 552 insertions(+) create mode 100644 lib/couchrest/commands.rb create mode 100644 lib/couchrest/commands/generate.rb create mode 100644 lib/couchrest/commands/push.rb create mode 100644 lib/couchrest/helpers/file_manager.rb create mode 100644 lib/couchrest/helpers/pager.rb create mode 100644 lib/couchrest/helpers/streamer.rb create mode 100644 lib/couchrest/monkeypatches.rb diff --git a/lib/couchrest/commands.rb b/lib/couchrest/commands.rb new file mode 100644 index 0000000..88369eb --- /dev/null +++ b/lib/couchrest/commands.rb @@ -0,0 +1,5 @@ +require File.join(File.dirname(__FILE__), "..", "couchrest") + +%w(push generate).each do |filename| + require File.join(File.dirname(__FILE__), "commands", filename) +end diff --git a/lib/couchrest/commands/generate.rb b/lib/couchrest/commands/generate.rb new file mode 100644 index 0000000..bc46bcb --- /dev/null +++ b/lib/couchrest/commands/generate.rb @@ -0,0 +1,71 @@ +require 'fileutils' + +class CouchRest + module Commands + module Generate + + def self.run(options) + directory = options[:directory] + design_names = options[:trailing_args] + + FileUtils.mkdir_p(directory) + filename = File.join(directory, "lib.js") + self.write(filename, <<-FUNC) + // Put global functions here. + // Include in your views with + // + // //include-lib + FUNC + + design_names.each do |design_name| + subdirectory = File.join(directory, design_name) + FileUtils.mkdir_p(subdirectory) + filename = File.join(subdirectory, "sample-map.js") + self.write(filename, <<-FUNC) + function(doc) { + // Keys is first letter of _id + emit(doc._id[0], doc); + } + FUNC + + filename = File.join(subdirectory, "sample-reduce.js") + self.write(filename, <<-FUNC) + function(keys, values) { + // Count the number of keys starting with this letter + return values.length; + } + FUNC + + filename = File.join(subdirectory, "lib.js") + self.write(filename, <<-FUNC) + // Put functions specific to '#{design_name}' here. + // Include in your views with + // + // //include-lib + FUNC + end + end + + def self.help + helpstring = <<-GEN + + Usage: couchview generate directory design1 design2 design3 ... + + Couchview will create directories and example views for the design documents you specify. + + GEN + helpstring.gsub(/^ /, '') + end + + def self.write(filename, contents) + puts "Writing #{filename}" + File.open(filename, "w") do |f| + # Remove leading spaces + contents.gsub!(/^ ( )?/, '') + f.write contents + end + end + + end + end +end diff --git a/lib/couchrest/commands/push.rb b/lib/couchrest/commands/push.rb new file mode 100644 index 0000000..a9a2e95 --- /dev/null +++ b/lib/couchrest/commands/push.rb @@ -0,0 +1,99 @@ +class CouchRest + + module Commands + + module Push + + def self.run(options) + directory = options[:directory] + database = options[:trailing_args].first + + fm = CouchRest::FileManager.new(database) + fm.loud = options[:loud] + puts "Pushing views from directory #{directory} to database #{fm.db}" + fm.push_views(directory) + end + + def self.help + helpstring = <<-GEN + + == Pushing views with Couchview == + + Usage: couchview push directory dbname + + Couchview expects a specific filesystem layout for your CouchDB views (see + example below). It also supports advanced features like inlining of library + code (so you can keep DRY) as well as avoiding unnecessary document + modification. + + Couchview also solves a problem with CouchDB's view API, which only provides + access to the final reduce side of any views which have both a map and a + reduce function defined. The intermediate map results are often useful for + development and production. CouchDB is smart enough to reuse map indexes for + functions duplicated across views within the same design document. + + For views with a reduce function defined, Couchview creates both a reduce view + and a map-only view, so that you can browse and query the map side as well as + the reduction, with no performance penalty. + + == Example == + + couchview push foo-project/bar-views baz-database + + This will push the views defined in foo-project/bar-views into a database + called baz-database. Couchview expects the views to be defined in files with + names like: + + foo-project/bar-views/my-design/viewname-map.js + foo-project/bar-views/my-design/viewname-reduce.js + foo-project/bar-views/my-design/noreduce-map.js + + Pushed to => http://localhost:5984/baz-database/_design/my-design + + And the design document: + { + "views" : { + "viewname-map" : { + "map" : "### contents of view-name-map.js ###" + }, + "viewname-reduce" : { + "map" : "### contents of view-name-map.js ###", + "reduce" : "### contents of view-name-reduce.js ###" + }, + "noreduce-map" : { + "map" : "### contents of noreduce-map.js ###" + } + } + } + + Couchview will create a design document for each subdirectory of the views + directory specified on the command line. + + == Library Inlining == + + Couchview can optionally inline library code into your views so you only have + to maintain it in one place. It looks for any files named lib.* in your + design-doc directory (for doc specific libs) and in the parent views directory + (for project global libs). These libraries are only inserted into views which + include the text + + //include-lib + + or + + #include-lib + + Couchview is a result of scratching my own itch. I'd be happy to make it more + general, so please contact me at jchris@grabb.it if you'd like to see anything + added or changed. + + GEN + helpstring.gsub(/^ /, '') + end + + end + + + end + +end diff --git a/lib/couchrest/helpers/file_manager.rb b/lib/couchrest/helpers/file_manager.rb new file mode 100644 index 0000000..60ee7a4 --- /dev/null +++ b/lib/couchrest/helpers/file_manager.rb @@ -0,0 +1,223 @@ +require 'digest/md5' + +class CouchRest + class FileManager + attr_reader :db + attr_accessor :loud + + LANGS = {"rb" => "ruby", "js" => "javascript"} + MIMES = { + "html" => "text/html", + "htm" => "text/html", + "png" => "image/png", + "css" => "text/css", + "js" => "test/javascript" + } + def initialize(dbname, host="http://localhost:5984") + @db = CouchRest.new(host).database(dbname) + end + + def push_directory(push_dir, docid=nil) + docid ||= push_dir.split('/').reverse.find{|part|!part.empty?} + + pushfiles = Dir["#{push_dir}/**/*.*"].collect do |f| + {f.split("#{push_dir}/").last => open(f).read} + end + + return if pushfiles.empty? + + @attachments = {} + @signatures = {} + pushfiles.each do |file| + name = file.keys.first + value = file.values.first + @signatures[name] = md5(value) + + @attachments[name] = { + "data" => value, + "content_type" => MIMES[name.split('.').last] + } + end + + doc = @db.get(docid) rescue nil + + unless doc + say "creating #{docid}" + @db.save({"_id" => docid, "_attachments" => @attachments, "signatures" => @signatures}) + return + end + + # remove deleted docs + to_be_removed = doc["signatures"].keys.select do |d| + !pushfiles.collect{|p| p.keys.first}.include?(d) + end + + to_be_removed.each do |p| + say "deleting #{p}" + doc["signatures"].delete(p) + doc["_attachments"].delete(p) + end + + # update existing docs: + doc["signatures"].each do |path, sig| + if (@signatures[path] == sig) + say "no change to #{path}. skipping..." + else + say "replacing #{path}" + doc["signatures"][path] = md5(@attachments[path]["data"]) + doc["_attachments"][path].delete("stub") + doc["_attachments"][path].delete("length") + doc["_attachments"][path]["data"] = @attachments[path]["data"] + doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} ) + + end + end + + # add in new files + new_files = pushfiles.select{|d| !doc["signatures"].keys.include?( d.keys.first) } + + new_files.each do |f| + say "creating #{f}" + path = f.keys.first + content = f.values.first + doc["signatures"][path] = md5(content) + + doc["_attachments"][path] = { + "data" => content, + "content_type" => MIMES[path.split('.').last] + } + end + + begin + @db.save(doc) + rescue Exception => e + say e.message + end + end + + def push_views(view_dir) + designs = {} + + Dir["#{view_dir}/**/*.*"].each do |design_doc| + design_doc_parts = design_doc.split('/') + next if /^lib\..*$/.match design_doc_parts.last + pre_normalized_view_name = design_doc_parts.last.split("-") + view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-") + + folder = design_doc_parts[-2] + + designs[folder] ||= {} + designs[folder]["views"] ||= {} + design_lang = design_doc_parts.last.split(".").last + designs[folder]["language"] ||= LANGS[design_lang] + + libs = "" + Dir["#{view_dir}/lib.#{design_lang}"].collect do |global_lib| + libs << open(global_lib).read + libs << "\n" + end + Dir["#{view_dir}/#{folder}/lib.#{design_lang}"].collect do |global_lib| + libs << open(global_lib).read + libs << "\n" + end + if design_doc_parts.last =~ /-map/ + designs[folder]["views"]["#{view_name}-map"] ||= {} + + designs[folder]["views"]["#{view_name}-map"]["map"] = read(design_doc, libs) + + designs[folder]["views"]["#{view_name}-reduce"] ||= {} + designs[folder]["views"]["#{view_name}-reduce"]["map"] = read(design_doc, libs) + end + + if design_doc_parts.last =~ /-reduce/ + designs[folder]["views"]["#{view_name}-reduce"] ||= {} + + designs[folder]["views"]["#{view_name}-reduce"]["reduce"] = read(design_doc, libs) + end + end + + # cleanup empty maps and reduces + designs.each do |name, props| + props["views"].each do |view, funcs| + next unless view.include?("reduce") + props["views"].delete(view) unless funcs.keys.include?("reduce") + end + end + + designs.each do |k,v| + create_or_update("_design/#{k}", v) + end + + designs + end + + def pull_views(view_dir) + prefix = "_design" + ds = db.documents(:startkey => '#{prefix}/', :endkey => '#{prefix}/ZZZZZZZZZ') + ds['rows'].collect{|r|r['id']}.each do |id| + puts directory = id.split('/').last + FileUtils.mkdir_p(File.join(view_dir,directory)) + views = db.get(id)['views'] + + vgroups = views.keys.group_by{|k|k.sub(/\-(map|reduce)$/,'')} + vgroups.each do|g,vs| + mapname = vs.find {|v|views[v]["map"]} + if mapname + # save map + mapfunc = views[mapname]["map"] + mapfile = File.join(view_dir, directory, "#{g}-map.js") # todo support non-js views + File.open(mapfile,'w') do |f| + f.write mapfunc + end + end + + reducename = vs.find {|v|views[v]["reduce"]} + if reducename + # save reduce + reducefunc = views[reducename]["reduce"] + reducefile = File.join(view_dir, directory, "#{g}-reduce.js") # todo support non-js views + File.open(reducefile,'w') do |f| + f.write reducefunc + end + end + end + end + + end + + + private + + def say words + puts words if @loud + end + + def md5 string + Digest::MD5.hexdigest(string) + end + + def read(file, libs=nil) + st = open(file).read + st.sub!(/(\/\/|#)include-lib/,libs) if libs + st + end + + def create_or_update(id, fields) + existing = @db.get(id) rescue nil + + if existing + updated = fields.merge({"_id" => id, "_rev" => existing["_rev"]}) + if existing != updated + say "replacing #{id}" + db.save(updated) + else + say "skipping #{id}" + end + else + say "creating #{id}" + db.save(fields.merge({"_id" => id})) + end + + end + end +end diff --git a/lib/couchrest/helpers/pager.rb b/lib/couchrest/helpers/pager.rb new file mode 100644 index 0000000..229ef8a --- /dev/null +++ b/lib/couchrest/helpers/pager.rb @@ -0,0 +1,103 @@ +class CouchRest + class Pager + attr_accessor :db + def initialize db + @db = db + end + + def all_docs(count=100, &block) + startkey = nil + oldend = nil + + while docrows = request_all_docs(count+1, startkey) + startkey = docrows.last['key'] + docrows.pop if docrows.length > count + if oldend == startkey + break + end + yield(docrows) + oldend = startkey + end + end + + def key_reduce(view, count, firstkey = nil, lastkey = nil, &block) + # start with no keys + startkey = firstkey + # lastprocessedkey = nil + keepgoing = true + + while keepgoing && viewrows = request_view(view, count, startkey) + startkey = viewrows.first['key'] + endkey = viewrows.last['key'] + + if (startkey == endkey) + # we need to rerequest to get a bigger page + # so we know we have all the rows for that key + viewrows = @db.view(view, :key => startkey)['rows'] + # we need to do an offset thing to find the next startkey + # otherwise we just get stuck + lastdocid = viewrows.last['id'] + fornextloop = @db.view(view, :startkey => startkey, :startkey_docid => lastdocid, :count => 2)['rows'] + + newendkey = fornextloop.last['key'] + if (newendkey == endkey) + keepgoing = false + else + startkey = newendkey + end + rows = viewrows + else + rows = [] + for r in viewrows + if (lastkey && r['key'] == lastkey) + keepgoing = false + break + end + break if (r['key'] == endkey) + rows << r + end + startkey = endkey + end + + key = :begin + values = [] + + rows.each do |r| + if key != r['key'] + # we're on a new key, yield the old first and then reset + yield(key, values) if key != :begin + key = r['key'] + values = [] + end + # keep accumulating + values << r['value'] + end + yield(key, values) + + end + end + + private + + def request_all_docs count, startkey = nil + opts = {} + opts[:count] = count if count + opts[:startkey] = startkey if startkey + results = @db.documents(opts) + rows = results['rows'] + rows unless rows.length == 0 + end + + def request_view view, count = nil, startkey = nil, endkey = nil + opts = {} + opts[:count] = count if count + opts[:startkey] = startkey if startkey + opts[:endkey] = endkey if endkey + + results = @db.view(view, opts) + rows = results['rows'] + rows unless rows.length == 0 + end + + end +end \ No newline at end of file diff --git a/lib/couchrest/helpers/streamer.rb b/lib/couchrest/helpers/streamer.rb new file mode 100644 index 0000000..c9133ee --- /dev/null +++ b/lib/couchrest/helpers/streamer.rb @@ -0,0 +1,29 @@ +class CouchRest + class Streamer + attr_accessor :db + def initialize db + @db = db + end + + def view name, params = nil + urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}" + url = CouchRest.paramify_url urlst, params + IO.popen("curl --silent #{url}") do |view| + view.gets # discard header + while row = parse_line(view.gets) + yield row + end + end + end + + private + + def parse_line line + return nil unless line + if /(\{.*\}),?/.match(line.chomp) + JSON.parse($1) + end + end + + end +end \ No newline at end of file diff --git a/lib/couchrest/monkeypatches.rb b/lib/couchrest/monkeypatches.rb new file mode 100644 index 0000000..b35dce2 --- /dev/null +++ b/lib/couchrest/monkeypatches.rb @@ -0,0 +1,22 @@ + +# this file must be loaded after the JSON gem + +class Time + # this date format sorts lexicographically + # and is compatible with Javascript's new Date(time_string) constructor + # note that sorting will break if you store times from multiple timezones + # I like to add a ENV['TZ'] = 'UTC' to my apps + + def to_json(options = nil) + %("#{strftime("%Y/%m/%d %H:%M:%S %z")}") + end + + # this works to decode the outputted time format + # copied from ActiveSupport + # def self.parse string, fallback=nil + # d = DateTime.parse(string).new_offset + # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec) + # rescue + # fallback + # end +end \ No newline at end of file From f5fdc8b91307ab5d37cef04066854770d850f711 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:14:34 -0700 Subject: [PATCH 06/14] made CouchRest a module --- lib/couchrest.rb | 13 ++-- lib/couchrest/commands.rb | 5 -- lib/couchrest/commands/generate.rb | 2 +- lib/couchrest/commands/push.rb | 2 +- lib/couchrest/{ => core}/database.rb | 2 +- lib/couchrest/core/server.rb | 98 +++++++++++++++++++++++++++ lib/couchrest/helpers/file_manager.rb | 2 +- lib/couchrest/helpers/pager.rb | 2 +- lib/couchrest/helpers/streamer.rb | 2 +- lib/couchrest/server.rb | 96 -------------------------- 10 files changed, 110 insertions(+), 114 deletions(-) delete mode 100644 lib/couchrest/commands.rb rename lib/couchrest/{ => core}/database.rb (99%) create mode 100644 lib/couchrest/core/server.rb delete mode 100644 lib/couchrest/server.rb diff --git a/lib/couchrest.rb b/lib/couchrest.rb index e63b520..42ffeca 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -2,15 +2,14 @@ require "rubygems" require 'json' require 'rest_client' -$:.unshift File.expand_path(File.dirname(__FILE__)) - - -require 'monkeypatches' -require 'couchrest/server' -require 'couchrest/database' - +require 'couchrest/monkeypatches' module CouchRest + autoload :Server, 'couchrest/core/server' + autoload :Database, 'couchrest/core/database' + autoload :Pager, 'couchrest/helper/pager' + autoload :FileManager, 'couchrest/helper/file_manager' + def self.new(*opts) Server.new(*opts) end diff --git a/lib/couchrest/commands.rb b/lib/couchrest/commands.rb deleted file mode 100644 index 88369eb..0000000 --- a/lib/couchrest/commands.rb +++ /dev/null @@ -1,5 +0,0 @@ -require File.join(File.dirname(__FILE__), "..", "couchrest") - -%w(push generate).each do |filename| - require File.join(File.dirname(__FILE__), "commands", filename) -end diff --git a/lib/couchrest/commands/generate.rb b/lib/couchrest/commands/generate.rb index bc46bcb..dfdfc22 100644 --- a/lib/couchrest/commands/generate.rb +++ b/lib/couchrest/commands/generate.rb @@ -1,6 +1,6 @@ require 'fileutils' -class CouchRest +module CouchRest module Commands module Generate diff --git a/lib/couchrest/commands/push.rb b/lib/couchrest/commands/push.rb index a9a2e95..604639d 100644 --- a/lib/couchrest/commands/push.rb +++ b/lib/couchrest/commands/push.rb @@ -1,4 +1,4 @@ -class CouchRest +module CouchRest module Commands diff --git a/lib/couchrest/database.rb b/lib/couchrest/core/database.rb similarity index 99% rename from lib/couchrest/database.rb rename to lib/couchrest/core/database.rb index 73397f6..90c99f5 100644 --- a/lib/couchrest/database.rb +++ b/lib/couchrest/core/database.rb @@ -1,7 +1,7 @@ require 'cgi' require "base64" -class CouchRest +module CouchRest class Database attr_reader :server, :host, :name, :root diff --git a/lib/couchrest/core/server.rb b/lib/couchrest/core/server.rb new file mode 100644 index 0000000..91481e3 --- /dev/null +++ b/lib/couchrest/core/server.rb @@ -0,0 +1,98 @@ +module CouchRest + class Server + attr_accessor :uri, :uuid_batch_count + def initialize server = 'http://localhost:5984', uuid_batch_count = 1000 + @uri = server + @uuid_batch_count = uuid_batch_count + end + + # ensure that a database exists + # creates it if it isn't already there + # returns it after it's been created + def self.database! url + uri = URI.parse url + path = uri.path + uri.path = '' + cr = CouchRest.new(uri.to_s) + cr.database!(path) + end + + def self.database url + uri = URI.parse url + path = uri.path + uri.path = '' + cr = CouchRest.new(uri.to_s) + cr.database(path) + end + + # list all databases on the server + def databases + CouchRest.get "#{@uri}/_all_dbs" + end + + def database name + CouchRest::Database.new(self, name) + end + + # creates the database if it doesn't exist + def database! name + create_db(name) rescue nil + database name + end + + # get the welcome message + def info + CouchRest.get "#{@uri}/" + end + + # create a database + def create_db name + CouchRest.put "#{@uri}/#{name}" + database name + end + + # restart the couchdb instance + def restart! + CouchRest.post "#{@uri}/_restart" + end + + def next_uuid count = @uuid_batch_count + @uuids ||= [] + if @uuids.empty? + @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"] + end + @uuids.pop + end + + class << self + def put uri, doc = nil + payload = doc.to_json if doc + JSON.parse(RestClient.put(uri, payload)) + end + + def get uri + JSON.parse(RestClient.get(uri), :max_nesting => false) + end + + def post uri, doc = nil + payload = doc.to_json if doc + JSON.parse(RestClient.post(uri, payload)) + end + + def delete uri + JSON.parse(RestClient.delete(uri)) + end + + def paramify_url url, params = nil + if params + query = params.collect do |k,v| + v = v.to_json if %w{key startkey endkey}.include?(k.to_s) + "#{k}=#{CGI.escape(v.to_s)}" + end.join("&") + url = "#{url}?#{query}" + end + url + end + end + end +end diff --git a/lib/couchrest/helpers/file_manager.rb b/lib/couchrest/helpers/file_manager.rb index 60ee7a4..e41940e 100644 --- a/lib/couchrest/helpers/file_manager.rb +++ b/lib/couchrest/helpers/file_manager.rb @@ -1,6 +1,6 @@ require 'digest/md5' -class CouchRest +module CouchRest class FileManager attr_reader :db attr_accessor :loud diff --git a/lib/couchrest/helpers/pager.rb b/lib/couchrest/helpers/pager.rb index 229ef8a..6442a90 100644 --- a/lib/couchrest/helpers/pager.rb +++ b/lib/couchrest/helpers/pager.rb @@ -1,4 +1,4 @@ -class CouchRest +module CouchRest class Pager attr_accessor :db def initialize db diff --git a/lib/couchrest/helpers/streamer.rb b/lib/couchrest/helpers/streamer.rb index c9133ee..ee7d4c0 100644 --- a/lib/couchrest/helpers/streamer.rb +++ b/lib/couchrest/helpers/streamer.rb @@ -1,4 +1,4 @@ -class CouchRest +module CouchRest class Streamer attr_accessor :db def initialize db diff --git a/lib/couchrest/server.rb b/lib/couchrest/server.rb deleted file mode 100644 index fa5e8ab..0000000 --- a/lib/couchrest/server.rb +++ /dev/null @@ -1,96 +0,0 @@ -class CouchRest - attr_accessor :uri, :uuid_batch_count - def initialize server = 'http://localhost:5984', uuid_batch_count = 1000 - @uri = server - @uuid_batch_count = uuid_batch_count - end - - # ensure that a database exists - # creates it if it isn't already there - # returns it after it's been created - def self.database! url - uri = URI.parse url - path = uri.path - uri.path = '' - cr = CouchRest.new(uri.to_s) - cr.database!(path) - end - - def self.database url - uri = URI.parse url - path = uri.path - uri.path = '' - cr = CouchRest.new(uri.to_s) - cr.database(path) - end - - # list all databases on the server - def databases - CouchRest.get "#{@uri}/_all_dbs" - end - - def database name - CouchRest::Database.new(self, name) - end - - # creates the database if it doesn't exist - def database! name - create_db(name) rescue nil - database name - end - - # get the welcome message - def info - CouchRest.get "#{@uri}/" - end - - # create a database - def create_db name - CouchRest.put "#{@uri}/#{name}" - database name - end - - # restart the couchdb instance - def restart! - CouchRest.post "#{@uri}/_restart" - end - - def next_uuid count = @uuid_batch_count - @uuids ||= [] - if @uuids.empty? - @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"] - end - @uuids.pop - end - - class << self - def put uri, doc = nil - payload = doc.to_json if doc - JSON.parse(RestClient.put(uri, payload)) - end - - def get uri - JSON.parse(RestClient.get(uri), :max_nesting => false) - end - - def post uri, doc = nil - payload = doc.to_json if doc - JSON.parse(RestClient.post(uri, payload)) - end - - def delete uri - JSON.parse(RestClient.delete(uri)) - end - - def paramify_url url, params = nil - if params - query = params.collect do |k,v| - v = v.to_json if %w{key startkey endkey}.include?(k.to_s) - "#{k}=#{CGI.escape(v.to_s)}" - end.join("&") - url = "#{url}?#{query}" - end - url - end - end -end From 7028f7f7b36ac7094378269cb976d120122578e8 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:16:01 -0700 Subject: [PATCH 07/14] ganked load path from merb --- lib/couchrest.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 42ffeca..fc9320f 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -2,6 +2,11 @@ require "rubygems" require 'json' require 'rest_client' +$:.unshift File.dirname(__FILE__) unless + $:.include?(File.dirname(__FILE__)) || + $:.include?(File.expand_path(File.dirname(__FILE__))) + + require 'couchrest/monkeypatches' module CouchRest From 211331f4a6151e90e407178a90cb64b32b764006 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:22:43 -0700 Subject: [PATCH 08/14] most specs passing with CouchRest module --- lib/couchrest.rb | 41 +++++++++++++++++-- lib/couchrest/core/server.rb | 30 -------------- .../{helpers => helper}/file_manager.rb | 0 lib/couchrest/{helpers => helper}/pager.rb | 0 lib/couchrest/{helpers => helper}/streamer.rb | 0 5 files changed, 38 insertions(+), 33 deletions(-) rename lib/couchrest/{helpers => helper}/file_manager.rb (100%) rename lib/couchrest/{helpers => helper}/pager.rb (100%) rename lib/couchrest/{helpers => helper}/streamer.rb (100%) diff --git a/lib/couchrest.rb b/lib/couchrest.rb index fc9320f..0ee562a 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -14,8 +14,43 @@ module CouchRest autoload :Database, 'couchrest/core/database' autoload :Pager, 'couchrest/helper/pager' autoload :FileManager, 'couchrest/helper/file_manager' + autoload :Streamer, 'couchrest/helper/streamer' - def self.new(*opts) - Server.new(*opts) - end + # The CouchRest module methods handle the basic JSON serialization + # and deserialization, as well as query parameters. + class << self + + def new(*opts) + Server.new(*opts) + end + + def put uri, doc = nil + payload = doc.to_json if doc + JSON.parse(RestClient.put(uri, payload)) + end + + def get uri + JSON.parse(RestClient.get(uri), :max_nesting => false) + end + + def post uri, doc = nil + payload = doc.to_json if doc + JSON.parse(RestClient.post(uri, payload)) + end + + def delete uri + JSON.parse(RestClient.delete(uri)) + end + + def paramify_url url, params = nil + if params + query = params.collect do |k,v| + v = v.to_json if %w{key startkey endkey}.include?(k.to_s) + "#{k}=#{CGI.escape(v.to_s)}" + end.join("&") + url = "#{url}?#{query}" + end + url + end + end # class << self end \ No newline at end of file diff --git a/lib/couchrest/core/server.rb b/lib/couchrest/core/server.rb index 91481e3..7365277 100644 --- a/lib/couchrest/core/server.rb +++ b/lib/couchrest/core/server.rb @@ -64,35 +64,5 @@ module CouchRest @uuids.pop end - class << self - def put uri, doc = nil - payload = doc.to_json if doc - JSON.parse(RestClient.put(uri, payload)) - end - - def get uri - JSON.parse(RestClient.get(uri), :max_nesting => false) - end - - def post uri, doc = nil - payload = doc.to_json if doc - JSON.parse(RestClient.post(uri, payload)) - end - - def delete uri - JSON.parse(RestClient.delete(uri)) - end - - def paramify_url url, params = nil - if params - query = params.collect do |k,v| - v = v.to_json if %w{key startkey endkey}.include?(k.to_s) - "#{k}=#{CGI.escape(v.to_s)}" - end.join("&") - url = "#{url}?#{query}" - end - url - end - end end end diff --git a/lib/couchrest/helpers/file_manager.rb b/lib/couchrest/helper/file_manager.rb similarity index 100% rename from lib/couchrest/helpers/file_manager.rb rename to lib/couchrest/helper/file_manager.rb diff --git a/lib/couchrest/helpers/pager.rb b/lib/couchrest/helper/pager.rb similarity index 100% rename from lib/couchrest/helpers/pager.rb rename to lib/couchrest/helper/pager.rb diff --git a/lib/couchrest/helpers/streamer.rb b/lib/couchrest/helper/streamer.rb similarity index 100% rename from lib/couchrest/helpers/streamer.rb rename to lib/couchrest/helper/streamer.rb From 711fdc1ca6e165b242c10895d43d8a298fcd198f Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:25:51 -0700 Subject: [PATCH 09/14] all specs pass with the new layout --- lib/couchrest.rb | 26 ++++++++++++++++++++++++-- lib/couchrest/core/server.rb | 19 ------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 0ee562a..2f016b3 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -17,13 +17,35 @@ module CouchRest autoload :Streamer, 'couchrest/helper/streamer' # The CouchRest module methods handle the basic JSON serialization - # and deserialization, as well as query parameters. + # and deserialization, as well as query parameters. The module also includes + # some helpers for tasks like instantiating a new Database or Server instance. class << self - + + # todo, make this parse the url and instantiate a Server or Database instance + # depending on the specificity. def new(*opts) Server.new(*opts) end + # ensure that a database exists + # creates it if it isn't already there + # returns it after it's been created + def database! url + uri = URI.parse url + path = uri.path + uri.path = '' + cr = CouchRest.new(uri.to_s) + cr.database!(path) + end + + def database url + uri = URI.parse url + path = uri.path + uri.path = '' + cr = CouchRest.new(uri.to_s) + cr.database(path) + end + def put uri, doc = nil payload = doc.to_json if doc JSON.parse(RestClient.put(uri, payload)) diff --git a/lib/couchrest/core/server.rb b/lib/couchrest/core/server.rb index 7365277..0091041 100644 --- a/lib/couchrest/core/server.rb +++ b/lib/couchrest/core/server.rb @@ -6,25 +6,6 @@ module CouchRest @uuid_batch_count = uuid_batch_count end - # ensure that a database exists - # creates it if it isn't already there - # returns it after it's been created - def self.database! url - uri = URI.parse url - path = uri.path - uri.path = '' - cr = CouchRest.new(uri.to_s) - cr.database!(path) - end - - def self.database url - uri = URI.parse url - path = uri.path - uri.path = '' - cr = CouchRest.new(uri.to_s) - cr.database(path) - end - # list all databases on the server def databases CouchRest.get "#{@uri}/_all_dbs" From 0f8baf85ba8ff2353f5b04c6dfead81ca990b450 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:31:59 -0700 Subject: [PATCH 10/14] added apache license --- LICENSE | 176 +++++++++++++++++++++++++++++++++++++++++++++++ lib/couchrest.rb | 15 ++++ 2 files changed, 191 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 2f016b3..d5da261 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -1,3 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + require "rubygems" require 'json' require 'rest_client' From 49a36af2b05915bd512ebb3969986f10b95fab11 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:39:48 -0700 Subject: [PATCH 11/14] fixed apache license --- lib/couchrest.rb | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/couchrest.rb b/lib/couchrest.rb index d5da261..2fb75a6 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -1,17 +1,16 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with this -# work for additional information regarding copyright ownership. The ASF -# licenses this file to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. +# Copyright 2008 J. Chris Anderson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. require "rubygems" require 'json' From b051cdd3a2981bec183c6e18631160b330d1cc23 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 21:39:56 -0700 Subject: [PATCH 12/14] thanks file --- THANKS | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 THANKS diff --git a/THANKS b/THANKS new file mode 100644 index 0000000..f571cc0 --- /dev/null +++ b/THANKS @@ -0,0 +1,14 @@ +CouchRest THANKS +===================== + +CouchRest was originally developed by J. Chris Anderson +and a number of other contributors. Many people further contributed to +CouchRest by reporting problems, suggesting various improvements or submitting +changes. A list of these people is included below. + + * Geoffrey Grosenbach + * Simon Rozet + +Patches are welcome. The primary source for this software project is: + +http://github.com/jchris/couchrest/tree/master From c7fae2eca92dad89a2a1020600f790a800abe470 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 22:21:16 -0700 Subject: [PATCH 13/14] updated rake for rdoc --- .gitignore | 1 + README.markdown => README.rdoc | 12 ++++++------ Rakefile | 11 +++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) rename README.markdown => README.rdoc (90%) diff --git a/.gitignore b/.gitignore index e43b0f9..7b3dafe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +html/* \ No newline at end of file diff --git a/README.markdown b/README.rdoc similarity index 90% rename from README.markdown rename to README.rdoc index ab0fee2..48e4bd4 100644 --- a/README.markdown +++ b/README.rdoc @@ -1,22 +1,22 @@ -## CouchRest - CouchDB, close to the metal +== CouchRest - CouchDB, close to the metal CouchRest is based on [CouchDB's couch.js test library](http://svn.apache.org/repos/asf/incubator/couchdb/trunk/share/www/script/couch.js), which I find to be concise, clear, and well designed. CouchRest lightly wraps CouchDB's HTTP API, managing JSON serialization, and remembering the URI-paths to CouchDB's API endpoints so you don't have to. CouchRest's lighweight is designed to make a simple base for application and framework-specific object oriented APIs. -### Easy Install +=== Easy Install -`sudo gem install jchris-couchrest -s http://gems.github.com` + sudo gem install jchris-couchrest -s http://gems.github.com -### Relax, it's RESTful +=== Relax, it's RESTful The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper. REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, while still giving you more control than Open-URI. I recommend it anytime you’re interfacing with a well-defined web service. -### Running the Specs +=== Running the Specs The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support). -### Examples +=== Examples Quick Start: diff --git a/Rakefile b/Rakefile index b34bff2..e58b43f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ require 'rake' +require "rake/rdoctask" require 'spec/rake/spectask' desc "Run all specs" @@ -12,4 +13,14 @@ Spec::Rake::SpecTask.new(:doc) do |t| t.spec_files = FileList['spec/*_spec.rb'] end +desc "Generate the rdoc" +Rake::RDocTask.new do |rdoc| + files = ["README.rdoc", "LICENSE", "lib/**/*.rb"] + rdoc.rdoc_files.add(files) + rdoc.main = "README.rdoc" + rdoc.title = "CouchRest: Ruby CouchDB, close to the metal" + # rdoc.rdoc_dir = "doc/rdoc" + # rdoc.options << "--line-numbers" << "--inline-source" +end + task :default => :spec From bcd96183412725d8abe724d2f7f3fa7a510848b7 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Thu, 11 Sep 2008 22:23:47 -0700 Subject: [PATCH 14/14] updated gem version --- THANKS | 1 + couchrest.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/THANKS b/THANKS index f571cc0..bfd4fb2 100644 --- a/THANKS +++ b/THANKS @@ -6,6 +6,7 @@ and a number of other contributors. Many people further contributed to CouchRest by reporting problems, suggesting various improvements or submitting changes. A list of these people is included below. + * Greg Borenstein * Geoffrey Grosenbach * Simon Rozet diff --git a/couchrest.gemspec b/couchrest.gemspec index 9f16fb9..1494879 100644 --- a/couchrest.gemspec +++ b/couchrest.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.name = "couchrest" - s.version = "0.9.3" - s.date = "2008-09-10" + s.version = "0.9.4" + s.date = "2008-09-11" s.summary = "Lean and RESTful interface to CouchDB." s.email = "jchris@grabb.it" s.homepage = "http://github.com/jchris/couchrest"