From 571cd257e000b54c65b6bde62bdc79bcefa1a52a Mon Sep 17 00:00:00 2001 From: Matt Lyon Date: Sat, 31 Jan 2009 10:38:44 -0800 Subject: [PATCH 01/13] database replication methods, no conflict resolution provided --- lib/couchrest/core/database.rb | 14 ++++++++++++- spec/couchrest/core/database_spec.rb | 31 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 36e3266..ef63789 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -213,7 +213,19 @@ module CouchRest def compact! CouchRest.post "#{@root}/_compact" end - + + # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts. + def replicate_from other_db + raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database) + CouchRest.post "#{@host}/_replicate", :source => other_db.root, :target => name + end + + # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts. + def replicate_to other_db + raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database) + CouchRest.post "#{@host}/_replicate", :target => other_db.root, :source => name + end + # DELETE the database itself. This is not undoable and could be rather # catastrophic. Use with care! def delete! diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 5acec75..350853d 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -624,6 +624,37 @@ describe CouchRest::Database do @cr.databases.should_not include('couchrest-test') end end + + describe "replicating a database" do + before do + @db.save({'_id' => 'test_doc', 'some-value' => 'foo'}) + @other_db = @cr.database 'couchrest-test-replication' + @other_db.delete! rescue nil + @other_db = @cr.create_db 'couchrest-test-replication' + end + + describe "via pulling" do + before do + @other_db.replicate_from @db + end + + it "contains the document from the original database" do + doc = @other_db.get('test_doc') + doc['some-value'].should == 'foo' + end + end + + describe "via pushing" do + before do + @db.replicate_to @other_db + end + + it "copies the document to the other database" do + doc = @other_db.get('test_doc') + doc['some-value'].should == 'foo' + end + end + end end From 9b3b56bbf546a33fccdc9a2b3432a3ee12907d1f Mon Sep 17 00:00:00 2001 From: Matt Lyon Date: Sun, 1 Feb 2009 23:34:30 -0800 Subject: [PATCH 02/13] documentation for Document#copy and #move, copied from Database --- lib/couchrest/core/document.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index 6fe5b77..dcd352a 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -57,12 +57,18 @@ module CouchRest result['ok'] end + # copies the document to a new id. If the destination id currently exists, a rev must be provided. + # dest can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc + # hash with a '_rev' key def copy(dest) raise ArgumentError, "doc.database required to copy" unless database result = database.copy(self, dest) result['ok'] end + # moves the document to a new id. If the destination id currently exists, a rev must be provided. + # dest can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc + # hash with a '_rev' key def move(dest) raise ArgumentError, "doc.database required to copy" unless database result = database.move(self, dest) From b915f7f708b211abd75f958a7c367b1cef4c0af0 Mon Sep 17 00:00:00 2001 From: Matt Lyon Date: Mon, 2 Feb 2009 00:11:38 -0800 Subject: [PATCH 03/13] - Added Database#delete_attachment, for removing them directly - Modified Database#fetch_attachment to take a doc as its first argument +as well as+ a docid, to be consistent with the other attachment methods. - Refactored the attachment uri generation used by #fetch_attachment, #put_attachment, and #delete_attachment to a common private method, #uri_for_attachment --- lib/couchrest/core/database.rb | 31 ++++++++++-------- spec/couchrest/core/database_spec.rb | 47 +++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index ef63789..392e319 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -87,25 +87,23 @@ module CouchRest end # GET an attachment directly from CouchDB - def fetch_attachment docid, name - slug = escape_docid(docid) - name = CGI.escape(name) - RestClient.get "#{@root}/#{slug}/#{name}" + def fetch_attachment doc, name + docid = doc.respond_to?(:has_key?) ? doc['_id'] : doc + RestClient.get uri_for_attachment({'_id' => docid}, name) end # PUT an attachment directly to CouchDB def put_attachment doc, name, file, options = {} - docid = escape_docid(doc['_id']) - name = CGI.escape(name) - uri = if doc['_rev'] - "#{@root}/#{docid}/#{name}?rev=#{doc['_rev']}" - else - "#{@root}/#{docid}/#{name}" - end - + uri = uri_for_attachment(doc, name) JSON.parse(RestClient.put(uri, file, options)) end + # DELETE an attachment directly from CouchDB + def delete_attachment doc, name + uri = uri_for_attachment(doc, name) + JSON.parse(RestClient.delete(uri)) + end + # Save a document to CouchDB. This will use the _id field from # the document as the id for PUT, or request a new UUID from CouchDB, if # no _id is present on the document. IDs are attached to @@ -233,7 +231,14 @@ module CouchRest end private - + + def uri_for_attachment doc, name + docid = escape_docid(doc['_id']) + name = CGI.escape(name) + rev = "?rev=#{doc['_rev']}" if doc['_rev'] + "#{@root}/#{docid}/#{name}#{rev}" + end + def escape_docid id /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id) end diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 350853d..99cb6eb 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -230,7 +230,31 @@ describe CouchRest::Database do end end - + + describe "fetch_attachment" do + before do + @attach = "My Doc

Has words.

" + @doc = { + "_id" => "mydocwithattachment", + "field" => ["some value"], + "_attachments" => { + "test.html" => { + "type" => "text/html", + "data" => @attach + } + } + } + @db.save(@doc) + end + + it "should get the attachment with the doc's _id" do + @db.fetch_attachment("mydocwithattachment", "test.html").should == @attach + end + it "should get the attachment with the doc itself" do + @db.fetch_attachment(@db.get('mydocwithattachment'), 'test.html').should == @attach + end + end + describe "PUT attachment from file" do before(:each) do filename = FIXTURE_PATH + '/attachments/couchdb.png' @@ -330,6 +354,27 @@ describe CouchRest::Database do attachment.should == @attach2 end end + + describe "DELETE an attachment directly from the database" do + before(:each) do + doc = { + '_id' => 'mydocwithattachment', + '_attachments' => { + 'test.html' => { + 'type' => 'text/html', + 'data' => "My Doc

Has words.

" + } + } + } + @db.save(doc) + @doc = @db.get('mydocwithattachment') + end + it "should delete the attachment" do + lambda { @db.fetch_attachment('mydocwithattachment','test.html') }.should_not raise_error + @db.delete_attachment(@doc, "test.html") + lambda { @db.fetch_attachment('mydocwithattachment','test.html') }.should raise_error(RestClient::ResourceNotFound) + end + end describe "POST document with attachment (with funky name)" do before(:each) do From a4a2b202ae79072acdabd58e48a056de95553a7e Mon Sep 17 00:00:00 2001 From: Matt Lyon Date: Mon, 2 Feb 2009 00:35:37 -0800 Subject: [PATCH 04/13] Added attachment methods to CocuhRest::Document: #put_attachment, #fetch_attachment and #delete_attachment. Note you can overwrite exisitng attachments with #put_attachment. --- lib/couchrest/core/document.rb | 23 +++++++++- spec/couchrest/core/document_spec.rb | 65 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index dcd352a..7c4e29b 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -74,7 +74,28 @@ module CouchRest result = database.move(self, dest) result['ok'] end - + + # saves an attachment directly to couchdb + def put_attachment(name, file, options={}) + raise ArgumentError, "doc.database required to put_attachment" unless database + result = database.put_attachment(self, name, file, options) + self['_rev'] = result['rev'] + result['ok'] + end + + # returns an attachment's data + def fetch_attachment(name) + raise ArgumentError, "doc.database required to put_attachment" unless database + database.fetch_attachment(self, name) + end + + # deletes an attachment directly from couchdb + def delete_attachment(name) + raise ArgumentError, "doc.database required to delete_attachment" unless database + result = database.delete_attachment(self, name) + self['_rev'] = result['rev'] + result['ok'] + end end diff --git a/spec/couchrest/core/document_spec.rb b/spec/couchrest/core/document_spec.rb index a714cd6..301bd47 100644 --- a/spec/couchrest/core/document_spec.rb +++ b/spec/couchrest/core/document_spec.rb @@ -211,3 +211,68 @@ describe "MOVE existing document" do end end end + +describe "dealing with attachments" do + before do + @db = reset_test_db! + @attach = "My Doc

Has words.

" + response = @db.save({'key' => 'value'}) + @doc = @db.get(response['id']) + end + + def append_attachment(name='test.html', attach=@attach) + @doc['_attachments'] ||= {} + @doc['_attachments'][name] = { + 'type' => 'text/html', + 'data' => attach + } + @doc.save + @rev = @doc['_rev'] + end + + describe "PUTing an attachment directly to the doc" do + before do + @doc.put_attachment('test.html', @attach) + end + + it "is there" do + @db.fetch_attachment(@doc, 'test.html').should == @attach + end + + it "updates the revision" do + @doc['_rev'].should_not == @rev + end + + it "updates attachments" do + @attach2 = "My Doc

Is Different.

