From d9fe6ba374acf13b0a78395a36801c0cbec20d45 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 28 Jan 2009 22:55:42 -0800 Subject: [PATCH] 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