diff --git a/Rakefile b/Rakefile index 6936814..2c4ee9e 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,7 @@ begin gemspec.description = "CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments." gemspec.email = "jchris@apache.org" gemspec.homepage = "http://github.com/couchrest/couchrest" - gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos"] + gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber"] gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"] gemspec.has_rdoc = true @@ -64,4 +64,4 @@ module Rake end Rake.remove_task("github:release") -Rake.remove_task("release") \ No newline at end of file +Rake.remove_task("release") diff --git a/history.txt b/history.txt index 3ac3c2c..b097d51 100644 --- a/history.txt +++ b/history.txt @@ -1,8 +1,12 @@ == Next Version * Major enhancements - + * Adds support for continuous replication (sauy7) + * Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber) + * Added a search method to CouchRest:Database to search the documents in a + given database. (Dave Farkas, Arnaud Berthomier, John Wood) * Minor enhancements + * Provide a description of the timeout error (John Wood) == 0.35 diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 38949db..25f2f46 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -96,15 +96,18 @@ module CouchRest def parse url case url - when /^https?:\/\/(.*)\/(.*)\/(.*)/ - host = $1 - db = $2 - docid = $3 - when /^https?:\/\/(.*)\/(.*)/ - host = $1 - db = $2 - when /^https?:\/\/(.*)/ - host = $1 + when /^(https?:\/\/)(.*)\/(.*)\/(.*)/ + scheme = $1 + host = $2 + db = $3 + docid = $4 + when /^(https?:\/\/)(.*)\/(.*)/ + scheme = $1 + host = $2 + db = $3 + when /^(https?:\/\/)(.*)/ + scheme = $1 + host = $2 when /(.*)\/(.*)\/(.*)/ host = $1 db = $2 @@ -117,9 +120,9 @@ module CouchRest end db = nil if db && db.empty? - + { - :host => host || "127.0.0.1:5984", + :host => (scheme || "http://") + (host || "127.0.0.1:5984"), :database => db, :doc => docid } diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 36597fc..1e7ecb3 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -44,7 +44,14 @@ module CouchRest CouchRest.get url end end - + + # Query a CouchDB-Lucene search view + def search(name, params={}) + # -> http://localhost:5984/yourdb/_fti/YourDesign/by_name?include_docs=true&q=plop*' + url = CouchRest.paramify_url "#{root}/_fti/#{name}", params + CouchRest.get url + end + # load a set of documents by passing an array of ids def get_bulk(ids) documents(:keys => ids, :include_docs => true) @@ -297,15 +304,13 @@ module CouchRest 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 + def replicate_from other_db, continuous=false + replicate other_db, continuous, :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 + def replicate_to other_db, continuous=false + replicate other_db, continuous, :source => name end # DELETE the database itself. This is not undoable and could be rather @@ -317,6 +322,19 @@ module CouchRest private + def replicate other_db, continuous, options + raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database) + raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source)) + payload = options + if options.has_key?(:target) + payload[:source] = other_db.root + else + payload[:target] = other_db.root + end + payload[:continuous] = continuous + CouchRest.post "#{@host}/_replicate", payload + end + def clear_extended_doc_fresh_cache ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) } end diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index e4ebc70..5548c65 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -42,7 +42,12 @@ module CouchRest # # Defaults are used if these options are not specified. def paginated_each(options, &block) - proxy = create_collection_proxy(options) + search = options.delete(:search) + unless search == true + proxy = create_collection_proxy(options) + else + proxy = create_search_collection_proxy(options) + end proxy.paginated_each(options, &block) end @@ -61,6 +66,11 @@ module CouchRest CollectionProxy.new(database, design_doc, view_name, view_options, self) end + def create_search_collection_proxy(options) + design_doc, search_name, search_options = parse_search_options(options) + CollectionProxy.new(@database, design_doc, search_name, search_options, self, :search) + end + def parse_view_options(options) design_doc = options.delete(:design_doc) raise ArgumentError, 'design_doc is required' if design_doc.nil? @@ -75,6 +85,18 @@ module CouchRest [design_doc, view_name, view_options] end + + def parse_search_options(options) + design_doc = options.delete(:design_doc) + raise ArgumentError, 'design_doc is required' if design_doc.nil? + + search_name = options.delete(:view_name) + raise ArgumentError, 'search_name is required' if search_name.nil? + + search_options = options.clone + [design_doc, search_name, search_options] + end + end class CollectionProxy @@ -91,11 +113,12 @@ module CouchRest # # The CollectionProxy provides support for paginating over a collection # via the paginate, and paginated_each methods. - def initialize(database, design_doc, view_name, view_options = {}, container_class = nil) + def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view) raise ArgumentError, "database is a required parameter" if database.nil? @database = database @container_class = container_class + @query_type = query_type strip_pagination_options(view_options) @view_options = view_options @@ -110,10 +133,22 @@ module CouchRest # See Collection.paginate def paginate(options = {}) page, per_page = parse_options(options) - results = @database.view(@view_name, pagination_options(page, per_page)) + results = @database.send(@query_type, @view_name, pagination_options(page, per_page)) remember_where_we_left_off(results, page) - results = convert_to_container_array(results) - results + instances = convert_to_container_array(results) + + begin + if Kernel.const_get('WillPaginate') + total_rows = results['total_rows'].to_i + paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager| + pager.replace(instances) + end + return paginated + end + rescue NameError + # When not using will_paginate, not much we could do about this. :x + end + return instances end # See Collection.paginated_each @@ -152,7 +187,8 @@ module CouchRest def load_target unless loaded? - results = @database.view(@view_name, @view_options) + @view_options.merge!({:include_docs => true}) if @query_type == :search + results = @database.send(@query_type, @view_name, @view_options) @target = convert_to_container_array(results) end @loaded = true @@ -189,7 +225,7 @@ module CouchRest def pagination_options(page, per_page) view_options = @view_options.clone - if @last_key && @last_docid && @last_page == page - 1 + if @query_type == :view && @last_key && @last_docid && @last_page == page - 1 key = view_options.delete(:key) end_key = view_options[:endkey] || key options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 } diff --git a/lib/couchrest/monkeypatches.rb b/lib/couchrest/monkeypatches.rb index 95c52c3..c48ac7c 100644 --- a/lib/couchrest/monkeypatches.rb +++ b/lib/couchrest/monkeypatches.rb @@ -39,7 +39,7 @@ if RUBY_VERSION.to_f < 1.9 if IO.select([@io], nil, nil, @read_timeout) retry else - raise Timeout::Error + raise Timeout::Error, "IO timeout" end end else diff --git a/spec/couchrest/core/couchrest_spec.rb b/spec/couchrest/core/couchrest_spec.rb index faf6847..0947fa1 100644 --- a/spec/couchrest/core/couchrest_spec.rb +++ b/spec/couchrest/core/couchrest_spec.rb @@ -46,137 +46,74 @@ describe CouchRest do it "should parse just a dbname" do db = CouchRest.parse "my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5984" + db[:host].should == "http://127.0.0.1:5984" end it "should parse a host and db" do db = CouchRest.parse "127.0.0.1/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1" - end - it "should parse a host and db with http" do - db = CouchRest.parse "https://127.0.0.1/my-db" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1" - end - it "should parse a host with a port and db" do - db = CouchRest.parse "127.0.0.1:5555/my-db" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - end - it "should parse a host with a port and db with http" do - db = CouchRest.parse "http://127.0.0.1:5555/my-db" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - end - it "should parse a host with a port and db with https" do - db = CouchRest.parse "https://127.0.0.1:5555/my-db" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - end - it "should parse just a host" do - db = CouchRest.parse "http://127.0.0.1:5555/" - db[:database].should be_nil - db[:host].should == "127.0.0.1:5555" - end - it "should parse just a host with https" do - db = CouchRest.parse "https://127.0.0.1:5555/" - db[:database].should be_nil - db[:host].should == "127.0.0.1:5555" - end - it "should parse just a host no slash" do - db = CouchRest.parse "http://127.0.0.1:5555" - db[:host].should == "127.0.0.1:5555" - db[:database].should be_nil - end - it "should parse just a host no slash and https" do - db = CouchRest.parse "https://127.0.0.1:5555" - db[:host].should == "127.0.0.1:5555" - db[:database].should be_nil - end - it "should get docid" do - db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - db[:doc].should == "my-doc" - end - it "should get docid with http" do - db = CouchRest.parse "http://127.0.0.1:5555/my-db/my-doc" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - db[:doc].should == "my-doc" - end - it "should get docid with https" do - db = CouchRest.parse "https://127.0.0.1:5555/my-db/my-doc" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" - db[:doc].should == "my-doc" - end - it "should parse a host and db" do - db = CouchRest.parse "127.0.0.1/my-db" - db[:database].should == "my-db" - db[:host].should == "127.0.0.1" + db[:host].should == "http://127.0.0.1" end it "should parse a host and db with http" do db = CouchRest.parse "http://127.0.0.1/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1" + db[:host].should == "http://127.0.0.1" end it "should parse a host and db with https" do db = CouchRest.parse "https://127.0.0.1/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1" + db[:host].should == "https://127.0.0.1" end it "should parse a host with a port and db" do db = CouchRest.parse "127.0.0.1:5555/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" end it "should parse a host with a port and db with http" do db = CouchRest.parse "http://127.0.0.1:5555/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" end it "should parse a host with a port and db with https" do - db = CouchRest.parse "http://127.0.0.1:5555/my-db" + db = CouchRest.parse "https://127.0.0.1:5555/my-db" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "https://127.0.0.1:5555" end it "should parse just a host" do db = CouchRest.parse "http://127.0.0.1:5555/" db[:database].should be_nil - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" end it "should parse just a host with https" do db = CouchRest.parse "https://127.0.0.1:5555/" db[:database].should be_nil - db[:host].should == "127.0.0.1:5555" + db[:host].should == "https://127.0.0.1:5555" end it "should parse just a host no slash" do db = CouchRest.parse "http://127.0.0.1:5555" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" db[:database].should be_nil end it "should parse just a host no slash and https" do db = CouchRest.parse "https://127.0.0.1:5555" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "https://127.0.0.1:5555" db[:database].should be_nil end it "should get docid" do db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" db[:doc].should == "my-doc" end it "should get docid with http" do db = CouchRest.parse "http://127.0.0.1:5555/my-db/my-doc" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "http://127.0.0.1:5555" db[:doc].should == "my-doc" end it "should get docid with https" do db = CouchRest.parse "https://127.0.0.1:5555/my-db/my-doc" db[:database].should == "my-db" - db[:host].should == "127.0.0.1:5555" + db[:host].should == "https://127.0.0.1:5555" db[:doc].should == "my-doc" end end @@ -185,7 +122,7 @@ describe CouchRest do it "should be possible without an explicit CouchRest instantiation" do db = CouchRest.database "http://127.0.0.1:5984/couchrest-test" db.should be_an_instance_of(CouchRest::Database) - db.host.should == "127.0.0.1:5984" + db.host.should == "http://127.0.0.1:5984" end # TODO add https support (need test environment...) # it "should work with https" # do diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 28a57dd..97347b5 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -703,12 +703,12 @@ describe CouchRest::Database do end end - describe "replicating a database" do + describe "simply replicating a database" do before do @db.save_doc({'_id' => 'test_doc', 'some-value' => 'foo'}) - @other_db = @cr.database 'couchrest-test-replication' + @other_db = @cr.database REPLICATIONDB @other_db.delete! rescue nil - @other_db = @cr.create_db 'couchrest-test-replication' + @other_db = @cr.create_db REPLICATIONDB end describe "via pulling" do @@ -733,6 +733,53 @@ describe CouchRest::Database do end end end + + describe "continuously replicating a database" do + before do + @db.save_doc({'_id' => 'test_doc', 'some-value' => 'foo'}) + @other_db = @cr.database REPLICATIONDB + @other_db.delete! rescue nil + @other_db = @cr.create_db REPLICATIONDB + end + + describe "via pulling" do + before do + @other_db.replicate_from @db, true + end + + it "contains the document from the original database" do + sleep(1) # Allow some time to replicate + doc = @other_db.get('test_doc') + doc['some-value'].should == 'foo' + end + + it "contains documents saved after replication initiated" do + @db.save_doc({'_id' => 'test_doc_after', 'some-value' => 'bar'}) + sleep(1) # Allow some time to replicate + doc = @other_db.get('test_doc_after') + doc['some-value'].should == 'bar' + end + end + + describe "via pushing" do + before do + @db.replicate_to @other_db, true + end + + it "copies the document to the other database" do + sleep(1) # Allow some time to replicate + doc = @other_db.get('test_doc') + doc['some-value'].should == 'foo' + end + + it "copies documents saved after replication initiated" do + @db.save_doc({'_id' => 'test_doc_after', 'some-value' => 'bar'}) + sleep(1) # Allow some time to replicate + doc = @other_db.get('test_doc_after') + doc['some-value'].should == 'bar' + end + end + end describe "creating a database" do before(:each) do @@ -769,5 +816,25 @@ describe CouchRest::Database do end + describe "searching a database" do + before(:each) do + search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'}, + 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); ret.add(doc['age'], {'field':'age'}); return ret; }" } + @db.save_doc({'_id' => '_design/search', 'fulltext' => {'people' => search_function}}) + @db.save_doc({'_id' => 'john', 'name' => 'John', 'age' => '31'}) + @db.save_doc({'_id' => 'jack', 'name' => 'Jack', 'age' => '32'}) + @db.save_doc({'_id' => 'dave', 'name' => 'Dave', 'age' => '33'}) + end + + it "should be able to search a database using couchdb-lucene" do + if couchdb_lucene_available? + result = @db.search('search/people', :q => 'name:J*') + doc_ids = result['rows'].collect{ |row| row['id'] } + doc_ids.size.should == 2 + doc_ids.should include('john') + doc_ids.should include('jack') + end + end + end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index f4ac00c..c28b2cf 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -762,4 +762,36 @@ describe "ExtendedDocument" do cat.save.should be_true end end + + describe "searching the contents of an extended document" do + before :each do + @db = reset_test_db! + + names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"] + names.each { |name| Cat.create(:name => name) } + + search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'}, + 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" } + @db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}}) + end + + it "should be able to paginate through a large set of search results" do + if couchdb_lucene_available? + names = [] + Cat.paginated_each(:design_doc => "search", :view_name => "cats", + :q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat| + cat.should_not be_nil + names << cat.name + end + + names.size.should == 5 + names.should include('Sockington') + names.should include('Smitty') + names.should include('Sammy') + names.should include('Samson') + names.should include('Simon') + end + end + end + end diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 71adfcd..c545ee0 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -172,7 +172,7 @@ describe "ExtendedDocument properties" do describe "when type primitive is an Object" do it "it should not cast given value" do @course.participants = [{}, 'q', 1] - @course['participants'].should eql([{}, 'q', 1]) + @course['participants'].should == [{}, 'q', 1] end it "should cast started_on to Date" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e750258..5574305 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,7 @@ unless defined?(FIXTURE_PATH) COUCHHOST = "http://127.0.0.1:5984" TESTDB = 'couchrest-test' + REPLICATIONDB = 'couchrest-test-replication' TEST_SERVER = CouchRest.new TEST_SERVER.default_database = TESTDB DB = TEST_SERVER.database(TESTDB) @@ -34,4 +35,15 @@ Spec::Runner.configure do |config| cr.database(db).delete! rescue nil end end -end \ No newline at end of file +end + +def couchdb_lucene_available? + lucene_path = "http://localhost:5985/" + url = URI.parse(lucene_path) + req = Net::HTTP::Get.new(url.path) + res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) } + true + rescue Exception => e + false +end +