" + @doc.put_attachment('test.html', @attach2) + @db.fetch_attachment(@doc, 'test.html').should == @attach2 + end + end + + describe "fetching an attachment from a doc directly" do + before do + append_attachment + end + + it "pulls the attachment" do + @doc.fetch_attachment('test.html').should == @attach + end + end + + describe "deleting an attachment from a doc directly" do + before do + append_attachment + @doc.delete_attachment('test.html') + end + + it "removes it" do + lambda { @db.fetch_attachment(@doc, 'test.html').should }.should raise_error(RestClient::ResourceNotFound) + end + + it "updates the revision" do + @doc['_rev'].should_not == @rev + end + end + +end \ No newline at end of file From 84e2bf94e4dd903ff26f6f70f7844da455125dc8 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 28 Jan 2009 17:36:36 -0800 Subject: [PATCH 05/13] slight change of API, CR::Document now uses _doc instead of , also added #create! and #recreate! to Document instances --- lib/couchrest/core/database.rb | 113 +++++++++++++++------- lib/couchrest/core/document.rb | 8 +- lib/couchrest/core/model.rb | 8 +- spec/couchrest/core/database_spec.rb | 136 +++++++++++++++++---------- spec/couchrest/core/design_spec.rb | 2 +- spec/couchrest/core/document_spec.rb | 16 ++-- spec/couchrest/core/model_spec.rb | 12 +-- spec/couchrest/helpers/pager_spec.rb | 2 +- 8 files changed, 190 insertions(+), 107 deletions(-) diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 392e319..1665f1e 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -3,7 +3,7 @@ require "base64" module CouchRest class Database - attr_reader :server, :host, :name, :root + attr_reader :server, :host, :name, :root, :uri attr_accessor :bulk_save_cache_limit # Create a CouchRest::Database adapter for the supplied CouchRest::Server @@ -13,11 +13,11 @@ module CouchRest # server:: database host # name:: database name # - def initialize server, name + def initialize(server, name) @name = name @server = server @host = server.uri - @root = "#{host}/#{name}" + @uri = @root = "#{host}/#{name}" @streamer = Streamer.new(self) @bulk_save_cache = [] @bulk_save_cache_limit = 50 @@ -25,18 +25,18 @@ module CouchRest # returns the database's uri def to_s - @root + @uri end # GET the database info from CouchDB def info - CouchRest.get @root + CouchRest.get @uri end # Query the _all_docs view. Accepts all the same arguments as view. - def documents params = {} + def documents(params = {}) keys = params.delete(:keys) - url = CouchRest.paramify_url "#{@root}/_all_docs", params + url = CouchRest.paramify_url "#{@uri}/_all_docs", params if keys CouchRest.post(url, {:keys => keys}) else @@ -47,10 +47,10 @@ module CouchRest # POST a temporary view function to CouchDB for querying. This is not # recommended, as you don't get any performance benefit from CouchDB's # materialized views. Can be quite slow on large databases. - def slow_view funcs, params = {} + def slow_view(funcs, params = {}) keys = params.delete(:keys) funcs = funcs.merge({:keys => keys}) if keys - url = CouchRest.paramify_url "#{@root}/_temp_view", params + url = CouchRest.paramify_url "#{@uri}/_temp_view", params JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'})) end @@ -59,9 +59,9 @@ module CouchRest # Query a CouchDB view as defined by a _design document. Accepts # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi - def view name, params = {}, &block + def view(name, params = {}, &block) keys = params.delete(:keys) - url = CouchRest.paramify_url "#{@root}/_view/#{name}", params + url = CouchRest.paramify_url "#{@uri}/_view/#{name}", params if keys CouchRest.post(url, {:keys => keys}) else @@ -74,9 +74,9 @@ module CouchRest end # GET a document from CouchDB, by id. Returns a Ruby Hash. - def get id + def get(id) slug = escape_docid(id) - hash = CouchRest.get("#{@root}/#{slug}") + hash = CouchRest.get("#{@uri}/#{slug}") doc = if /^_design/ =~ hash["_id"] Design.new(hash) else @@ -87,14 +87,21 @@ module CouchRest end # GET an attachment directly from CouchDB - def fetch_attachment doc, name - docid = doc.respond_to?(:has_key?) ? doc['_id'] : doc - RestClient.get uri_for_attachment({'_id' => docid}, name) + def fetch_attachment(docid, name) + slug = escape_docid(docid) + name = CGI.escape(name) + RestClient.get "#{@uri}/#{slug}/#{name}" end # PUT an attachment directly to CouchDB - def put_attachment doc, name, file, options = {} - uri = uri_for_attachment(doc, name) + def put_attachment(doc, name, file, options = {}) + docid = escape_docid(doc['_id']) + name = CGI.escape(name) + uri = if doc['_rev'] + "#{@uri}/#{docid}/#{name}?rev=#{doc['_rev']}" + else + "#{@uri}/#{docid}/#{name}" + end JSON.parse(RestClient.put(uri, file, options)) end @@ -113,7 +120,7 @@ module CouchRest # # If bulk is true (false by default) the document is cached for bulk-saving later. # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save. - def save (doc, bulk = false) + def save_doc(doc, bulk = false) if doc['_attachments'] doc['_attachments'] = encode_attachments(doc['_attachments']) end @@ -126,13 +133,13 @@ module CouchRest end result = if doc['_id'] slug = escape_docid(doc['_id']) - CouchRest.put "#{@root}/#{slug}", doc + CouchRest.put "#{@uri}/#{slug}", doc else begin slug = doc['_id'] = @server.next_uuid - CouchRest.put "#{@root}/#{slug}", doc + CouchRest.put "#{@uri}/#{slug}", doc rescue #old version of couchdb - CouchRest.post @root, doc + CouchRest.post @uri, doc end end if result['ok'] @@ -143,6 +150,13 @@ module CouchRest result end + ### DEPRECATION NOTICE + def save(doc, bulk=false) + puts "CouchRest::Database's save method is being deprecated, please use save_doc instead" + save_doc(doc, bulk) + end + + # POST an array of documents to CouchDB. If any of the documents are # missing ids, supply one from the uuid cache. # @@ -160,7 +174,7 @@ module CouchRest doc['_id'] = nextid if nextid end end - CouchRest.post "#{@root}/_bulk_docs", {:docs => docs} + CouchRest.post "#{@uri}/_bulk_docs", {:docs => docs} end # DELETE the document from CouchDB that has the given _id and @@ -168,7 +182,7 @@ module CouchRest # # If bulk is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later. # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save. - def delete (doc, bulk = false) + def delete_doc(doc, bulk = false) raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev'] if bulk @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true } @@ -176,13 +190,19 @@ module CouchRest return { "ok" => true } # Mimic the non-deferred version end slug = escape_docid(doc['_id']) - CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}" + CouchRest.delete "#{@uri}/#{slug}?rev=#{doc['_rev']}" + end + + ### DEPRECATION NOTICE + def delete(doc, bulk=false) + puts "CouchRest::Database's delete method is being deprecated, please use delete_doc instead" + delete_doc(doc, bulk) end # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided. # dest can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc # hash with a '_rev' key - def copy doc, dest + def copy_doc(doc, dest) raise ArgumentError, "_id is required for copying" unless doc['_id'] slug = escape_docid(doc['_id']) destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev'] @@ -190,13 +210,19 @@ module CouchRest else dest end - CouchRest.copy "#{@root}/#{slug}", destination + CouchRest.copy "#{@uri}/#{slug}", destination + end + + ### DEPRECATION NOTICE + def copy(doc, dest) + puts "CouchRest::Database's copy method is being deprecated, please use copy_doc instead" + copy_doc(doc, dest) end # MOVE an existing document to a new id. If the destination id currently exists, a rev must be provided. # dest can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc # hash with a '_rev' key - def move doc, dest + def move_doc(doc, dest) raise ArgumentError, "_id and _rev are required for moving" unless doc['_id'] && doc['_rev'] slug = escape_docid(doc['_id']) destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev'] @@ -204,12 +230,33 @@ module CouchRest else dest end - CouchRest.move "#{@root}/#{slug}?rev=#{doc['_rev']}", destination + CouchRest.move "#{@uri}/#{slug}?rev=#{doc['_rev']}", destination + end + + ### DEPRECATION NOTICE + def move(doc, dest) + puts "CouchRest::Database's move method is being deprecated, please use move_doc instead" + move_doc(doc, dest) end # Compact the database, removing old document revisions and optimizing space use. def compact! - CouchRest.post "#{@root}/_compact" + CouchRest.post "#{@uri}/_compact" + end + + # Create the database + def create! + bool = server.create_db(@name) rescue false + bool && true + end + + # Delete and re create the database + def recreate! + delete! + create! + rescue RestClient::ResourceNotFound + ensure + create! end # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts. @@ -227,7 +274,7 @@ module CouchRest # DELETE the database itself. This is not undoable and could be rather # catastrophic. Use with care! def delete! - CouchRest.delete @root + CouchRest.delete @uri end private @@ -243,7 +290,7 @@ module CouchRest /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id) end - def encode_attachments attachments + def encode_attachments(attachments) attachments.each do |k,v| next if v['stub'] v['data'] = base64(v['data']) @@ -251,7 +298,7 @@ module CouchRest attachments end - def base64 data + def base64(data) Base64.encode64(data).gsub(/\s/,'') end end diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index 7c4e29b..0760624 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -38,7 +38,7 @@ module CouchRest # If bulk is true (defaults to false) the document is cached for bulk save. def save(bulk = false) raise ArgumentError, "doc.database required for saving" unless database - result = database.save self, bulk + result = database.save_doc self, bulk result['ok'] end @@ -49,7 +49,7 @@ module CouchRest # actually be deleted from the db until bulk save. def destroy(bulk = false) raise ArgumentError, "doc.database required to destroy" unless database - result = database.delete(self, bulk) + result = database.delete_doc(self, bulk) if result['ok'] self['_rev'] = nil self['_id'] = nil @@ -62,7 +62,7 @@ module CouchRest # hash with a '_rev' key def copy(dest) raise ArgumentError, "doc.database required to copy" unless database - result = database.copy(self, dest) + result = database.copy_doc(self, dest) result['ok'] end @@ -71,7 +71,7 @@ module CouchRest # hash with a '_rev' key def move(dest) raise ArgumentError, "doc.database required to copy" unless database - result = database.move(self, dest) + result = database.move_doc(self, dest) result['ok'] end diff --git a/lib/couchrest/core/model.rb b/lib/couchrest/core/model.rb index ff552d0..3f208cd 100644 --- a/lib/couchrest/core/model.rb +++ b/lib/couchrest/core/model.rb @@ -365,7 +365,7 @@ module CouchRest ddocs = all_design_doc_versions ddocs["rows"].each do |row| if (row['id'] != design_doc_id) - database.delete({ + database.delete_doc({ "_id" => row['id'], "_rev" => row['value']['rev'] }) @@ -485,9 +485,11 @@ module CouchRest alias :new_record? :new_document? # Overridden to set the unique ID. + # Returns a boolean value def save bulk = false set_unique_id if new_document? && self.respond_to?(:set_unique_id) - super(bulk) + result = database.save_doc(self, bulk) + result["ok"] == true end # Saves the document to the db using create or update. Raises an exception @@ -500,7 +502,7 @@ module CouchRest # Removes the _id and _rev fields, preparing the # document to be saved to a new _id. def destroy - result = database.delete self + result = database.delete_doc self if result['ok'] self['_rev'] = nil self['_id'] = nil diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 99cb6eb..3fac9fe 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -61,7 +61,7 @@ describe CouchRest::Database do emit(doc.word,null); } }'}} - @db.save({ + @db.save_doc({ "_id" => "_design/test", :views => @view }) @@ -80,7 +80,7 @@ describe CouchRest::Database do describe "select from an existing view" do before(:each) do - r = @db.save({ + r = @db.save_doc({ "_id" => "_design/first", :views => { :test => { @@ -129,9 +129,9 @@ describe CouchRest::Database do describe "GET (document by id) when the doc exists" do before(:each) do - @r = @db.save({'lemons' => 'from texas', 'and' => 'spain'}) + @r = @db.save_doc({'lemons' => 'from texas', 'and' => 'spain'}) @docid = "http://example.com/stuff.cgi?things=and%20stuff" - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should get the document" do doc = @db.get(@r['id']) @@ -176,7 +176,7 @@ describe CouchRest::Database do end it "in the case of an id conflict should not insert anything" do - @r = @db.save({'lemons' => 'from texas', 'and' => 'how', "_id" => "oneB"}) + @r = @db.save_doc({'lemons' => 'from texas', 'and' => 'how', "_id" => "oneB"}) lambda do rs = @db.bulk_save([ @@ -192,7 +192,7 @@ describe CouchRest::Database do end it "should empty the bulk save cache if no documents are given" do - @db.save({"_id" => "bulk_cache_1", "val" => "test"}, true) + @db.save_doc({"_id" => "bulk_cache_1", "val" => "test"}, true) lambda do @db.get('bulk_cache_1') end.should raise_error(RestClient::ResourceNotFound) @@ -201,7 +201,7 @@ describe CouchRest::Database do end it "should raise an error that is useful for recovery" do - @r = @db.save({"_id" => "taken", "field" => "stuff"}) + @r = @db.save_doc({"_id" => "taken", "field" => "stuff"}) begin rs = @db.bulk_save([ {"_id" => "taken", "wild" => "and random"}, @@ -220,13 +220,13 @@ describe CouchRest::Database do @db.documents["total_rows"].should == 0 end it "should create the document and return the id" do - r = @db.save({'lemons' => 'from texas', 'and' => 'spain'}) + r = @db.save_doc({'lemons' => 'from texas', 'and' => 'spain'}) r2 = @db.get(r['id']) r2["lemons"].should == "from texas" end it "should use PUT with UUIDs" do CouchRest.should_receive(:put).and_return({"ok" => true, "id" => "100", "rev" => "55"}) - r = @db.save({'just' => ['another document']}) + r = @db.save_doc({'just' => ['another document']}) end end @@ -284,7 +284,7 @@ describe CouchRest::Database do } } } - @db.save(@doc) + @db.save_doc(@doc) end it "should save and be indicated" do doc = @db.get("mydocwithattachment") @@ -308,10 +308,10 @@ describe CouchRest::Database do } } } - @db.save(doc) + @db.save_doc(doc) doc = @db.get('mydocwithattachment') doc['field'] << 'another value' - @db.save(doc) + @db.save_doc(doc) end it 'should be there' do @@ -338,7 +338,7 @@ describe CouchRest::Database do } } } - @db.save(@doc) + @db.save_doc(@doc) end it "should save and be indicated" do doc = @db.get("mydocwithattachment") @@ -388,7 +388,7 @@ describe CouchRest::Database do } } } - @docid = @db.save(@doc)['id'] + @docid = @db.save_doc(@doc)['id'] end it "should save and be indicated" do doc = @db.get(@docid) @@ -403,15 +403,15 @@ describe CouchRest::Database do describe "PUT (new document with url id)" do it "should create the document" do @docid = "http://example.com/stuff.cgi?things=and%20stuff" - @db.save({'_id' => @docid, 'will-exist' => 'here'}) - lambda{@db.save({'_id' => @docid})}.should raise_error(RestClient::Request::RequestFailed) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) + lambda{@db.save_doc({'_id' => @docid})}.should raise_error(RestClient::Request::RequestFailed) @db.get(@docid)['will-exist'].should == 'here' end end describe "PUT (new document with id)" do it "should start without the document" do - # r = @db.save({'lemons' => 'from texas', 'and' => 'spain'}) + # r = @db.save_doc({'lemons' => 'from texas', 'and' => 'spain'}) @db.documents['rows'].each do |doc| doc['id'].should_not == 'my-doc' end @@ -420,17 +420,17 @@ describe CouchRest::Database do # or instead make it return something with a fancy <=> method end it "should create the document" do - @db.save({'_id' => 'my-doc', 'will-exist' => 'here'}) - lambda{@db.save({'_id' => 'my-doc'})}.should raise_error(RestClient::Request::RequestFailed) + @db.save_doc({'_id' => 'my-doc', 'will-exist' => 'here'}) + lambda{@db.save_doc({'_id' => 'my-doc'})}.should raise_error(RestClient::Request::RequestFailed) end end describe "PUT (existing document with rev)" do before(:each) do - @db.save({'_id' => 'my-doc', 'will-exist' => 'here'}) + @db.save_doc({'_id' => 'my-doc', 'will-exist' => 'here'}) @doc = @db.get('my-doc') @docid = "http://example.com/stuff.cgi?things=and%20stuff" - @db.save({'_id' => @docid, 'now' => 'save'}) + @db.save_doc({'_id' => @docid, 'now' => 'save'}) end it "should start with the document" do @doc['will-exist'].should == 'here' @@ -439,18 +439,18 @@ describe CouchRest::Database do it "should save with url id" do doc = @db.get(@docid) doc['yaml'] = ['json', 'word.'] - @db.save doc + @db.save_doc doc @db.get(@docid)['yaml'].should == ['json', 'word.'] end it "should fail to resave without the rev" do @doc['them-keys'] = 'huge' @doc['_rev'] = 'wrong' - # @db.save(@doc) - lambda {@db.save(@doc)}.should raise_error + # @db.save_doc(@doc) + lambda {@db.save_doc(@doc)}.should raise_error end it "should update the document" do @doc['them-keys'] = 'huge' - @db.save(@doc) + @db.save_doc(@doc) now = @db.get('my-doc') now['them-keys'].should == 'huge' end @@ -459,7 +459,7 @@ describe CouchRest::Database do describe "cached bulk save" do it "stores documents in a database-specific cache" do td = {"_id" => "btd1", "val" => "test"} - @db.save(td, true) + @db.save_doc(td, true) @db.instance_variable_get("@bulk_save_cache").should == [td] end @@ -468,8 +468,8 @@ describe CouchRest::Database do @db.bulk_save_cache_limit = 3 td1 = {"_id" => "td1", "val" => true} td2 = {"_id" => "td2", "val" => 4} - @db.save(td1, true) - @db.save(td2, true) + @db.save_doc(td1, true) + @db.save_doc(td2, true) lambda do @db.get(td1["_id"]) end.should raise_error(RestClient::ResourceNotFound) @@ -477,7 +477,7 @@ describe CouchRest::Database do @db.get(td2["_id"]) end.should raise_error(RestClient::ResourceNotFound) td3 = {"_id" => "td3", "val" => "foo"} - @db.save(td3, true) + @db.save_doc(td3, true) @db.get(td1["_id"])["val"].should == td1["val"] @db.get(td2["_id"])["val"].should == td2["val"] @db.get(td3["_id"])["val"].should == td3["val"] @@ -487,11 +487,11 @@ describe CouchRest::Database do td1 = {"_id" => "blah", "val" => true} td2 = {"_id" => "steve", "val" => 3} @db.bulk_save_cache_limit = 50 - @db.save(td1, true) + @db.save_doc(td1, true) lambda do @db.get(td1["_id"]) end.should raise_error(RestClient::ResourceNotFound) - @db.save(td2) + @db.save_doc(td2) @db.get(td1["_id"])["val"].should == td1["val"] @db.get(td2["_id"])["val"].should == td2["val"] end @@ -499,27 +499,27 @@ describe CouchRest::Database do describe "DELETE existing document" do before(:each) do - @r = @db.save({'lemons' => 'from texas', 'and' => 'spain'}) + @r = @db.save_doc({'lemons' => 'from texas', 'and' => 'spain'}) @docid = "http://example.com/stuff.cgi?things=and%20stuff" - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should work" do doc = @db.get(@r['id']) doc['and'].should == 'spain' - @db.delete doc + @db.delete_doc doc lambda{@db.get @r['id']}.should raise_error end it "should work with uri id" do doc = @db.get(@docid) - @db.delete doc + @db.delete_doc doc lambda{@db.get @docid}.should raise_error end it "should fail without an _id" do - lambda{@db.delete({"not"=>"a real doc"})}.should raise_error(ArgumentError) + lambda{@db.delete_doc({"not"=>"a real doc"})}.should raise_error(ArgumentError) end it "should defer actual deletion when using bulk save" do doc = @db.get(@docid) - @db.delete doc, true + @db.delete_doc doc, true lambda{@db.get @docid}.should_not raise_error @db.bulk_save lambda{@db.get @docid}.should raise_error @@ -529,13 +529,13 @@ describe CouchRest::Database do describe "COPY existing document" do before :each do - @r = @db.save({'artist' => 'Zappa', 'title' => 'Muffin Man'}) + @r = @db.save_doc({'artist' => 'Zappa', 'title' => 'Muffin Man'}) @docid = 'tracks/zappa/muffin-man' @doc = @db.get(@r['id']) end describe "to a new location" do it "should work" do - @db.copy @doc, @docid + @db.copy_doc @doc, @docid newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' end @@ -545,20 +545,20 @@ describe CouchRest::Database do end describe "to an existing location" do before :each do - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should fail without a rev" do - lambda{@db.copy @doc, @docid}.should raise_error(RestClient::RequestFailed) + lambda{@db.copy_doc @doc, @docid}.should raise_error(RestClient::RequestFailed) end it "should succeed with a rev" do @to_be_overwritten = @db.get(@docid) - @db.copy @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}" + @db.copy_doc @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}" newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' end it "should succeed given the doc to overwrite" do @to_be_overwritten = @db.get(@docid) - @db.copy @doc, @to_be_overwritten + @db.copy_doc @doc, @to_be_overwritten newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' end @@ -567,13 +567,13 @@ describe CouchRest::Database do describe "MOVE existing document" do before :each do - @r = @db.save({'artist' => 'Zappa', 'title' => 'Muffin Man'}) + @r = @db.save_doc({'artist' => 'Zappa', 'title' => 'Muffin Man'}) @docid = 'tracks/zappa/muffin-man' @doc = @db.get(@r['id']) end describe "to a new location" do it "should work" do - @db.move @doc, @docid + @db.move_doc @doc, @docid newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound) @@ -585,22 +585,22 @@ describe CouchRest::Database do end describe "to an existing location" do before :each do - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should fail without a rev" do - lambda{@db.move @doc, @docid}.should raise_error(RestClient::RequestFailed) + lambda{@db.move_doc @doc, @docid}.should raise_error(RestClient::RequestFailed) lambda{@db.get(@r['id'])}.should_not raise_error end it "should succeed with a rev" do @to_be_overwritten = @db.get(@docid) - @db.move @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}" + @db.move_doc @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}" newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound) end it "should succeed given the doc to overwrite" do @to_be_overwritten = @db.get(@docid) - @db.move @doc, @to_be_overwritten + @db.move_doc @doc, @to_be_overwritten newdoc = @db.get(@docid) newdoc['artist'].should == 'Zappa' lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound) @@ -611,7 +611,7 @@ describe CouchRest::Database do it "should list documents" do 5.times do - @db.save({'another' => 'doc', 'will-exist' => 'anywhere'}) + @db.save_doc({'another' => 'doc', 'will-exist' => 'anywhere'}) end ds = @db.documents ds['rows'].should be_an_instance_of(Array) @@ -622,7 +622,7 @@ describe CouchRest::Database do describe "documents / _all_docs" do before(:each) do 9.times do |i| - @db.save({'_id' => "doc#{i}",'another' => 'doc', 'will-exist' => 'here'}) + @db.save_doc({'_id' => "doc#{i}",'another' => 'doc', 'will-exist' => 'here'}) end end it "should list documents with keys and such" do @@ -699,6 +699,40 @@ describe CouchRest::Database do doc['some-value'].should == 'foo' end end + + describe "creating a database" do + before(:each) do + @db = @cr.database('couchrest-test-db_to_create') + @db.delete! + end + + it "should just work fine" do + @cr.databases.should_not include('couchrest-test-db_to_create') + @db.create! + @cr.databases.should include('couchrest-test-db_to_create') + end + end + + describe "recreating a database" do + before(:each) do + @db = @cr.database('couchrest-test-db_to_create') + @db2 = @cr.database('couchrest-test-db_to_recreate') + @cr.databases.include?(@db.name) ? nil : @db.create! + @cr.databases.include?(@db2.name) ? @db2.delete! : nil + end + + it "should drop and recreate a database" do + @cr.databases.should include(@db.name) + @db.recreate! + @cr.databases.should include(@db.name) + end + + it "should recreate a db even tho it doesn't exist" do + @cr.databases.should_not include(@db2.name) + @db2.recreate! + @cr.databases.should include(@db2.name) + end + end diff --git a/spec/couchrest/core/design_spec.rb b/spec/couchrest/core/design_spec.rb index 4161653..af1fb01 100644 --- a/spec/couchrest/core/design_spec.rb +++ b/spec/couchrest/core/design_spec.rb @@ -62,7 +62,7 @@ describe CouchRest::Design do describe "from a saved document" do before(:each) do @db = reset_test_db! - @db.save({ + @db.save_doc({ "_id" => "_design/test", "views" => { "by_name" => { diff --git a/spec/couchrest/core/document_spec.rb b/spec/couchrest/core/document_spec.rb index 301bd47..a588db7 100644 --- a/spec/couchrest/core/document_spec.rb +++ b/spec/couchrest/core/document_spec.rb @@ -42,7 +42,7 @@ describe CouchRest::Document, "saving using a database" do before(:all) do @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") @db = reset_test_db! - @resp = @db.save(@doc) + @resp = @db.save_doc(@doc) end it "should apply the database" do @doc.database.should == @db @@ -71,7 +71,7 @@ end describe "getting from a database" do before(:all) do @db = reset_test_db! - @resp = @db.save({ + @resp = @db.save_doc({ "key" => "value" }) @doc = @db.get @resp['id'] @@ -95,7 +95,7 @@ end describe "destroying a document from a db" do before(:all) do @db = reset_test_db! - @resp = @db.save({ + @resp = @db.save_doc({ "key" => "value" }) @doc = @db.get @resp['id'] @@ -114,7 +114,7 @@ end describe "destroying a document from a db using bulk save" do before(:all) do @db = reset_test_db! - @resp = @db.save({ + @resp = @db.save_doc({ "key" => "value" }) @doc = @db.get @resp['id'] @@ -132,7 +132,7 @@ end describe "copying a document" do before :each do @db = reset_test_db! - @resp = @db.save({'key' => 'value'}) + @resp = @db.save_doc({'key' => 'value'}) @docid = 'new-location' @doc = @db.get(@resp['id']) end @@ -148,7 +148,7 @@ describe "copying a document" do end describe "to an existing location" do before :each do - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should fail without a rev" do lambda{@doc.copy @docid}.should raise_error(RestClient::RequestFailed) @@ -171,7 +171,7 @@ end describe "MOVE existing document" do before :each do @db = reset_test_db! - @resp = @db.save({'key' => 'value'}) + @resp = @db.save_doc({'key' => 'value'}) @docid = 'new-location' @doc = @db.get(@resp['id']) end @@ -189,7 +189,7 @@ describe "MOVE existing document" do end describe "to an existing location" do before :each do - @db.save({'_id' => @docid, 'will-exist' => 'here'}) + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) end it "should fail without a rev" do lambda{@doc.move @docid}.should raise_error(RestClient::RequestFailed) diff --git a/spec/couchrest/core/model_spec.rb b/spec/couchrest/core/model_spec.rb index 383e414..fdad16a 100644 --- a/spec/couchrest/core/model_spec.rb +++ b/spec/couchrest/core/model_spec.rb @@ -275,7 +275,7 @@ describe CouchRest::Model do } ] } - r = Course.database.save course_doc + r = Course.database.save_doc course_doc @course = Course.get r['id'] end it "should load the course" do @@ -336,7 +336,7 @@ describe CouchRest::Model do }, "final_test_at" => "2008/12/19 13:00:00 +0800" } - r = Course.database.save course_doc + r = Course.database.save_doc course_doc @course = Course.get r['id'] end it "should load the course" do @@ -353,7 +353,7 @@ describe CouchRest::Model do describe "cast keys to any type" do before(:all) do event_doc = { :subject => "Some event", :occurs_at => Time.now } - e = Event.database.save event_doc + e = Event.database.save_doc event_doc @event = Event.get e['id'] end @@ -408,7 +408,7 @@ describe CouchRest::Model do before(:each) do @art = Article.new @old = Article.database.get('this-is-the-title') rescue nil - Article.database.delete(@old) if @old + Article.database.delete_doc(@old) if @old end it "should be a new document" do @@ -538,7 +538,7 @@ describe CouchRest::Model do end it "should not include non-Articles" do - Article.database.save({"date" => 1}) + Article.database.save_doc({"date" => 1}) view = Article.by_date :raw => true view['rows'].length.should == 4 end @@ -591,7 +591,7 @@ describe CouchRest::Model do describe "a ducktype view" do before(:all) do - @id = @db.save({:dept => true})['id'] + @id = @db.save_doc({:dept => true})['id'] end it "should setup" do duck = Course.get(@id) # from a different db diff --git a/spec/couchrest/helpers/pager_spec.rb b/spec/couchrest/helpers/pager_spec.rb index 3d2ff60..ed16df3 100644 --- a/spec/couchrest/helpers/pager_spec.rb +++ b/spec/couchrest/helpers/pager_spec.rb @@ -60,7 +60,7 @@ describe CouchRest::Pager do @docs << ({:number => (i % 10)}) end @db.bulk_save(@docs) - @db.save({ + @db.save_doc({ '_id' => '_design/magic', 'views' => { 'number' => { From 427122c98a671437e44f6c6cb6359ffc2b7cb36a Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 28 Jan 2009 18:37:45 -0800 Subject: [PATCH 06/13] added some monkey patches to improve the http connection speed. (by keeping the http connection open) --- lib/couchrest/monkeypatches.rb | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lib/couchrest/monkeypatches.rb b/lib/couchrest/monkeypatches.rb index 3147ff3..bad327d 100644 --- a/lib/couchrest/monkeypatches.rb +++ b/lib/couchrest/monkeypatches.rb @@ -23,6 +23,24 @@ class Time # end end +# Monkey patch for faster net/http io +if RUBY_VERSION.to_f < 1.9 + class Net::BufferedIO #:nodoc: + alias :old_rbuf_fill :rbuf_fill + def rbuf_fill + begin + @rbuf << @io.read_nonblock(65536) + rescue Errno::EWOULDBLOCK + if IO.select([@io], nil, nil, @read_timeout) + @rbuf << @io.read_nonblock(65536) + else + raise Timeout::TimeoutError + end + end + end + end +end + module RestClient def self.copy(url, headers={}) Request.execute(:method => :copy, @@ -35,4 +53,47 @@ module RestClient :url => url, :headers => headers) end + + class Request + def transmit(uri, req, payload) + setup_credentials(req) + + Thread.current[:host] ||= uri.host + Thread.current[:port] ||= uri.port + + net = net_http_class.new(uri.host, uri.port) + + if Thread.current[:connection].nil? || Thread.current[:host] != uri.host + Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?) + net.use_ssl = uri.is_a?(URI::HTTPS) + net.verify_mode = OpenSSL::SSL::VERIFY_NONE + Thread.current[:connection] = net + Thread.current[:connection].start + end + + display_log request_log + http = Thread.current[:connection] + + http.read_timeout = @timeout if @timeout + begin + res = http.request(req, payload) + rescue + # p "Net::HTTP connection failed, reconnecting" + Thread.current[:connection].finish + http = Thread.current[:connection] = net + Thread.current[:connection].start + res = http.request(req, payload) + display_log response_log(res) + process_result res + else + display_log response_log(res) + process_result res + end + + rescue EOFError + raise RestClient::ServerBrokeConnection + rescue Timeout::Error + raise RestClient::RequestTimeout + end + end end \ No newline at end of file From d9fe6ba374acf13b0a78395a36801c0cbec20d45 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 28 Jan 2009 22:55:42 -0800 Subject: [PATCH 07/13] Started the refactoring work on couchrest. * A server can have multiple defined available databases set to be used by documents (think DM repos) * A server can have a default database so documents can easily share the same db connection * Let a document class have a default database to use * Give access to a document uri * extracted some of the document features to a mixin --- lib/couchrest.rb | 2 + lib/couchrest/core/document.rb | 67 ++--- lib/couchrest/core/server.rb | 55 +++- lib/couchrest/mixins.rb | 3 + lib/couchrest/mixins/views.rb | 59 ++++ spec/couchrest/core/document_spec.rb | 412 ++++++++++++++------------- spec/couchrest/core/server_spec.rb | 34 +++ 7 files changed, 392 insertions(+), 240 deletions(-) create mode 100644 lib/couchrest/mixins.rb create mode 100644 lib/couchrest/mixins/views.rb create mode 100644 spec/couchrest/core/server_spec.rb diff --git a/lib/couchrest.rb b/lib/couchrest.rb index f55d606..ad99639 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -37,6 +37,8 @@ module CouchRest autoload :FileManager, 'couchrest/helper/file_manager' autoload :Streamer, 'couchrest/helper/streamer' + require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') + # The CouchRest module methods handle the basic JSON serialization # and deserialization, as well as query parameters. The module also includes # some helpers for tasks like instantiating a new Database or Server instance. diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index 0760624..e2ca399 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -14,47 +14,20 @@ module CouchRest end class Document < Response + include CouchRest::Mixins::Views attr_accessor :database - - # alias for self['_id'] - def id - self['_id'] + @@database = nil + + # override the CouchRest::Model-wide default_database + # This is not a thread safe operation, do not change the model + # database at runtime. + def self.use_database(db) + @@database = db end - - # alias for self['_rev'] - def rev - self['_rev'] - end - - # returns true if the document has never been saved - def new_document? - !rev - end - - # Saves the document to the db using create or update. Also runs the :save - # callbacks. Sets the _id and _rev fields based on - # CouchDB's response. - # If bulk is true (defaults to false) the document is cached for bulk save. - def save(bulk = false) - raise ArgumentError, "doc.database required for saving" unless database - result = database.save_doc self, bulk - result['ok'] - end - - # Deletes the document from the database. Runs the :delete callbacks. - # Removes the _id and _rev fields, preparing the - # document to be saved to a new _id. - # If bulk is true (defaults to false) the document won't - # actually be deleted from the db until bulk save. - def destroy(bulk = false) - raise ArgumentError, "doc.database required to destroy" unless database - result = database.delete_doc(self, bulk) - if result['ok'] - self['_rev'] = nil - self['_id'] = nil - end - result['ok'] + + def self.database + @@database end # copies the document to a new id. If the destination id currently exists, a rev must be provided. @@ -75,6 +48,23 @@ module CouchRest result['ok'] end + # Returns the CouchDB uri for the document + def uri(append_rev = false) + return nil if new_document? + couch_uri = "http://#{database.uri}/#{CGI.escape(id)}" + if append_rev == true + couch_uri << "?rev=#{rev}" + elsif append_rev.kind_of?(Integer) + couch_uri << "?rev=#{append_rev}" + end + couch_uri + end + + # Returns the document's database + def database + @database || self.class.database + end + # saves an attachment directly to couchdb def put_attachment(name, file, options={}) raise ArgumentError, "doc.database required to put_attachment" unless database @@ -97,6 +87,5 @@ module CouchRest result['ok'] end end - end diff --git a/lib/couchrest/core/server.rb b/lib/couchrest/core/server.rb index e76ab82..8d39998 100644 --- a/lib/couchrest/core/server.rb +++ b/lib/couchrest/core/server.rb @@ -1,25 +1,62 @@ module CouchRest class Server - attr_accessor :uri, :uuid_batch_count - def initialize server = 'http://127.0.0.1:5984', uuid_batch_count = 1000 + attr_accessor :uri, :uuid_batch_count, :available_databases + def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000) @uri = server @uuid_batch_count = uuid_batch_count end - # List all databases on the server + # Lists all "available" databases. + # An available database, is a database that was specified + # as avaiable by your code. + # It allows to define common databases to use and reuse in your code + def available_databases + @available_databases ||= {} + end + + # Adds a new available database and create it unless it already exists + # + # Example: + # + # @couch = CouchRest::Server.new + # @couch.define_available_database(:default, "tech-blog") + # + def define_available_database(reference, db_name, create_unless_exists = true) + available_databases[reference.to_sym] = create_unless_exists ? database!(db_name) : database(db_name) + end + + # Checks that a database is set as available + # + # Example: + # + # @couch.available_database?(:default) + # + def available_database?(ref_or_name) + ref_or_name.is_a?(Symbol) ? available_databases.keys.include?(ref_or_name) : available_databases.values.map{|db| db.name}.include?(ref_or_name) + end + + def default_database=(name, create_unless_exists = true) + define_available_database(:default, name, create_unless_exists = true) + end + + def default_database + available_databases[:default] + end + + # Lists all databases on the server def databases CouchRest.get "#{@uri}/_all_dbs" end # Returns a CouchRest::Database for the given name - def database name + def database(name) CouchRest::Database.new(self, name) end # Creates the database if it doesn't exist - def database! name + def database!(name) create_db(name) rescue nil - database name + database(name) end # GET the welcome message @@ -28,9 +65,9 @@ module CouchRest end # Create a database - def create_db name + def create_db(name) CouchRest.put "#{@uri}/#{name}" - database name + database(name) end # Restart the CouchDB instance @@ -39,7 +76,7 @@ module CouchRest end # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs. - def next_uuid count = @uuid_batch_count + def next_uuid(count = @uuid_batch_count) @uuids ||= [] if @uuids.empty? @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"] diff --git a/lib/couchrest/mixins.rb b/lib/couchrest/mixins.rb new file mode 100644 index 0000000..e482ffb --- /dev/null +++ b/lib/couchrest/mixins.rb @@ -0,0 +1,3 @@ +mixins_dir = File.join(File.dirname(__FILE__), 'mixins') + +require File.join(mixins_dir, 'views') \ No newline at end of file diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/mixins/views.rb new file mode 100644 index 0000000..36eebf0 --- /dev/null +++ b/lib/couchrest/mixins/views.rb @@ -0,0 +1,59 @@ +module CouchRest + module Mixins + module Views + + # alias for self['_id'] + def id + self['_id'] + end + + # alias for self['_rev'] + def rev + self['_rev'] + end + + # returns true if the document has never been saved + def new_document? + !rev + end + + # Saves the document to the db using create or update. Also runs the :save + # callbacks. Sets the _id and _rev fields based on + # CouchDB's response. + # If bulk is true (defaults to false) the document is cached for bulk save. + def save(bulk = false) + raise ArgumentError, "doc.database required for saving" unless database + result = database.save_doc self, bulk + result['ok'] + end + + # Deletes the document from the database. Runs the :delete callbacks. + # Removes the _id and _rev fields, preparing the + # document to be saved to a new _id. + # If bulk is true (defaults to false) the document won't + # actually be deleted from the db until bulk save. + def destroy(bulk = false) + raise ArgumentError, "doc.database required to destroy" unless database + result = database.delete_doc(self, bulk) + if result['ok'] + self['_rev'] = nil + self['_id'] = nil + end + result['ok'] + end + + def copy(dest) + raise ArgumentError, "doc.database required to copy" unless database + result = database.copy_doc(self, dest) + result['ok'] + end + + def move(dest) + raise ArgumentError, "doc.database required to copy" unless database + result = database.move_doc(self, dest) + result['ok'] + end + + end + end +end \ No newline at end of file diff --git a/spec/couchrest/core/document_spec.rb b/spec/couchrest/core/document_spec.rb index a588db7..745432f 100644 --- a/spec/couchrest/core/document_spec.rb +++ b/spec/couchrest/core/document_spec.rb @@ -1,213 +1,241 @@ require File.dirname(__FILE__) + '/../../spec_helper' -describe CouchRest::Document, "[]=" do - before(:each) do - @doc = CouchRest::Document.new - end - it "should work" do - @doc["enamel"].should == nil - @doc["enamel"] = "Strong" - @doc["enamel"].should == "Strong" - end - it "[]= should convert to string" do - @doc["enamel"].should == nil - @doc[:enamel] = "Strong" - @doc["enamel"].should == "Strong" - end - it "should read as a string" do - @doc[:enamel] = "Strong" - @doc[:enamel].should == "Strong" - end -end +class Video < CouchRest::Document; end -describe CouchRest::Document, "new" do - before(:each) do - @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") - end - it "should create itself from a Hash" do - @doc["key"].should == [1,2,3] - @doc["more"].should == "values" - end - it "should not have rev and id" do - @doc.rev.should be_nil - @doc.id.should be_nil - end - it "should freak out when saving without a database" do - lambda{@doc.save}.should raise_error(ArgumentError) - end -end - -# move to database spec -describe CouchRest::Document, "saving using a database" do +describe CouchRest::Document do + before(:all) do - @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") - @db = reset_test_db! - @resp = @db.save_doc(@doc) - end - it "should apply the database" do - @doc.database.should == @db - end - it "should get id and rev" do - @doc.id.should == @resp["id"] - @doc.rev.should == @resp["rev"] - end -end - -describe CouchRest::Document, "bulk saving" do - before :all do - @db = reset_test_db! + @couch = CouchRest.new + @db = @couch.database!(TESTDB) end - it "should use the document bulk save cache" do - doc = CouchRest::Document.new({"_id" => "bulkdoc", "val" => 3}) - doc.database = @db - doc.save(true) - lambda { doc.database.get(doc["_id"]) }.should raise_error(RestClient::ResourceNotFound) - doc.database.bulk_save - doc.database.get(doc["_id"])["val"].should == doc["val"] - end -end - -describe "getting from a database" do - before(:all) do - @db = reset_test_db! - @resp = @db.save_doc({ - "key" => "value" - }) - @doc = @db.get @resp['id'] - end - it "should return a document" do - @doc.should be_an_instance_of(CouchRest::Document) - end - it "should have a database" do - @doc.database.should == @db - end - it "should be saveable and resavable" do - @doc["more"] = "keys" - @doc.save - @db.get(@resp['id'])["more"].should == "keys" - @doc["more"] = "these keys" - @doc.save - @db.get(@resp['id'])["more"].should == "these keys" - end -end - -describe "destroying a document from a db" do - before(:all) do - @db = reset_test_db! - @resp = @db.save_doc({ - "key" => "value" - }) - @doc = @db.get @resp['id'] - end - it "should make it disappear" do - @doc.destroy - lambda{@db.get @resp['id']}.should raise_error - end - it "should error when there's no db" do - @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") - lambda{@doc.destroy}.should raise_error(ArgumentError) - end -end - - -describe "destroying a document from a db using bulk save" do - before(:all) do - @db = reset_test_db! - @resp = @db.save_doc({ - "key" => "value" - }) - @doc = @db.get @resp['id'] - end - it "should defer actual deletion" do - @doc.destroy(true) - @doc['_id'].should == nil - @doc['_rev'].should == nil - lambda{@db.get @resp['id']}.should_not raise_error - @db.bulk_save - lambda{@db.get @resp['id']}.should raise_error - end -end - -describe "copying a document" do - before :each do - @db = reset_test_db! - @resp = @db.save_doc({'key' => 'value'}) - @docid = 'new-location' - @doc = @db.get(@resp['id']) - end - describe "to a new location" do + describe "[]=" do + before(:each) do + @doc = CouchRest::Document.new + end it "should work" do - @doc.copy @docid - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' + @doc["enamel"].should == nil + @doc["enamel"] = "Strong" + @doc["enamel"].should == "Strong" end - it "should fail without a database" do - lambda{CouchRest::Document.new({"not"=>"a real doc"}).copy}.should raise_error(ArgumentError) + it "[]= should convert to string" do + @doc["enamel"].should == nil + @doc[:enamel] = "Strong" + @doc["enamel"].should == "Strong" + end + it "should read as a string" do + @doc[:enamel] = "Strong" + @doc[:enamel].should == "Strong" end end - describe "to an existing location" do - before :each do - @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) - end - it "should fail without a rev" do - lambda{@doc.copy @docid}.should raise_error(RestClient::RequestFailed) - end - it "should succeed with a rev" do - @to_be_overwritten = @db.get(@docid) - @doc.copy "#{@docid}?rev=#{@to_be_overwritten['_rev']}" - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' - end - it "should succeed given the doc to overwrite" do - @to_be_overwritten = @db.get(@docid) - @doc.copy @to_be_overwritten - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' - end - end -end -describe "MOVE existing document" do - before :each do - @db = reset_test_db! - @resp = @db.save_doc({'key' => 'value'}) - @docid = 'new-location' - @doc = @db.get(@resp['id']) - end - describe "to a new location" do - it "should work" do - @doc.move @docid - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' - lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + describe "default database" do + it "should be set using use_database on the model" do + Video.new.database.should be_nil + Video.use_database @db + Video.new.database.should == @db + Video.use_database nil end - it "should fail without a database" do - lambda{CouchRest::Document.new({"not"=>"a real doc"}).move}.should raise_error(ArgumentError) - lambda{CouchRest::Document.new({"_id"=>"not a real doc"}).move}.should raise_error(ArgumentError) + + it "should be overwritten by instance" do + db = @couch.database('test') + article = Video.new + article.database.should be_nil + article.database = db + article.database.should_not be_nil + article.database.should == db end end - describe "to an existing location" do + + describe "new" do + before(:each) do + @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") + end + it "should create itself from a Hash" do + @doc["key"].should == [1,2,3] + @doc["more"].should == "values" + end + it "should not have rev and id" do + @doc.rev.should be_nil + @doc.id.should be_nil + end + it "should freak out when saving without a database" do + lambda{@doc.save}.should raise_error(ArgumentError) + end + end + + # move to database spec + describe "saving using a database" do + before(:all) do + @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") + @db = reset_test_db! + @resp = @db.save_doc(@doc) + end + it "should apply the database" do + @doc.database.should == @db + end + it "should get id and rev" do + @doc.id.should == @resp["id"] + @doc.rev.should == @resp["rev"] + end + end + + describe "bulk saving" do + before :all do + @db = reset_test_db! + end + + it "should use the document bulk save cache" do + doc = CouchRest::Document.new({"_id" => "bulkdoc", "val" => 3}) + doc.database = @db + doc.save(true) + lambda { doc.database.get(doc["_id"]) }.should raise_error(RestClient::ResourceNotFound) + doc.database.bulk_save + doc.database.get(doc["_id"])["val"].should == doc["val"] + end + end + + describe "getting from a database" do + before(:all) do + @db = reset_test_db! + @resp = @db.save_doc({ + "key" => "value" + }) + @doc = @db.get @resp['id'] + end + it "should return a document" do + @doc.should be_an_instance_of(CouchRest::Document) + end + it "should have a database" do + @doc.database.should == @db + end + it "should be saveable and resavable" do + @doc["more"] = "keys" + @doc.save + @db.get(@resp['id'])["more"].should == "keys" + @doc["more"] = "these keys" + @doc.save + @db.get(@resp['id'])["more"].should == "these keys" + end + end + + describe "destroying a document from a db" do + before(:all) do + @db = reset_test_db! + @resp = @db.save_doc({ + "key" => "value" + }) + @doc = @db.get @resp['id'] + end + it "should make it disappear" do + @doc.destroy + lambda{@db.get @resp['id']}.should raise_error + end + it "should error when there's no db" do + @doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") + lambda{@doc.destroy}.should raise_error(ArgumentError) + end + end + + + describe "destroying a document from a db using bulk save" do + before(:all) do + @db = reset_test_db! + @resp = @db.save_doc({ + "key" => "value" + }) + @doc = @db.get @resp['id'] + end + it "should defer actual deletion" do + @doc.destroy(true) + @doc['_id'].should == nil + @doc['_rev'].should == nil + lambda{@db.get @resp['id']}.should_not raise_error + @db.bulk_save + lambda{@db.get @resp['id']}.should raise_error + end + end + + describe "copying a document" do before :each do - @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) + @db = reset_test_db! + @resp = @db.save_doc({'key' => 'value'}) + @docid = 'new-location' + @doc = @db.get(@resp['id']) end - it "should fail without a rev" do - lambda{@doc.move @docid}.should raise_error(RestClient::RequestFailed) - lambda{@db.get(@resp['id'])}.should_not raise_error + describe "to a new location" do + it "should work" do + @doc.copy @docid + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + end + it "should fail without a database" do + lambda{CouchRest::Document.new({"not"=>"a real doc"}).copy}.should raise_error(ArgumentError) + end end - it "should succeed with a rev" do - @to_be_overwritten = @db.get(@docid) - @doc.move "#{@docid}?rev=#{@to_be_overwritten['_rev']}" - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' - lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + describe "to an existing location" do + before :each do + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) + end + it "should fail without a rev" do + lambda{@doc.copy @docid}.should raise_error(RestClient::RequestFailed) + end + it "should succeed with a rev" do + @to_be_overwritten = @db.get(@docid) + @doc.copy "#{@docid}?rev=#{@to_be_overwritten['_rev']}" + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + end + it "should succeed given the doc to overwrite" do + @to_be_overwritten = @db.get(@docid) + @doc.copy @to_be_overwritten + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + end end - it "should succeed given the doc to overwrite" do - @to_be_overwritten = @db.get(@docid) - @doc.move @to_be_overwritten - newdoc = @db.get(@docid) - newdoc['key'].should == 'value' - lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + end + + describe "MOVE existing document" do + before :each do + @db = reset_test_db! + @resp = @db.save_doc({'key' => 'value'}) + @docid = 'new-location' + @doc = @db.get(@resp['id']) + end + describe "to a new location" do + it "should work" do + @doc.move @docid + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + end + it "should fail without a database" do + lambda{CouchRest::Document.new({"not"=>"a real doc"}).move}.should raise_error(ArgumentError) + lambda{CouchRest::Document.new({"_id"=>"not a real doc"}).move}.should raise_error(ArgumentError) + end + end + describe "to an existing location" do + before :each do + @db.save_doc({'_id' => @docid, 'will-exist' => 'here'}) + end + it "should fail without a rev" do + lambda{@doc.move @docid}.should raise_error(RestClient::RequestFailed) + lambda{@db.get(@resp['id'])}.should_not raise_error + end + it "should succeed with a rev" do + @to_be_overwritten = @db.get(@docid) + @doc.move "#{@docid}?rev=#{@to_be_overwritten['_rev']}" + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + end + it "should succeed given the doc to overwrite" do + @to_be_overwritten = @db.get(@docid) + @doc.move @to_be_overwritten + newdoc = @db.get(@docid) + newdoc['key'].should == 'value' + lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) + end end end end diff --git a/spec/couchrest/core/server_spec.rb b/spec/couchrest/core/server_spec.rb new file mode 100644 index 0000000..8a23e10 --- /dev/null +++ b/spec/couchrest/core/server_spec.rb @@ -0,0 +1,34 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +describe CouchRest::Server do + + before(:all) do + @couch = CouchRest::Server.new + end + + after(:all) do + @couch.available_databases.each do |ref, db| + db.delete! + end + end + + describe "available databases" do + it "should let you add more databases" do + @couch.available_databases.should be_empty + @couch.define_available_database(:default, "cr-server-test-db") + @couch.available_databases.keys.should include(:default) + end + + it "should verify that a database is available" do + @couch.available_database?(:default).should be_true + @couch.available_database?("cr-server-test-db").should be_true + @couch.available_database?(:matt).should be_false + end + + it "should let you set a default database" do + @couch.default_database = 'cr-server-test-default-db' + @couch.available_database?(:default).should be_true + end + end + +end \ No newline at end of file From 0c27fa6498635ec33ddf4c0c5dc1b1c23d19f5f1 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 28 Jan 2009 23:04:22 -0800 Subject: [PATCH 08/13] updated readme file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c42f06b..eeb0cfc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Quick Start: # with !, it creates the database if it doesn't already exist @db = CouchRest.database!("http://127.0.0.1:5984/couchrest-test") - response = @db.save({:key => 'value', 'another key' => 'another value'}) + response = @db.save_doc({:key => 'value', 'another key' => 'another value'}) doc = @db.get(response['id']) puts doc.inspect @@ -47,7 +47,7 @@ Bulk Save: Creating and Querying Views: - @db.save({ + @db.save_doc({ "_id" => "_design/first", :views => { :test => { From 83d73415535c0351cac9184eb270fcd3dc8539ab Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Thu, 29 Jan 2009 18:25:45 -0800 Subject: [PATCH 09/13] Started on the ExtendedDocument class with features moved to mixins. Properties got added, they define getters, setters and aliases. They will also be the base of the new validation system. --- lib/couchrest.rb | 2 + lib/couchrest/core/document.rb | 2 +- lib/couchrest/mixins/design_doc.rb | 63 +++++++ lib/couchrest/mixins/document_queries.rb | 42 +++++ .../mixins/extended_document_mixins.rb | 4 + lib/couchrest/mixins/extended_views.rb | 169 ++++++++++++++++++ lib/couchrest/mixins/properties.rb | 63 +++++++ lib/couchrest/more/extended_document.rb | 114 ++++++++++++ lib/couchrest/more/property.rb | 26 +++ spec/couchrest/core/document_spec.rb | 5 + spec/couchrest/more/property_spec.rb | 36 ++++ spec/spec_helper.rb | 15 +- 12 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 lib/couchrest/mixins/design_doc.rb create mode 100644 lib/couchrest/mixins/document_queries.rb create mode 100644 lib/couchrest/mixins/extended_document_mixins.rb create mode 100644 lib/couchrest/mixins/extended_views.rb create mode 100644 lib/couchrest/mixins/properties.rb create mode 100644 lib/couchrest/more/extended_document.rb create mode 100644 lib/couchrest/more/property.rb create mode 100644 spec/couchrest/more/property_spec.rb diff --git a/lib/couchrest.rb b/lib/couchrest.rb index ad99639..74a72a3 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -37,6 +37,8 @@ module CouchRest autoload :FileManager, 'couchrest/helper/file_manager' autoload :Streamer, 'couchrest/helper/streamer' + autoload :ExtendedDocument, 'couchrest/more/extended_document' + require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') # The CouchRest module methods handle the basic JSON serialization diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index e2ca399..a6e46f8 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -1,6 +1,6 @@ module CouchRest class Response < Hash - def initialize keys = {} + def initialize(keys = {}) keys.each do |k,v| self[k.to_s] = v end diff --git a/lib/couchrest/mixins/design_doc.rb b/lib/couchrest/mixins/design_doc.rb new file mode 100644 index 0000000..96e44e1 --- /dev/null +++ b/lib/couchrest/mixins/design_doc.rb @@ -0,0 +1,63 @@ +require 'digest/md5' + +module CouchRest + module Mixins + module DesignDoc + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def design_doc_id + "_design/#{design_doc_slug}" + end + + def design_doc_slug + return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh + funcs = [] + design_doc['views'].each do |name, view| + funcs << "#{name}/#{view['map']}#{view['reduce']}" + end + md5 = Digest::MD5.hexdigest(funcs.sort.join('')) + self.design_doc_slug_cache = "#{self.to_s}-#{md5}" + end + + def default_design_doc + { + "language" => "javascript", + "views" => { + 'all' => { + 'map' => "function(doc) { + if (doc['couchrest-type'] == '#{self.to_s}') { + emit(null,null); + } + }" + } + } + } + end + + def refresh_design_doc + did = design_doc_id + saved = database.get(did) rescue nil + if saved + design_doc['views'].each do |name, view| + saved['views'][name] = view + end + database.save_doc(saved) + self.design_doc = saved + else + design_doc['_id'] = did + design_doc.delete('_rev') + design_doc.database = database + design_doc.save + end + self.design_doc_fresh = true + end + + end # module ClassMethods + + end + end +end \ No newline at end of file diff --git a/lib/couchrest/mixins/document_queries.rb b/lib/couchrest/mixins/document_queries.rb new file mode 100644 index 0000000..c999ed1 --- /dev/null +++ b/lib/couchrest/mixins/document_queries.rb @@ -0,0 +1,42 @@ +module CouchRest + module Mixins + module DocumentQueries + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + + # Load all documents that have the "couchrest-type" field equal to the + # name of the current class. Take the standard set of + # CouchRest::Database#view options. + def all(opts = {}, &block) + self.design_doc ||= Design.new(default_design_doc) + unless design_doc_fresh + refresh_design_doc + end + view :all, opts, &block + end + + # Load the first document that have the "couchrest-type" field equal to + # the name of the current class. + # + # ==== Returns + # Object:: The first object instance available + # or + # Nil:: if no instances available + # + # ==== Parameters + # opts:: + # View options, see CouchRest::Database#view options for more info. + def first(opts = {}) + first_instance = self.all(opts.merge!(:limit => 1)) + first_instance.empty? ? nil : first_instance.first + end + + end + + end + end +end \ No newline at end of file diff --git a/lib/couchrest/mixins/extended_document_mixins.rb b/lib/couchrest/mixins/extended_document_mixins.rb new file mode 100644 index 0000000..8b9767e --- /dev/null +++ b/lib/couchrest/mixins/extended_document_mixins.rb @@ -0,0 +1,4 @@ +require File.join(File.dirname(__FILE__), 'properties') +require File.join(File.dirname(__FILE__), 'document_queries') +require File.join(File.dirname(__FILE__), 'extended_views') +require File.join(File.dirname(__FILE__), 'design_doc') \ No newline at end of file diff --git a/lib/couchrest/mixins/extended_views.rb b/lib/couchrest/mixins/extended_views.rb new file mode 100644 index 0000000..7f32f20 --- /dev/null +++ b/lib/couchrest/mixins/extended_views.rb @@ -0,0 +1,169 @@ +module CouchRest + module Mixins + module ExtendedViews + + def self.included(base) + base.extend(ClassMethods) + # extlib is required for the following code + base.send(:class_inheritable_accessor, :design_doc) + base.send(:class_inheritable_accessor, :design_doc_slug_cache) + base.send(:class_inheritable_accessor, :design_doc_fresh) + end + + module ClassMethods + + # Define a CouchDB view. The name of the view will be the concatenation + # of by and the keys joined by _and_ + # + # ==== Example views: + # + # class Post + # # view with default options + # # query with Post.by_date + # view_by :date, :descending => true + # + # # view with compound sort-keys + # # query with Post.by_user_id_and_date + # view_by :user_id, :date + # + # # view with custom map/reduce functions + # # query with Post.by_tags :reduce => true + # view_by :tags, + # :map => + # "function(doc) { + # if (doc['couchrest-type'] == 'Post' && doc.tags) { + # doc.tags.forEach(function(tag){ + # emit(doc.tag, 1); + # }); + # } + # }", + # :reduce => + # "function(keys, values, rereduce) { + # return sum(values); + # }" + # end + # + # view_by :date will create a view defined by this Javascript + # function: + # + # function(doc) { + # if (doc['couchrest-type'] == 'Post' && doc.date) { + # emit(doc.date, null); + # } + # } + # + # It can be queried by calling Post.by_date which accepts all + # valid options for CouchRest::Database#view. In addition, calling with + # the :raw => true option will return the view rows + # themselves. By default Post.by_date will return the + # documents included in the generated view. + # + # CouchRest::Database#view options can be applied at view definition + # time as defaults, and they will be curried and used at view query + # time. Or they can be overridden at query time. + # + # Custom views can be queried with :reduce => true to return + # reduce results. The default for custom views is to query with + # :reduce => false. + # + # Views are generated (on a per-model basis) lazily on first-access. + # This means that if you are deploying changes to a view, the views for + # that model won't be available until generation is complete. This can + # take some time with large databases. Strategies are in the works. + # + # To understand the capabilities of this view system more compeletly, + # it is recommended that you read the RSpec file at + # spec/core/model_spec.rb. + + def view_by(*keys) + self.design_doc ||= Design.new(default_design_doc) + opts = keys.pop if keys.last.is_a?(Hash) + opts ||= {} + ducktype = opts.delete(:ducktype) + unless ducktype || opts[:map] + opts[:guards] ||= [] + opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')" + end + keys.push opts + self.design_doc.view_by(*keys) + self.design_doc_fresh = false + end + + # returns stored defaults if the there is a view named this in the design doc + def has_view?(view) + view = view.to_s + design_doc && design_doc['views'] && design_doc['views'][view] + end + + # Dispatches to any named view. + def view name, query={}, &block + unless design_doc_fresh + refresh_design_doc + end + query[:raw] = true if query[:reduce] + raw = query.delete(:raw) + fetch_view_with_docs(name, query, raw, &block) + end + + def all_design_doc_versions + database.documents :startkey => "_design/#{self.to_s}-", + :endkey => "_design/#{self.to_s}-\u9999" + end + + # Deletes any non-current design docs that were created by this class. + # Running this when you're deployed version of your application is steadily + # and consistently using the latest code, is the way to clear out old design + # docs. Running it to early could mean that live code has to regenerate + # potentially large indexes. + def cleanup_design_docs! + ddocs = all_design_doc_versions + ddocs["rows"].each do |row| + if (row['id'] != design_doc_id) + database.delete_doc({ + "_id" => row['id'], + "_rev" => row['value']['rev'] + }) + end + end + end + + private + + def fetch_view_with_docs name, opts, raw=false, &block + if raw + fetch_view name, opts, &block + else + begin + view = fetch_view name, opts.merge({:include_docs => true}), &block + view['rows'].collect{|r|new(r['doc'])} if view['rows'] + rescue + # fallback for old versions of couchdb that don't + # have include_docs support + view = fetch_view name, opts, &block + view['rows'].collect{|r|new(database.get(r['id']))} if view['rows'] + end + end + end + + def fetch_view view_name, opts, &block + retryable = true + begin + design_doc.view(view_name, opts, &block) + # the design doc could have been deleted by a rouge process + rescue RestClient::ResourceNotFound => e + if retryable + refresh_design_doc + retryable = false + retry + else + raise e + end + end + end + + end # module ClassMethods + + + end + end +end \ No newline at end of file diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb new file mode 100644 index 0000000..8b92ada --- /dev/null +++ b/lib/couchrest/mixins/properties.rb @@ -0,0 +1,63 @@ +module CouchRest + module Mixins + module DocumentProperties + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # Stores the class properties + def properties + @@properties ||= [] + end + + # This is not a thread safe operation, if you have to set new properties at runtime + # make sure to use a mutex. + def property(name, options={}) + unless properties.map{|p| p.name}.include?(name.to_s) + property = CouchRest::Property.new(name, options.delete(:type), options) + create_property_getter(property) + create_property_setter(property) unless property.read_only == true + properties << property + end + end + + protected + # defines the getter for the property + def create_property_getter(property) + meth = property.name + class_eval <<-EOS + def #{meth} + self['#{meth}'] + end + EOS + + if property.alias + class_eval <<-EOS + alias #{property.alias.to_sym} #{meth.to_sym} + EOS + end + end + + # defines the setter for the property + def create_property_setter(property) + meth = property.name + class_eval <<-EOS + def #{meth}=(value) + self['#{meth}'] = value + end + EOS + + if property.alias + class_eval <<-EOS + alias #{property.alias.to_sym}= #{meth.to_sym}= + EOS + end + end + + end # module ClassMethods + + end + end +end \ No newline at end of file diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb new file mode 100644 index 0000000..bf429b0 --- /dev/null +++ b/lib/couchrest/more/extended_document.rb @@ -0,0 +1,114 @@ +require 'rubygems' +begin + gem 'extlib' + require 'extlib' +rescue + puts "CouchRest::Model requires extlib. This is left out of the gemspec on purpose." + raise +end +require 'mime/types' +require File.join(File.dirname(__FILE__), "property") +require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins') + +module CouchRest + + # Same as CouchRest::Document but with properties and validations + class ExtendedDocument < Document + include CouchRest::Mixins::DocumentQueries + include CouchRest::Mixins::DocumentProperties + include CouchRest::Mixins::ExtendedViews + include CouchRest::Mixins::DesignDoc + + + # Automatically set updated_at and created_at fields + # on the document whenever saving occurs. CouchRest uses a pretty + # decent time format by default. See Time#to_json + def self.timestamps! + before(:save) do + self['updated_at'] = Time.now + self['created_at'] = self['updated_at'] if new_document? + end + end + + # Name a method that will be called before the document is first saved, + # which returns a string to be used for the document's _id. + # Because CouchDB enforces a constraint that each id must be unique, + # this can be used to enforce eg: uniq usernames. Note that this id + # must be globally unique across all document types which share a + # database, so if you'd like to scope uniqueness to this class, you + # should use the class name as part of the unique id. + def self.unique_id method = nil, &block + if method + define_method :set_unique_id do + self['_id'] ||= self.send(method) + end + elsif block + define_method :set_unique_id do + uniqid = block.call(self) + raise ArgumentError, "unique_id block must not return nil" if uniqid.nil? + self['_id'] ||= uniqid + end + end + end + + ### instance methods + + # Returns the Class properties + # + # ==== Returns + # Array:: the list of properties for the instance + def properties + self.class.properties + end + + # Takes a hash as argument, and applies the values by using writer methods + # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are + # missing. In case of error, no attributes are changed. + def update_attributes_without_saving hash + hash.each do |k, v| + raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=") + end + hash.each do |k, v| + self.send("#{k}=",v) + end + end + + # Takes a hash as argument, and applies the values by using writer methods + # for each key. Raises a NoMethodError if the corresponding methods are + # missing. In case of error, no attributes are changed. + def update_attributes hash + update_attributes_without_saving hash + save + end + + # for compatibility with old-school frameworks + alias :new_record? :new_document? + + # Overridden to set the unique ID. + # Returns a boolean value + def save bulk = false + set_unique_id if new_document? && self.respond_to?(:set_unique_id) + result = database.save_doc(self, bulk) + result["ok"] == true + end + + # Saves the document to the db using create or update. Raises an exception + # if the document is not saved properly. + def save! + raise "#{self.inspect} failed to save" unless self.save + end + + # Deletes the document from the database. Runs the :destroy callbacks. + # Removes the _id and _rev fields, preparing the + # document to be saved to a new _id. + def destroy + result = database.delete_doc self + if result['ok'] + self['_rev'] = nil + self['_id'] = nil + end + result['ok'] + end + + end +end \ No newline at end of file diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb new file mode 100644 index 0000000..7b993ff --- /dev/null +++ b/lib/couchrest/more/property.rb @@ -0,0 +1,26 @@ +module CouchRest + + # Basic attribute support adding getter/setter + validation + class Property + attr_reader :name, :type, :validation_format, :required, :read_only, :alias + + # attribute to define + def initialize(name, type = String, options = {}) + @name = name.to_s + @type = type + parse_options(options) + self + end + + + private + def parse_options(options) + return if options.empty? + @required = true if (options[:required] && (options[:required] == true)) + @validation_format = options[:format] if options[:format] + @read_only = options[:read_only] if options[:read_only] + @alias = options[:alias] if options + end + + end +end \ No newline at end of file diff --git a/spec/couchrest/core/document_spec.rb b/spec/couchrest/core/document_spec.rb index 745432f..140ce8b 100644 --- a/spec/couchrest/core/document_spec.rb +++ b/spec/couchrest/core/document_spec.rb @@ -30,6 +30,9 @@ describe CouchRest::Document do end describe "default database" do + before(:each) do + Video.use_database nil + end it "should be set using use_database on the model" do Video.new.database.should be_nil Video.use_database @db @@ -59,9 +62,11 @@ describe CouchRest::Document do @doc.rev.should be_nil @doc.id.should be_nil end + it "should freak out when saving without a database" do lambda{@doc.save}.should raise_error(ArgumentError) end + end # move to database spec diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb new file mode 100644 index 0000000..780e587 --- /dev/null +++ b/spec/couchrest/more/property_spec.rb @@ -0,0 +1,36 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +# check the following file to see how to use the spec'd features. +require File.join(FIXTURE_PATH, 'more', 'card') + +describe "ExtendedDocument properties" do + + before(:each) do + @card = Card.new(:first_name => "matt") + end + + it "should be accessible from the object" do + @card.properties.should be_an_instance_of(Array) + @card.properties.map{|p| p.name}.should include("first_name") + end + + it "should let you access a property value (getter)" do + @card.first_name.should == "matt" + end + + it "should let you set a property value (setter)" do + @card.last_name = "Aimonetti" + @card.last_name.should == "Aimonetti" + end + + it "should not let you set a property value if it's read only" do + lambda{@card.read_only_value = "test"}.should raise_error + end + + it "should let you use an alias for an attribute" do + @card.last_name = "Aimonetti" + @card.family_name.should == "Aimonetti" + @card.family_name.should == @card.last_name + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 96ad5f0..06c7979 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,20 +1,21 @@ require "rubygems" require "spec" # Satisfies Autotest and anyone else not using the Rake tasks -require File.dirname(__FILE__) + '/../lib/couchrest' +require File.join(File.dirname(__FILE__), '/../lib/couchrest') unless defined?(FIXTURE_PATH) - FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures' - SCRATCH_PATH = File.dirname(__FILE__) + '/tmp' + FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures') + SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp') COUCHHOST = "http://127.0.0.1:5984" - TESTDB = 'couchrest-test' + TESTDB = 'couchrest-test' + TEST_SERVER = CouchRest.new + TEST_SERVER.default_database = TESTDB end def reset_test_db! - cr = CouchRest.new(COUCHHOST) + cr = TEST_SERVER db = cr.database(TESTDB) - db.delete! rescue nil - db = cr.create_db(TESTDB) rescue nin + db.recreate! rescue nil db end \ No newline at end of file From d64fa45cf05032a74a1624c7d3fcc73d6d1a6af3 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Thu, 29 Jan 2009 18:45:01 -0800 Subject: [PATCH 10/13] Started on the ExtendedDocument class with features moved to mixins. Properties got added, they define getters, setters and aliases. They will also be the base of the new validation system. --- lib/couchrest/mixins/document_queries.rb | 6 ++++++ lib/couchrest/more/extended_document.rb | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/couchrest/mixins/document_queries.rb b/lib/couchrest/mixins/document_queries.rb index c999ed1..bd7e450 100644 --- a/lib/couchrest/mixins/document_queries.rb +++ b/lib/couchrest/mixins/document_queries.rb @@ -35,6 +35,12 @@ module CouchRest first_instance.empty? ? nil : first_instance.first end + # Load a document from the database by id + def get(id) + doc = database.get id + new(doc) + end + end end diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index bf429b0..40bbfd6 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -64,7 +64,7 @@ module CouchRest # Takes a hash as argument, and applies the values by using writer methods # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are # missing. In case of error, no attributes are changed. - def update_attributes_without_saving hash + def update_attributes_without_saving(hash) hash.each do |k, v| raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=") end @@ -76,7 +76,7 @@ module CouchRest # Takes a hash as argument, and applies the values by using writer methods # for each key. Raises a NoMethodError if the corresponding methods are # missing. In case of error, no attributes are changed. - def update_attributes hash + def update_attributes(hash) update_attributes_without_saving hash save end @@ -86,7 +86,7 @@ module CouchRest # Overridden to set the unique ID. # Returns a boolean value - def save bulk = false + def save(bulk = false) set_unique_id if new_document? && self.respond_to?(:set_unique_id) result = database.save_doc(self, bulk) result["ok"] == true From 55cf74185969c90297d738e299b94df0e32454d9 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Mon, 2 Feb 2009 14:56:37 -0800 Subject: [PATCH 11/13] fix rebase end balance --- spec/couchrest/core/database_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 3fac9fe..7e621c2 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -699,6 +699,7 @@ describe CouchRest::Database do doc['some-value'].should == 'foo' end end + end describe "creating a database" do before(:each) do From bd2dafd1070f3edb958c23534760af117cd0c45c Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Mon, 2 Feb 2009 15:03:10 -0800 Subject: [PATCH 12/13] add mattetti's 5aebd53a93c5f9df8b94879920c8b7f3d23b8f7a --- spec/fixtures/more/card.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 spec/fixtures/more/card.rb diff --git a/spec/fixtures/more/card.rb b/spec/fixtures/more/card.rb new file mode 100644 index 0000000..b6f1420 --- /dev/null +++ b/spec/fixtures/more/card.rb @@ -0,0 +1,7 @@ +class Card < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :first_name + property :last_name, :alias => :family_name + property :read_only_value, :read_only => true + +end \ No newline at end of file From 60c577963d8656bb08959762d4eb86d2ce56ba0d Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Mon, 2 Feb 2009 15:24:31 -0800 Subject: [PATCH 13/13] all specs pass; refined attachment api --- lib/couchrest/core/database.rb | 31 ++++++++++++++++++++----------- lib/couchrest/core/document.rb | 10 ++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 1665f1e..ed98c16 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -87,27 +87,28 @@ module CouchRest end # GET an attachment directly from CouchDB - def fetch_attachment(docid, name) - slug = escape_docid(docid) - name = CGI.escape(name) - RestClient.get "#{@uri}/#{slug}/#{name}" + def fetch_attachment(doc, name) + # slug = escape_docid(docid) + # name = CGI.escape(name) + + uri = uri_for_attachment(doc, name) + + RestClient.get uri + # "#{@uri}/#{slug}/#{name}" end # PUT an attachment directly to CouchDB def put_attachment(doc, name, file, options = {}) docid = escape_docid(doc['_id']) name = CGI.escape(name) - uri = if doc['_rev'] - "#{@uri}/#{docid}/#{name}?rev=#{doc['_rev']}" - else - "#{@uri}/#{docid}/#{name}" - end + uri = uri_for_attachment(doc, name) JSON.parse(RestClient.put(uri, file, options)) end # DELETE an attachment directly from CouchDB def delete_attachment doc, name uri = uri_for_attachment(doc, name) + # this needs a rev JSON.parse(RestClient.delete(uri)) end @@ -280,9 +281,17 @@ module CouchRest private def uri_for_attachment doc, name - docid = escape_docid(doc['_id']) + if doc.is_a?(String) + puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id" + docid = doc + rev = nil + else + docid = doc['_id'] + rev = doc['_rev'] + end + docid = escape_docid(docid) name = CGI.escape(name) - rev = "?rev=#{doc['_rev']}" if doc['_rev'] + rev = "?rev=#{doc['_rev']}" if rev "#{@root}/#{docid}/#{name}#{rev}" end diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index a6e46f8..59e09f1 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -30,6 +30,14 @@ module CouchRest @@database end + def id + self['_id'] + end + + def rev + self['_rev'] + end + # copies the document to a new id. If the destination id currently exists, a rev must be provided. # dest can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc # hash with a '_rev' key @@ -67,6 +75,7 @@ module CouchRest # saves an attachment directly to couchdb def put_attachment(name, file, options={}) + raise ArgumentError, "doc must be saved" unless self.rev raise ArgumentError, "doc.database required to put_attachment" unless database result = database.put_attachment(self, name, file, options) self['_rev'] = result['rev'] @@ -75,6 +84,7 @@ module CouchRest # returns an attachment's data def fetch_attachment(name) + raise ArgumentError, "doc must be saved" unless self.rev raise ArgumentError, "doc.database required to put_attachment" unless database database.fetch_attachment(self, name) end