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
This commit is contained in:
Matt Aimonetti 2009-01-28 22:55:42 -08:00
parent a5f17cb13f
commit 6b2e5f84ad
7 changed files with 388 additions and 248 deletions

View file

@ -37,6 +37,8 @@ module CouchRest
autoload :FileManager, 'couchrest/helper/file_manager' autoload :FileManager, 'couchrest/helper/file_manager'
autoload :Streamer, 'couchrest/helper/streamer' autoload :Streamer, 'couchrest/helper/streamer'
require File.join(File.dirname(__FILE__), 'couchrest', 'mixins')
# The CouchRest module methods handle the basic JSON serialization # The CouchRest module methods handle the basic JSON serialization
# and deserialization, as well as query parameters. The module also includes # and deserialization, as well as query parameters. The module also includes
# some helpers for tasks like instantiating a new Database or Server instance. # some helpers for tasks like instantiating a new Database or Server instance.

View file

@ -14,62 +14,39 @@ module CouchRest
end end
class Document < Response class Document < Response
include CouchRest::Mixins::Views
attr_accessor :database attr_accessor :database
@@database = nil
# alias for self['_id'] # override the CouchRest::Model-wide default_database
def id # This is not a thread safe operation, do not change the model
self['_id'] # database at runtime.
def self.use_database(db)
@@database = db
end end
# alias for self['_rev'] def self.database
def rev @@database
self['_rev']
end end
# returns true if the document has never been saved # Returns the CouchDB uri for the document
def new_document? def uri(append_rev = false)
!rev return nil if new_document?
end couch_uri = "http://#{database.uri}/#{CGI.escape(id)}"
if append_rev == true
# Saves the document to the db using create or update. Also runs the :save couch_uri << "?rev=#{rev}"
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on elsif append_rev.kind_of?(Integer)
# CouchDB's response. couch_uri << "?rev=#{append_rev}"
# If <tt>bulk</tt> is <tt>true</tt> (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 <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
# document to be saved to a new <tt>_id</tt>.
# If <tt>bulk</tt> is <tt>true</tt> (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 end
result['ok'] couch_uri
end end
def copy(dest) # Returns the document's database
raise ArgumentError, "doc.database required to copy" unless database def database
result = database.copy_doc(self, dest) @database || self.class.database
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
end end

View file

@ -1,25 +1,62 @@
module CouchRest module CouchRest
class Server class Server
attr_accessor :uri, :uuid_batch_count attr_accessor :uri, :uuid_batch_count, :available_databases
def initialize server = 'http://127.0.0.1:5984', uuid_batch_count = 1000 def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000)
@uri = server @uri = server
@uuid_batch_count = uuid_batch_count @uuid_batch_count = uuid_batch_count
end 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 def databases
CouchRest.get "#{@uri}/_all_dbs" CouchRest.get "#{@uri}/_all_dbs"
end end
# Returns a CouchRest::Database for the given name # Returns a CouchRest::Database for the given name
def database name def database(name)
CouchRest::Database.new(self, name) CouchRest::Database.new(self, name)
end end
# Creates the database if it doesn't exist # Creates the database if it doesn't exist
def database! name def database!(name)
create_db(name) rescue nil create_db(name) rescue nil
database name database(name)
end end
# GET the welcome message # GET the welcome message
@ -28,9 +65,9 @@ module CouchRest
end end
# Create a database # Create a database
def create_db name def create_db(name)
CouchRest.put "#{@uri}/#{name}" CouchRest.put "#{@uri}/#{name}"
database name database(name)
end end
# Restart the CouchDB instance # Restart the CouchDB instance
@ -39,7 +76,7 @@ module CouchRest
end end
# Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs. # 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 ||= [] @uuids ||= []
if @uuids.empty? if @uuids.empty?
@uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"] @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]

3
lib/couchrest/mixins.rb Normal file
View file

@ -0,0 +1,3 @@
mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
require File.join(mixins_dir, 'views')

View file

@ -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 <tt>_id</tt> and <tt>_rev</tt> fields based on
# CouchDB's response.
# If <tt>bulk</tt> is <tt>true</tt> (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 <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
# document to be saved to a new <tt>_id</tt>.
# If <tt>bulk</tt> is <tt>true</tt> (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

View file

@ -1,213 +1,241 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.dirname(__FILE__) + '/../../spec_helper'
describe CouchRest::Document, "[]=" do class Video < CouchRest::Document; end
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
describe CouchRest::Document, "new" do describe CouchRest::Document 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
before(:all) do before(:all) do
@doc = CouchRest::Document.new("key" => [1,2,3], :more => "values") @couch = CouchRest.new
@db = reset_test_db! @db = @couch.database!(TESTDB)
@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!
end end
it "should use the document bulk save cache" do describe "[]=" do
doc = CouchRest::Document.new({"_id" => "bulkdoc", "val" => 3}) before(:each) do
doc.database = @db @doc = CouchRest::Document.new
doc.save(true) end
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
it "should work" do it "should work" do
@doc.copy @docid @doc["enamel"].should == nil
newdoc = @db.get(@docid) @doc["enamel"] = "Strong"
newdoc['key'].should == 'value' @doc["enamel"].should == "Strong"
end end
it "should fail without a database" do it "[]= should convert to string" do
lambda{CouchRest::Document.new({"not"=>"a real doc"}).copy}.should raise_error(ArgumentError) @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
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 describe "default database" do
before :each do it "should be set using use_database on the model" do
@db = reset_test_db! Video.new.database.should be_nil
@resp = @db.save_doc({'key' => 'value'}) Video.use_database @db
@docid = 'new-location' Video.new.database.should == @db
@doc = @db.get(@resp['id']) Video.use_database nil
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 end
it "should fail without a database" do
lambda{CouchRest::Document.new({"not"=>"a real doc"}).move}.should raise_error(ArgumentError) it "should be overwritten by instance" do
lambda{CouchRest::Document.new({"_id"=>"not a real doc"}).move}.should raise_error(ArgumentError) 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
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 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 end
it "should fail without a rev" do describe "to a new location" do
lambda{@doc.move @docid}.should raise_error(RestClient::RequestFailed) it "should work" do
lambda{@db.get(@resp['id'])}.should_not raise_error @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 end
it "should succeed with a rev" do describe "to an existing location" do
@to_be_overwritten = @db.get(@docid) before :each do
@doc.move "#{@docid}?rev=#{@to_be_overwritten['_rev']}" @db.save_doc({'_id' => @docid, 'will-exist' => 'here'})
newdoc = @db.get(@docid) end
newdoc['key'].should == 'value' it "should fail without a rev" do
lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) 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
it "should succeed given the doc to overwrite" do end
@to_be_overwritten = @db.get(@docid)
@doc.move @to_be_overwritten describe "MOVE existing document" do
newdoc = @db.get(@docid) before :each do
newdoc['key'].should == 'value' @db = reset_test_db!
lambda {@db.get(@resp['id'])}.should raise_error(RestClient::ResourceNotFound) @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 end
end end

View file

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